Skip to content
39 changes: 28 additions & 11 deletions internal/convert/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const (
)

var (
dynamicBlockAllowList = []string{nTags, nLabels, nConfigSrc}
dynamicBlockAllowList = []string{nTags, nLabels, nConfigSrc, nRepSpecs}
)

type attrVals struct {
Expand Down Expand Up @@ -91,17 +91,22 @@ func convertResource(block *hclwrite.Block) (bool, error) {
}

var err error
if blockb.FirstMatchingBlock(nRepSpecs, nil) != nil {
err = fillCluster(blockb)
} else {
if isFreeTierCluster(blockb) {
err = fillFreeTierCluster(blockb)
} else {
err = fillCluster(blockb)
}
if err != nil {
return false, err
}
return true, nil
}

func isFreeTierCluster(resourceb *hclwrite.Body) bool {
d, _ := getDynamicBlock(resourceb, nRepSpecs)
return resourceb.FirstMatchingBlock(nRepSpecs, nil) == nil && !d.IsPresent()
}

func convertDataSource(block *hclwrite.Block) bool {
if block.Type() != dataSourceType {
return false
Expand Down Expand Up @@ -190,6 +195,15 @@ func fillCluster(resourceb *hclwrite.Body) error {
}

func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error {
d, err := fillReplicationSpecsWithDynamicBlock(resourceb, root)
if err != nil {
return err
}
if d.IsPresent() {
resourceb.RemoveBlock(d.block)
resourceb.SetAttributeRaw(nRepSpecs, d.tokens)
return nil
}
// at least one replication_specs exists here, if not it would be a free tier cluster
var specbs []*hclwrite.Body
for {
Expand Down Expand Up @@ -312,6 +326,15 @@ func fillBlockOpt(resourceb *hclwrite.Body, name string) {
resourceb.SetAttributeRaw(name, hcl.TokensObject(block.Body()))
}

// fillReplicationSpecsWithDynamicBlock used for dynamic blocks in replication_specs
func fillReplicationSpecsWithDynamicBlock(resourceb *hclwrite.Body, root attrVals) (dynamicBlock, error) {
d, err := getDynamicBlock(resourceb, nRepSpecs)
if err != nil || !d.IsPresent() {
return dynamicBlock{}, err
}
return d, nil
}

// fillReplicationSpecsWithDynamicRegionConfigs is used for dynamic blocks in region_configs
func fillReplicationSpecsWithDynamicRegionConfigs(specbSrc *hclwrite.Body, root attrVals) (dynamicBlock, error) {
d, err := getDynamicBlock(specbSrc, nConfigSrc)
Expand Down Expand Up @@ -414,7 +437,7 @@ func getSpecs(configSrc *hclwrite.Block, countName string, root attrVals, isDyna
}
tokens := hcl.TokensObject(fileb)
if isDynamicBlock {
tokens = encloseDynamicBlockRegionSpec(tokens, countName)
tokens = append(hcl.TokensFromExpr(fmt.Sprintf("%s.%s == 0 ? null :", nRegion, countName)), tokens...)
}
return tokens, nil
}
Expand Down Expand Up @@ -520,12 +543,6 @@ func replaceDynamicBlockExpr(attr *hclwrite.Attribute, blockName, attrName strin
return strings.ReplaceAll(expr, fmt.Sprintf("%s.%s", blockName, attrName), attrName)
}

func encloseDynamicBlockRegionSpec(specTokens hclwrite.Tokens, countName string) hclwrite.Tokens {
tokens := hcl.TokensFromExpr(fmt.Sprintf("%s.%s > 0 ?", nRegion, countName))
tokens = append(tokens, specTokens...)
return append(tokens, hcl.TokensFromExpr(": null")...)
}

// getDynamicBlockRegionConfigsRegionArray returns the region array for a dynamic block in replication_specs.
// e.g. [ for region in var.replication_specs.regions_config : { ... } if priority == region.priority ]
func getDynamicBlockRegionConfigsRegionArray(d dynamicBlock, root attrVals) (hclwrite.Tokens, error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,27 @@ resource "mongodbatlas_advanced_cluster" "cluster" {
provider_name = var.provider_name
region_name = region.region_name
priority = region.priority
electable_specs = region.electable_nodes > 0 ? {
electable_specs = region.electable_nodes == 0 ? null : {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: easier to read as we only need to change the first line of the object but not the last one

Copy link
Contributor

@EspenAlbert EspenAlbert Mar 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible that any of these are nullable? (electable_nodes, read_only_nodes, etc.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, read_only_nodes for sure, and in the latest PR I also allow electable_nodes to be null (e.g. a region only with read-only nodes)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lantoli, will the comparison crash if region.read_only_nodes are null?

node_count = region.electable_nodes
instance_size = var.provider_instance_size_name
disk_size_gb = var.disk_size_gb
ebs_volume_type = var.provider_volume_type
disk_iops = var.provider_disk_iops
} : null
read_only_specs = region.read_only_nodes > 0 ? {
}
read_only_specs = region.read_only_nodes == 0 ? null : {
node_count = region.read_only_nodes
instance_size = var.provider_instance_size_name
disk_size_gb = var.disk_size_gb
ebs_volume_type = var.provider_volume_type
disk_iops = var.provider_disk_iops
} : null
analytics_specs = region.analytics_nodes > 0 ? {
}
analytics_specs = region.analytics_nodes == 0 ? null : {
node_count = region.analytics_nodes
instance_size = var.provider_instance_size_name
disk_size_gb = var.disk_size_gb
ebs_volume_type = var.provider_volume_type
disk_iops = var.provider_disk_iops
} : null
}
auto_scaling = {
disk_gb_enabled = var.auto_scaling_disk_gb_enabled
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ resource "mongodbatlas_advanced_cluster" "dynamic_regions_config" {
provider_name = "AWS"
region_name = region.region_name
priority = region.prio
electable_specs = region.electable_nodes > 0 ? {
electable_specs = region.electable_nodes == 0 ? null : {
node_count = region.electable_nodes
instance_size = "M10"
} : null
read_only_specs = region.read_only_nodes > 0 ? {
}
read_only_specs = region.read_only_nodes == 0 ? null : {
node_count = region.read_only_nodes
instance_size = "M10"
} : null
}
} if priority == region.prio
]
])
Expand Down
87 changes: 87 additions & 0 deletions internal/convert/testdata/clu2adv/dynamic_replication_specs.in.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Based on https://github.com/mongodb/terraform-provider-mongodbatlas/blob/master/examples/migrate_cluster_to_advanced_cluster/module_maintainer/v1/main.tf
resource "mongodbatlas_cluster" "this" {
lifecycle {
precondition {
condition = !(var.auto_scaling_disk_gb_enabled && var.disk_size > 0)
error_message = "Must use either auto_scaling_disk_gb_enabled or disk_size, not both."
}
}

project_id = var.project_id
name = var.cluster_name
auto_scaling_disk_gb_enabled = var.auto_scaling_disk_gb_enabled
cluster_type = var.cluster_type
disk_size_gb = var.disk_size
mongo_db_major_version = var.mongo_db_major_version
provider_instance_size_name = var.instance_size
provider_name = var.provider_name

dynamic "tags" {
for_each = var.tags
content {
key = tags.key
value = tags.value
}
}

dynamic "replication_specs" {
for_each = var.replication_specs
content {
num_shards = replication_specs.value.num_shards
zone_name = replication_specs.value.zone_name

dynamic "regions_config" {
for_each = replication_specs.value.regions_config
content {
electable_nodes = regions_config.value.electable_nodes
priority = regions_config.value.priority
read_only_nodes = regions_config.value.read_only_nodes
region_name = regions_config.value.region_name
}
}
}
}
}

# example of variable for demostration purposes, not used in the conversion
variable "replication_specs" {
description = "List of replication specifications in mongodbatlas_cluster format"
type = list(object({
num_shards = number
zone_name = string
regions_config = list(object({
region_name = string
electable_nodes = number
priority = number
read_only_nodes = optional(number, 0)
}))
}))
default = [
{
num_shards = 1
zone_name = "Zone 1"
regions_config = [
{
region_name = "US_EAST_1"
electable_nodes = 3
priority = 7
}
]
}, {
num_shards = 2
zone_name = "Zone 2"
regions_config = [
{
region_name = "US_WEST_2"
electable_nodes = 2
priority = 6
read_only_nodes = 1
}, {
region_name = "EU_WEST_1"
electable_nodes = 3
priority = 7
}
]
}
]
}
89 changes: 89 additions & 0 deletions internal/convert/testdata/clu2adv/dynamic_replication_specs.out.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Based on https://github.com/mongodb/terraform-provider-mongodbatlas/blob/master/examples/migrate_cluster_to_advanced_cluster/module_maintainer/v1/main.tf
resource "mongodbatlas_advanced_cluster" "this" {
lifecycle {
precondition {
condition = !(var.auto_scaling_disk_gb_enabled && var.disk_size > 0)
error_message = "Must use either auto_scaling_disk_gb_enabled or disk_size, not both."
}
}

project_id = var.project_id
name = var.cluster_name
cluster_type = var.cluster_type
mongo_db_major_version = var.mongo_db_major_version

replication_specs = flatten([
for spec in var.replication_specs : [
for i in range(var.replication_specs.num_shards) : {
zone_name = var.zone_name
region_configs = flatten([
# Regions must be sorted by priority in descending order.
for priority in range(7, 0, -1) : [
for region in var.replication_specs.regions_config : {
provider_name = var.provider_name
region_name = region.region_name
priority = region.priority
electable_specs = region.electable_nodes == 0 ? null : {
node_count = region.electable_nodes
instance_size = var.instance_size
disk_size_gb = var.disk_size
}
read_only_specs = region.read_only_nodes == 0 ? null : {
node_count = region.read_only_nodes
instance_size = var.instance_size
disk_size_gb = var.disk_size
}
} if priority == region.priority
]
])
}
]
])
tags = var.tags

# Generated by atlas-cli-plugin-terraform.
# Please review the changes and confirm that references to this resource are updated.
}

# example of variable for demostration purposes, not used in the conversion
variable "replication_specs" {
description = "List of replication specifications in mongodbatlas_cluster format"
type = list(object({
num_shards = number
zone_name = string
regions_config = list(object({
region_name = string
electable_nodes = number
priority = number
read_only_nodes = optional(number, 0)
}))
}))
default = [
{
num_shards = 1
zone_name = "Zone 1"
regions_config = [
{
region_name = "US_EAST_1"
electable_nodes = 3
priority = 7
}
]
}, {
num_shards = 2
zone_name = "Zone 2"
regions_config = [
{
region_name = "US_WEST_2"
electable_nodes = 2
priority = 6
read_only_nodes = 1
}, {
region_name = "EU_WEST_1"
electable_nodes = 3
priority = 7
}
]
}
]
}
9 changes: 4 additions & 5 deletions internal/convert/testdata/clu2adv/errors.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
{
"autoscaling_missing_attribute": "setting replication_specs: attribute provider_instance_size_name not found",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ordered alphabetically

"configuration_file_error": "failed to parse Terraform config file",
"free_cluster_missing_attribute": "free cluster (because no replication_specs): attribute backing_provider_name not found",
"autoscaling_missing_attribute": "setting replication_specs: attribute provider_instance_size_name not found",
"replication_specs_missing_regions_config": "setting replication_specs: regions_config not found",
"regions_config_missing_priority": "setting replication_specs: attribute priority not found",
"replication_specs_unsupported_dynamic": "dynamic blocks are not supported",
"replication_specs_non_literal_num_shards": "setting num_shards: failed to evaluate number",
"replication_specs_missing_num_shards": "num_shards not found"
"replication_specs_missing_num_shards": "num_shards not found",
"replication_specs_missing_regions_config": "setting replication_specs: regions_config not found",
"replication_specs_non_literal_num_shards": "setting num_shards: failed to evaluate number"
}

This file was deleted.

Loading