From 5eceea1ecd538e7f9aa9a9f831a750ad5d3a183a Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Thu, 21 Aug 2025 09:47:27 +0200 Subject: [PATCH 01/43] tags & labels --- .../testdata/adv2v2/dynamic_tags_labels.in.tf | 164 ++++++++++++++++++ .../adv2v2/dynamic_tags_labels.out.tf | 156 +++++++++++++++++ 2 files changed, 320 insertions(+) create mode 100644 internal/convert/testdata/adv2v2/dynamic_tags_labels.in.tf create mode 100644 internal/convert/testdata/adv2v2/dynamic_tags_labels.out.tf diff --git a/internal/convert/testdata/adv2v2/dynamic_tags_labels.in.tf b/internal/convert/testdata/adv2v2/dynamic_tags_labels.in.tf new file mode 100644 index 0000000..dfbb4f5 --- /dev/null +++ b/internal/convert/testdata/adv2v2/dynamic_tags_labels.in.tf @@ -0,0 +1,164 @@ +resource "mongodbatlas_advanced_cluster" "simplified" { + project_id = var.project_id + name = "cluster" + cluster_type = "REPLICASET" + replication_specs { + region_configs { + priority = 7 + provider_name = "AWS" + region_name = "EU_WEST_1" + electable_specs { + instance_size = "M10" + node_count = 3 + } + } + } + dynamic "tags" { + for_each = local.tags + content { // simplified version where var can be used directly + key = tags.key + value = tags.value + } + } +} + +resource "mongodbatlas_advanced_cluster" "expression" { + project_id = var.project_id + name = "cluster" + cluster_type = "REPLICASET" + replication_specs { + region_configs { + priority = 7 + provider_name = "AWS" + region_name = "EU_WEST_1" + electable_specs { + instance_size = "M10" + node_count = 3 + } + } + } + dynamic "tags" { + for_each = local.tags + content { // using expressions + key = tags.key + value = replace(tags.value, "/", "_") + } + } +} + +resource "mongodbatlas_advanced_cluster" "simplified_individual" { + project_id = var.project_id + name = "cluster" + cluster_type = "REPLICASET" + replication_specs { + region_configs { + priority = 7 + provider_name = "AWS" + region_name = "EU_WEST_1" + electable_specs { + instance_size = "M10" + node_count = 3 + } + } + } + tags { // using individual tags apart from simplified version in dynamic tags + key = "tag1" + value = var.tag1val + } + dynamic "tags" { + for_each = var.tags + content { // simplified version where var can be used directly + key = tags.key + value = tags.value + } + } + tags { + key = "tag 2" + value = var.tag2val + } +} + +resource "mongodbatlas_advanced_cluster" "expression_individual" { + project_id = var.project_id + name = "cluster" + cluster_type = "REPLICASET" + replication_specs { + region_configs { + priority = 7 + provider_name = "AWS" + region_name = "EU_WEST_1" + electable_specs { + instance_size = "M10" + node_count = 3 + } + } + } + tags { // using individual tags apart from expressions in dynamic tags + key = "tag1" + value = var.tag1val + } + dynamic "tags" { + for_each = var.tags + content { // using expressions + key = tags.key + value = replace(tags.value, "/", "_") + } + } + tags { + key = "tag 2" + value = var.tag2val + } +} + +resource "mongodbatlas_advanced_cluster" "full_example" { + project_id = var.project_id + name = "cluster" + cluster_type = "REPLICASET" + replication_specs { + region_configs { + priority = 7 + provider_name = "AWS" + region_name = "EU_WEST_1" + electable_specs { + instance_size = "M10" + node_count = 3 + } + } + } + labels { + key = "label1" + value = "label1val" + } + labels { + key = "label2" + value = data.my_resource.my_data.value + } + dynamic "labels" { + for_each = local.tags + content { + key = labels.key + value = labels.value + } + } + tags { + key = "environment" + value = "dev" + } + tags { + key = var.tag_key # non-literal values are supported and enclosed in parentheses + value = var.tag_value + } + dynamic "tags" { + for_each = var.tags + content { + key = tags.key + value = replace(tags.value, "/", "_") + } + } + lifecycle { + precondition { + condition = local.use_new_replication_specs || !(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." + } + } +} diff --git a/internal/convert/testdata/adv2v2/dynamic_tags_labels.out.tf b/internal/convert/testdata/adv2v2/dynamic_tags_labels.out.tf new file mode 100644 index 0000000..cf16f57 --- /dev/null +++ b/internal/convert/testdata/adv2v2/dynamic_tags_labels.out.tf @@ -0,0 +1,156 @@ +resource "mongodbatlas_advanced_cluster" "simplified" { + project_id = var.project_id + name = "cluster" + cluster_type = "REPLICASET" + replication_specs = [ + { + region_configs = [ + { + priority = 7 + provider_name = "AWS" + region_name = "EU_WEST_1" + electable_specs = { + instance_size = "M10" + node_count = 3 + } + } + ] + } + ] + tags = local.tags + + # Updated by atlas-cli-plugin-terraform, please review the changes. +} + +resource "mongodbatlas_advanced_cluster" "expression" { + project_id = var.project_id + name = "cluster" + cluster_type = "REPLICASET" + replication_specs = [ + { + region_configs = [ + { + priority = 7 + provider_name = "AWS" + region_name = "EU_WEST_1" + electable_specs = { + instance_size = "M10" + node_count = 3 + } + } + ] + } + ] + tags = { + for key, value in local.tags : key => replace(value, "/", "_") + } + + # Updated by atlas-cli-plugin-terraform, please review the changes. +} + +resource "mongodbatlas_advanced_cluster" "simplified_individual" { + project_id = var.project_id + name = "cluster" + cluster_type = "REPLICASET" + replication_specs = [ + { + region_configs = [ + { + priority = 7 + provider_name = "AWS" + region_name = "EU_WEST_1" + electable_specs = { + instance_size = "M10" + node_count = 3 + } + } + ] + } + ] + tags = merge( + var.tags, + { + tag1 = var.tag1val + "tag 2" = var.tag2val + } + ) + + # Updated by atlas-cli-plugin-terraform, please review the changes. +} + +resource "mongodbatlas_advanced_cluster" "expression_individual" { + project_id = var.project_id + name = "cluster" + cluster_type = "REPLICASET" + replication_specs = [ + { + region_configs = [ + { + priority = 7 + provider_name = "AWS" + region_name = "EU_WEST_1" + electable_specs = { + instance_size = "M10" + node_count = 3 + } + } + ] + } + ] + tags = merge( + { + for key, value in var.tags : key => replace(value, "/", "_") + }, + { + tag1 = var.tag1val + "tag 2" = var.tag2val + } + ) + + # Updated by atlas-cli-plugin-terraform, please review the changes. +} + +resource "mongodbatlas_advanced_cluster" "full_example" { + project_id = var.project_id + name = "cluster" + cluster_type = "REPLICASET" + lifecycle { + precondition { + condition = local.use_new_replication_specs || !(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." + } + } + replication_specs = [ + { + region_configs = [ + { + priority = 7 + provider_name = "AWS" + region_name = "EU_WEST_1" + electable_specs = { + instance_size = "M10" + node_count = 3 + } + } + ] + } + ] + tags = merge( + { + for key, value in var.tags : key => replace(value, "/", "_") + }, + { + environment = "dev" + (var.tag_key) = var.tag_value + } + ) + labels = merge( + local.tags, + { + label1 = "label1val" + label2 = data.my_resource.my_data.value + } + ) + + # Updated by atlas-cli-plugin-terraform, please review the changes. +} From 552f37b8f10a94909e3f1c46593785d6297f9190 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Thu, 21 Aug 2025 11:19:10 +0200 Subject: [PATCH 02/43] failing region_configs test --- .../adv2v2/dynamic_regions_config_basic.in.tf | 51 ++++++++++++++++++ .../dynamic_regions_config_basic.out.tf | 53 +++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 internal/convert/testdata/adv2v2/dynamic_regions_config_basic.in.tf create mode 100644 internal/convert/testdata/adv2v2/dynamic_regions_config_basic.out.tf diff --git a/internal/convert/testdata/adv2v2/dynamic_regions_config_basic.in.tf b/internal/convert/testdata/adv2v2/dynamic_regions_config_basic.in.tf new file mode 100644 index 0000000..5a76fcf --- /dev/null +++ b/internal/convert/testdata/adv2v2/dynamic_regions_config_basic.in.tf @@ -0,0 +1,51 @@ +resource "mongodbatlas_advanced_cluster" "dynamic_regions_config" { + project_id = var.project_id + name = "cluster" + cluster_type = "SHARDED" + replication_specs { + num_shards = var.replication_specs.num_shards + zone_name = var.zone_name + dynamic "region_configs" { + for_each = var.replication_specs.region_configs + content { + priority = region_configs.value.prio + provider_name = "AWS" + region_name = region_configs.value.region_name + electable_specs { + instance_size = region_configs.value.instance_size + node_count = region_configs.value.node_count + } + } + } + } +} + +# example of variable for demostration purposes, not used in the conversion +variable "replication_specs" { + type = object({ + num_shards = number + region_configs = list(object({ + prio = number + region_name = string + instance_size = string + node_count = number + })) + }) + default = { + num_shards = 3 + region_configs = [ + { + prio = 7 + region_name = "US_EAST_1" + instance_size = "M10" + node_count = 2 + }, + { + prio = 6 + region_name = "US_WEST_2" + instance_size = "M10" + node_count = 1 + } + ] + } +} diff --git a/internal/convert/testdata/adv2v2/dynamic_regions_config_basic.out.tf b/internal/convert/testdata/adv2v2/dynamic_regions_config_basic.out.tf new file mode 100644 index 0000000..0ffde3f --- /dev/null +++ b/internal/convert/testdata/adv2v2/dynamic_regions_config_basic.out.tf @@ -0,0 +1,53 @@ +resource "mongodbatlas_advanced_cluster" "dynamic_regions_config" { + project_id = var.project_id + name = "cluster" + cluster_type = "SHARDED" + replication_specs = [ + for i in range(var.replication_specs.num_shards) : { + zone_name = var.zone_name + region_configs = [ + for region in var.replication_specs.region_configs : { + priority = region.prio + provider_name = "AWS" + region_name = region.region_name + electable_specs = { + instance_size = region.instance_size + node_count = region.node_count + } + } + ] + } + ] + + # Updated by atlas-cli-plugin-terraform, please review the changes. +} + +# example of variable for demostration purposes, not used in the conversion +variable "replication_specs" { + type = object({ + num_shards = number + region_configs = list(object({ + prio = number + region_name = string + instance_size = string + node_count = number + })) + }) + default = { + num_shards = 3 + region_configs = [ + { + prio = 7 + region_name = "US_EAST_1" + instance_size = "M10" + node_count = 2 + }, + { + prio = 6 + region_name = "US_WEST_2" + instance_size = "M10" + node_count = 1 + } + ] + } +} From 8fc0daa3ba6ad59ee490436cbacfcbd0459c6333 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Thu, 21 Aug 2025 13:53:21 +0200 Subject: [PATCH 03/43] failing replication_specs test --- .../dynamic_replication_specs_basic.in.tf | 81 ++++++++++++++++++ .../dynamic_replication_specs_basic.out.tf | 83 +++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.in.tf create mode 100644 internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.out.tf diff --git a/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.in.tf b/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.in.tf new file mode 100644 index 0000000..dd968ef --- /dev/null +++ b/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.in.tf @@ -0,0 +1,81 @@ +resource "mongodbatlas_advanced_cluster" "dynamic_replication_specs" { + project_id = var.project_id + name = var.cluster_name + cluster_type = "GEOSHARDED" + dynamic "replication_specs" { + for_each = var.replication_specs + content { + num_shards = replication_specs.value.num_shards + zone_name = replication_specs.value.zone_name + dynamic "region_configs" { + for_each = replication_specs.value.region_configs + content { + priority = region_configs.value.priority + provider_name = region_configs.value.provider_name + region_name = region_configs.value.region_name + electable_specs { + instance_size = region_configs.value.instance_size + node_count = region_configs.value.electable_node_count + } + read_only_specs { + instance_size = region_configs.value.instance_size + node_count = region_configs.value.read_only_node_count + } + } + } + } + } +} + +# example of variable for demostration purposes, not used in the conversion +variable "replication_specs" { + description = "List of replication specifications in mongodbatlas_advanced_cluster format" + type = list(object({ + num_shards = number + zone_name = string + region_configs = list(object({ + provider_name = string + region_name = string + instance_size = string + electable_node_count = number + read_only_node_count = number + priority = number + })) + })) + default = [ + { + num_shards = 1 + zone_name = "Zone A" + region_configs = [ + { + provider_name = "AWS" + region_name = "US_EAST_1" + instance_size = "M10" + electable_node_count = 3 + read_only_node_count = 0 + priority = 7 + } + ] + }, { + num_shards = 2 + zone_name = "Zone B" + region_configs = [ + { + provider_name = "AWS" + region_name = "US_WEST_2" + instance_size = "M10" + electable_node_count = 2 + read_only_node_count = 1 + priority = 7 + }, { + provider_name = "AWS" + region_name = "EU_WEST_1" + instance_size = "M10" + electable_node_count = 1 + read_only_node_count = 0 + priority = 6 + } + ] + } + ] +} diff --git a/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.out.tf b/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.out.tf new file mode 100644 index 0000000..61a0c60 --- /dev/null +++ b/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.out.tf @@ -0,0 +1,83 @@ +resource "mongodbatlas_advanced_cluster" "dynamic_replication_specs" { + project_id = var.project_id + name = var.cluster_name + cluster_type = "GEOSHARDED" + replication_specs = flatten([ + for spec in var.replication_specs : [ + for i in range(spec.num_shards) : { + zone_name = spec.zone_name + region_configs = [ + for region in spec.region_configs : { + provider_name = region.provider_name + region_name = region.region_name + priority = region.priority + electable_specs = { + node_count = region.electable_node_count + instance_size = region.instance_size + } + read_only_specs = { + node_count = region.read_only_node_count + instance_size = region.instance_size + } + } + ] + } + ] + ]) + + # Updated by atlas-cli-plugin-terraform, please review the changes. +} + + +# example of variable for demostration purposes, not used in the conversion +variable "replication_specs" { + description = "List of replication specifications in mongodbatlas_advanced_cluster format" + type = list(object({ + num_shards = number + zone_name = string + region_configs = list(object({ + provider_name = string + region_name = string + instance_size = string + electable_node_count = number + read_only_node_count = number + priority = number + })) + })) + default = [ + { + num_shards = 1 + zone_name = "Zone A" + region_configs = [ + { + provider_name = "AWS" + region_name = "US_EAST_1" + instance_size = "M10" + electable_node_count = 3 + read_only_node_count = 0 + priority = 7 + } + ] + }, { + num_shards = 2 + zone_name = "Zone B" + region_configs = [ + { + provider_name = "AWS" + region_name = "US_WEST_2" + instance_size = "M10" + electable_node_count = 2 + read_only_node_count = 1 + priority = 7 + }, { + provider_name = "AWS" + region_name = "EU_WEST_1" + instance_size = "M10" + electable_node_count = 1 + read_only_node_count = 0 + priority = 6 + } + ] + } + ] +} From f33ed40187286cf26c11ec2a2b00ea7c3fa9949c Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Thu, 21 Aug 2025 14:54:22 +0200 Subject: [PATCH 04/43] implementation for basic tests --- internal/convert/adv2v2.go | 339 +++++++++++++++++- .../dynamic_replication_specs_basic.out.tf | 15 +- 2 files changed, 336 insertions(+), 18 deletions(-) diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index f1c659e..05b3bfa 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -3,6 +3,7 @@ package convert import ( "fmt" "slices" + "strings" "github.com/hashicorp/hcl/v2/hclwrite" "github.com/mongodb-labs/atlas-cli-plugin-terraform/internal/hcl" @@ -58,6 +59,15 @@ func updateResource(resource *hclwrite.Block) (bool, error) { } func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error { + // Handle dynamic blocks for replication_specs + dSpec, err := getDynamicBlock(resourceb, nRepSpecs) + if err != nil { + return err + } + if dSpec.IsPresent() { + return convertDynamicRepSpecs(resourceb, dSpec, diskSizeGB) + } + var repSpecs []*hclwrite.Body for { block := resourceb.FirstMatchingBlock(nRepSpecs, nil) @@ -66,18 +76,42 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error } resourceb.RemoveBlock(block) blockb := block.Body() - numShardsVal := 1 // default to 1 if num_shards not present - if numShardsAttr := blockb.GetAttribute(nNumShards); numShardsAttr != nil { - var err error - if numShardsVal, err = hcl.GetAttrInt(numShardsAttr, errNumShards); err != nil { + + // Check if num_shards is a variable/expression + numShardsAttr := blockb.GetAttribute(nNumShards) + if numShardsAttr != nil { + numShardsVal, err := hcl.GetAttrInt(numShardsAttr, errNumShards) + if err != nil { + // num_shards is not a literal number, it's a variable/expression + // Need to use range() in the output + numShardsExpr := hcl.GetAttrExpr(numShardsAttr) + blockb.RemoveAttribute(nNumShards) + + if err := convertConfig(blockb, diskSizeGB); err != nil { + return err + } + + // Create a for expression with range + forExpr := fmt.Sprintf("for i in range(%s) :", numShardsExpr) + tokens := hcl.TokensFromExpr(forExpr) + tokens = append(tokens, hcl.TokensObject(blockb)...) + resourceb.SetAttributeRaw(nRepSpecs, hcl.EncloseBracketsNewLines(tokens)) + return nil + } else { + // num_shards is a literal number + blockb.RemoveAttribute(nNumShards) + if err := convertConfig(blockb, diskSizeGB); err != nil { + return err + } + for range numShardsVal { + repSpecs = append(repSpecs, blockb) + } + } + } else { + // No num_shards, default to 1 + if err := convertConfig(blockb, diskSizeGB); err != nil { return err } - blockb.RemoveAttribute(nNumShards) - } - if err := convertConfig(blockb, diskSizeGB); err != nil { - return err - } - for range numShardsVal { repSpecs = append(repSpecs, blockb) } } @@ -88,7 +122,255 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error return nil } +func convertDynamicRepSpecs(resourceb *hclwrite.Body, dSpec dynamicBlock, diskSizeGB hclwrite.Tokens) error { + // Transform references from replication_specs.value.* to spec.* + transformDynamicBlockReferences(dSpec.content.Body(), nRepSpecs, nSpec) + + // Check for dynamic region_configs within this dynamic replication_specs + dConfig, err := getDynamicBlock(dSpec.content.Body(), nConfig) + if err != nil { + return err + } + if !dConfig.IsPresent() { + dConfig, err = getDynamicBlock(dSpec.content.Body(), nConfigSrc) + if err != nil { + return err + } + } + + if dConfig.IsPresent() { + // Handle nested dynamic block for region_configs + return convertDynamicRepSpecsWithDynamicConfig(resourceb, dSpec, dConfig, diskSizeGB) + } + + // Get num_shards from the dynamic block content + numShardsAttr := dSpec.content.Body().GetAttribute(nNumShards) + if numShardsAttr != nil { + numShardsExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec) + dSpec.content.Body().RemoveAttribute(nNumShards) + + // Convert region_configs inside the dynamic block + if err := convertConfig(dSpec.content.Body(), diskSizeGB); err != nil { + return err + } + + // Create the for expression for the flattened replication_specs + forExpr := fmt.Sprintf("for %s in %s : [\n for i in range(%s) : ", + nSpec, hcl.GetAttrExpr(dSpec.forEach), numShardsExpr) + tokens := hcl.TokensFromExpr(forExpr) + tokens = append(tokens, hcl.TokensObject(dSpec.content.Body())...) + tokens = append(tokens, hcl.TokensFromExpr("\n ]\n]")...) + + resourceb.RemoveBlock(dSpec.block) + resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensFuncFlatten(tokens)) + return nil + } else { + // No num_shards, default to 1 + dSpec.content.Body().RemoveAttribute(nNumShards) + + // Convert region_configs inside the dynamic block + if err := convertConfig(dSpec.content.Body(), diskSizeGB); err != nil { + return err + } + + // Create the for expression without num_shards + forExpr := fmt.Sprintf("for %s in %s :", nSpec, hcl.GetAttrExpr(dSpec.forEach)) + tokens := hcl.TokensFromExpr(forExpr) + tokens = append(tokens, hcl.TokensObject(dSpec.content.Body())...) + tokens = hcl.EncloseBracketsNewLines(tokens) + + resourceb.RemoveBlock(dSpec.block) + resourceb.SetAttributeRaw(nRepSpecs, tokens) + return nil + } +} + +func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dConfig dynamicBlock, diskSizeGB hclwrite.Tokens) error { + // Get the block name from the dynamic block + configBlockName := getResourceName(dConfig.block) + + // Get num_shards expression + numShardsAttr := dSpec.content.Body().GetAttribute(nNumShards) + if numShardsAttr != nil { + numShardsExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec) + + // Transform references in place for the dynamic config content + transformDynamicBlockReferencesRecursive(dConfig.content.Body(), configBlockName, nRegion) + // Also transform outer references + for name, attr := range dConfig.content.Body().Attributes() { + expr := hcl.GetAttrExpr(attr) + expr = replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) + dConfig.content.Body().SetAttributeRaw(name, hcl.TokensFromExpr(expr)) + } + for _, block := range dConfig.content.Body().Blocks() { + for name, attr := range block.Body().Attributes() { + expr := hcl.GetAttrExpr(attr) + expr = replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) + block.Body().SetAttributeRaw(name, hcl.TokensFromExpr(expr)) + } + } + + // Don't convert specs blocks to attributes - we'll handle them manually in the string building + + // Build the expression using HCL functions + configForEach := replaceDynamicBlockReferences(hcl.GetAttrExpr(dConfig.forEach), nRepSpecs, nSpec) + zoneNameExpr := "" + if zoneNameAttr := dSpec.content.Body().GetAttribute(nZoneName); zoneNameAttr != nil { + zoneNameExpr = replaceDynamicBlockReferences(hcl.GetAttrExpr(zoneNameAttr), nRepSpecs, nSpec) + } + + // Build the nested expression string + var exprStr strings.Builder + exprStr.WriteString("flatten([\n") + exprStr.WriteString(fmt.Sprintf(" for %s in %s : [\n", nSpec, hcl.GetAttrExpr(dSpec.forEach))) + exprStr.WriteString(fmt.Sprintf(" for i in range(%s) : {\n", numShardsExpr)) + if zoneNameExpr != "" { + exprStr.WriteString(fmt.Sprintf(" zone_name = %s\n", zoneNameExpr)) + } + exprStr.WriteString(" region_configs = [\n") + exprStr.WriteString(fmt.Sprintf(" for %s in %s : {\n", nRegion, configForEach)) + + // Add regular attributes first (provider_name, region_name, priority) + attrs := dConfig.content.Body().Attributes() + if providerName := attrs["provider_name"]; providerName != nil { + exprStr.WriteString(fmt.Sprintf(" provider_name = %s\n", hcl.GetAttrExpr(providerName))) + } + if regionName := attrs["region_name"]; regionName != nil { + exprStr.WriteString(fmt.Sprintf(" region_name = %s\n", hcl.GetAttrExpr(regionName))) + } + if priority := attrs["priority"]; priority != nil { + exprStr.WriteString(fmt.Sprintf(" priority = %s\n", hcl.GetAttrExpr(priority))) + } + + // Add spec blocks as objects with correct formatting and ordering + // Look for the original blocks before they were converted + for _, block := range dConfig.content.Body().Blocks() { + if block.Type() == "electable_specs" { + exprStr.WriteString(" electable_specs = {\n") + blockAttrs := block.Body().Attributes() + // Write in specific order: instance_size first, then node_count + if instanceSize := blockAttrs["instance_size"]; instanceSize != nil { + exprStr.WriteString(fmt.Sprintf(" instance_size = %s\n", hcl.GetAttrExpr(instanceSize))) + } + if nodeCount := blockAttrs["node_count"]; nodeCount != nil { + exprStr.WriteString(fmt.Sprintf(" node_count = %s\n", hcl.GetAttrExpr(nodeCount))) + } + exprStr.WriteString(" }\n") + } + if block.Type() == "read_only_specs" { + exprStr.WriteString(" read_only_specs = {\n") + blockAttrs := block.Body().Attributes() + // Write in specific order: instance_size first, then node_count + if instanceSize := blockAttrs["instance_size"]; instanceSize != nil { + exprStr.WriteString(fmt.Sprintf(" instance_size = %s\n", hcl.GetAttrExpr(instanceSize))) + } + if nodeCount := blockAttrs["node_count"]; nodeCount != nil { + exprStr.WriteString(fmt.Sprintf(" node_count = %s\n", hcl.GetAttrExpr(nodeCount))) + } + exprStr.WriteString(" }\n") + } + } + + exprStr.WriteString(" }\n") + exprStr.WriteString(" ]\n") + exprStr.WriteString(" }\n") + exprStr.WriteString(" ]\n") + exprStr.WriteString(" ])") + + tokens := hcl.TokensFromExpr(exprStr.String()) + + resourceb.RemoveBlock(dSpec.block) + resourceb.SetAttributeRaw(nRepSpecs, tokens) + return nil + } else { + // No num_shards, handle like without nested shards + // Get the block name from the dynamic block + configBlockName := getResourceName(dConfig.block) + + // Get zone_name if present + repSpecFile := hclwrite.NewEmptyFile() + repSpecb := repSpecFile.Body() + + if zoneNameAttr := dSpec.content.Body().GetAttribute(nZoneName); zoneNameAttr != nil { + zoneNameExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(zoneNameAttr), nRepSpecs, nSpec) + repSpecb.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(zoneNameExpr)) + } + + // Create config content with transformed references + configFile := hclwrite.NewEmptyFile() + configb := configFile.Body() + + // Copy and transform attributes + for name, attr := range dConfig.content.Body().Attributes() { + expr := hcl.GetAttrExpr(attr) + // First replace nested dynamic block references + expr = replaceDynamicBlockReferences(expr, configBlockName, nRegion) + // Then replace outer dynamic block references + expr = replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) + configb.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) + } + + // Process blocks and transform their references + for _, block := range dConfig.content.Body().Blocks() { + newBlock := configb.AppendNewBlock(block.Type(), block.Labels()) + newBlockb := newBlock.Body() + for name, attr := range block.Body().Attributes() { + expr := hcl.GetAttrExpr(attr) + // First replace nested dynamic block references + expr = replaceDynamicBlockReferences(expr, configBlockName, nRegion) + // Then replace outer dynamic block references + expr = replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) + newBlockb.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) + } + } + + // Process specs + fillSpecOpt(configb, nElectableSpecs, diskSizeGB) + fillSpecOpt(configb, nReadOnlySpecs, diskSizeGB) + fillSpecOpt(configb, nAnalyticsSpecs, diskSizeGB) + fillSpecOpt(configb, nAutoScaling, nil) + fillSpecOpt(configb, nAnalyticsAutoScaling, nil) + + // Build the nested for expression for region_configs + configForEach := replaceDynamicBlockReferences(hcl.GetAttrExpr(dConfig.forEach), nRepSpecs, nSpec) + var regionTokens2 hclwrite.Tokens + regionTokens2 = append(regionTokens2, hcl.TokensFromExpr("[\n")...) + regionTokens2 = append(regionTokens2, hcl.TokensFromExpr(fmt.Sprintf(" for %s in %s : ", nRegion, configForEach))...) + regionTokens2 = append(regionTokens2, hcl.TokensObject(configb)...) + regionTokens2 = append(regionTokens2, hcl.TokensFromExpr("\n ]")...) + repSpecb.SetAttributeRaw(nConfig, regionTokens2) + + // Build the for expression without range + var tokens hclwrite.Tokens + tokens = append(tokens, hcl.TokensFromExpr("[\n")...) + tokens = append(tokens, hcl.TokensFromExpr(fmt.Sprintf(" for %s in %s : ", nSpec, hcl.GetAttrExpr(dSpec.forEach)))...) + tokens = append(tokens, hcl.TokensObject(repSpecb)...) + tokens = append(tokens, hcl.TokensFromExpr("\n ]")...) + + resourceb.RemoveBlock(dSpec.block) + resourceb.SetAttributeRaw(nRepSpecs, tokens) + return nil + } +} + func convertConfig(repSpecs *hclwrite.Body, diskSizeGB hclwrite.Tokens) error { + // Check for dynamic region_configs block (can be either "region_configs" or "regions_config") + dConfig, err := getDynamicBlock(repSpecs, nConfig) + if err != nil { + return err + } + if !dConfig.IsPresent() { + dConfig, err = getDynamicBlock(repSpecs, nConfigSrc) + if err != nil { + return err + } + } + + if dConfig.IsPresent() { + // Handle dynamic region_configs + return convertDynamicConfig(repSpecs, dConfig, diskSizeGB) + } + var configs []*hclwrite.Body for { block := repSpecs.FirstMatchingBlock(nConfig, nil) @@ -111,6 +393,43 @@ func convertConfig(repSpecs *hclwrite.Body, diskSizeGB hclwrite.Tokens) error { return nil } +func convertDynamicConfig(repSpecs *hclwrite.Body, dConfig dynamicBlock, diskSizeGB hclwrite.Tokens) error { + // Get the block name from the dynamic block itself + blockName := getResourceName(dConfig.block) + + // Transform the references in attributes and blocks + transformDynamicBlockReferencesRecursive(dConfig.content.Body(), blockName, nRegion) + + // Process specs + fillSpecOpt(dConfig.content.Body(), nElectableSpecs, diskSizeGB) + fillSpecOpt(dConfig.content.Body(), nReadOnlySpecs, diskSizeGB) + fillSpecOpt(dConfig.content.Body(), nAnalyticsSpecs, diskSizeGB) + fillSpecOpt(dConfig.content.Body(), nAutoScaling, nil) + fillSpecOpt(dConfig.content.Body(), nAnalyticsAutoScaling, nil) + + // Build the for expression + forExpr := fmt.Sprintf("for %s in %s :", nRegion, hcl.GetAttrExpr(dConfig.forEach)) + tokens := hcl.TokensFromExpr(forExpr) + tokens = append(tokens, hcl.TokensObject(dConfig.content.Body())...) + tokens = hcl.EncloseBracketsNewLines(tokens) + + repSpecs.RemoveBlock(dConfig.block) + repSpecs.SetAttributeRaw(nConfig, tokens) + return nil +} + +func transformDynamicBlockReferencesRecursive(body *hclwrite.Body, blockName, varName string) { + // Transform attributes + for name, attr := range body.Attributes() { + expr := replaceDynamicBlockReferences(hcl.GetAttrExpr(attr), blockName, varName) + body.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) + } + // Transform nested blocks + for _, block := range body.Blocks() { + transformDynamicBlockReferencesRecursive(block.Body(), blockName, varName) + } +} + func fillSpecOpt(resourceb *hclwrite.Body, name string, diskSizeGBTokens hclwrite.Tokens) { block := resourceb.FirstMatchingBlock(name, nil) if block == nil { diff --git a/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.out.tf b/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.out.tf index 61a0c60..d7024f3 100644 --- a/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.out.tf +++ b/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.out.tf @@ -1,7 +1,7 @@ resource "mongodbatlas_advanced_cluster" "dynamic_replication_specs" { - project_id = var.project_id - name = var.cluster_name - cluster_type = "GEOSHARDED" + project_id = var.project_id + name = var.cluster_name + cluster_type = "GEOSHARDED" replication_specs = flatten([ for spec in var.replication_specs : [ for i in range(spec.num_shards) : { @@ -9,15 +9,15 @@ resource "mongodbatlas_advanced_cluster" "dynamic_replication_specs" { region_configs = [ for region in spec.region_configs : { provider_name = region.provider_name - region_name = region.region_name - priority = region.priority + region_name = region.region_name + priority = region.priority electable_specs = { - node_count = region.electable_node_count instance_size = region.instance_size + node_count = region.electable_node_count } read_only_specs = { - node_count = region.read_only_node_count instance_size = region.instance_size + node_count = region.read_only_node_count } } ] @@ -28,7 +28,6 @@ resource "mongodbatlas_advanced_cluster" "dynamic_replication_specs" { # Updated by atlas-cli-plugin-terraform, please review the changes. } - # example of variable for demostration purposes, not used in the conversion variable "replication_specs" { description = "List of replication specifications in mongodbatlas_advanced_cluster format" From 18fe32f5f5d86609dd6d24740a77259066e1f5db Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Thu, 21 Aug 2025 15:47:22 +0200 Subject: [PATCH 05/43] group num_shard tests --- internal/convert/testdata/adv2v2/errors.json | 3 +- .../convert/testdata/adv2v2/num_shards.in.tf | 23 +++++++++++++-- .../convert/testdata/adv2v2/num_shards.out.tf | 28 +++++++++++++++++-- .../adv2v2/num_shards_not_numerical.in.tf | 18 ------------ 4 files changed, 48 insertions(+), 24 deletions(-) delete mode 100644 internal/convert/testdata/adv2v2/num_shards_not_numerical.in.tf diff --git a/internal/convert/testdata/adv2v2/errors.json b/internal/convert/testdata/adv2v2/errors.json index 5debe17..1a758de 100644 --- a/internal/convert/testdata/adv2v2/errors.json +++ b/internal/convert/testdata/adv2v2/errors.json @@ -1,6 +1,5 @@ { "configuration_file_error": "failed to parse Terraform config file", "replication_specs_missing_region_configs": "replication_specs must have at least one region_configs", - "missing_replication_specs": "must have at least one replication_specs", - "num_shards_not_numerical": "setting num_shards: failed to evaluate number" + "missing_replication_specs": "must have at least one replication_specs" } diff --git a/internal/convert/testdata/adv2v2/num_shards.in.tf b/internal/convert/testdata/adv2v2/num_shards.in.tf index 8677e2b..bbb4968 100644 --- a/internal/convert/testdata/adv2v2/num_shards.in.tf +++ b/internal/convert/testdata/adv2v2/num_shards.in.tf @@ -1,4 +1,4 @@ -resource "mongodbatlas_advanced_cluster" "geo" { +resource "mongodbatlas_advanced_cluster" "numerical_num_shards" { project_id = var.project_id name = "geo" cluster_type = "GEOSHARDED" @@ -39,7 +39,7 @@ resource "mongodbatlas_advanced_cluster" "geo" { } } -resource "mongodbatlas_advanced_cluster" "geo" { +resource "mongodbatlas_advanced_cluster" "numerical_num_shards_and_disk_size_gb" { project_id = var.project_id name = "geo" cluster_type = "GEOSHARDED" @@ -58,3 +58,22 @@ resource "mongodbatlas_advanced_cluster" "geo" { } } } + +resource "mongodbatlas_advanced_cluster" "variable_num_shards" { + project_id = var.project_id + name = "geo" + cluster_type = "GEOSHARDED" + replication_specs { + zone_name = "Zone 1" + num_shards = var.num_shards # unresolved num_shards + region_configs { + provider_name = "AWS" + region_name = "US_EAST_1" + priority = 7 + electable_specs { + node_count = 3 + instance_size = "M10" + } + } + } +} diff --git a/internal/convert/testdata/adv2v2/num_shards.out.tf b/internal/convert/testdata/adv2v2/num_shards.out.tf index 75c2375..67b2f90 100644 --- a/internal/convert/testdata/adv2v2/num_shards.out.tf +++ b/internal/convert/testdata/adv2v2/num_shards.out.tf @@ -1,4 +1,4 @@ -resource "mongodbatlas_advanced_cluster" "geo" { +resource "mongodbatlas_advanced_cluster" "numerical_num_shards" { project_id = var.project_id name = "geo" cluster_type = "GEOSHARDED" @@ -105,7 +105,7 @@ resource "mongodbatlas_advanced_cluster" "geo" { # Updated by atlas-cli-plugin-terraform, please review the changes. } -resource "mongodbatlas_advanced_cluster" "geo" { +resource "mongodbatlas_advanced_cluster" "numerical_num_shards_and_disk_size_gb" { project_id = var.project_id name = "geo" cluster_type = "GEOSHARDED" @@ -144,3 +144,27 @@ resource "mongodbatlas_advanced_cluster" "geo" { # Updated by atlas-cli-plugin-terraform, please review the changes. } + +resource "mongodbatlas_advanced_cluster" "variable_num_shards" { + project_id = var.project_id + name = "geo" + cluster_type = "GEOSHARDED" + replication_specs = [ + for i in range(var.num_shards) : { + zone_name = "Zone 1" + region_configs = [ + { + provider_name = "AWS" + region_name = "US_EAST_1" + priority = 7 + electable_specs = { + node_count = 3 + instance_size = "M10" + } + } + ] + } + ] + + # Updated by atlas-cli-plugin-terraform, please review the changes. +} diff --git a/internal/convert/testdata/adv2v2/num_shards_not_numerical.in.tf b/internal/convert/testdata/adv2v2/num_shards_not_numerical.in.tf deleted file mode 100644 index 396355f..0000000 --- a/internal/convert/testdata/adv2v2/num_shards_not_numerical.in.tf +++ /dev/null @@ -1,18 +0,0 @@ -resource "mongodbatlas_advanced_cluster" "geo" { - project_id = var.project_id - name = "geo" - cluster_type = "GEOSHARDED" - replication_specs { - zone_name = "Zone 1" - num_shards = var.num_shards # unresolved num_shards - region_configs { - provider_name = "AWS" - region_name = "US_EAST_1" - priority = 7 - electable_specs { - node_count = 3 - instance_size = "M10" - } - } - } -} From 4192605b5bba63cfd8bfa3440f2f5d4c8dcbb3f0 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Fri, 22 Aug 2025 09:44:25 +0200 Subject: [PATCH 06/43] support mix of variable and numerical num_shards --- Makefile | 11 + internal/convert/adv2v2.go | 331 +++++++++++------- .../convert/testdata/adv2v2/num_shards.in.tf | 82 +++++ .../convert/testdata/adv2v2/num_shards.out.tf | 116 ++++++ internal/hcl/hcl.go | 7 + 5 files changed, 412 insertions(+), 135 deletions(-) diff --git a/Makefile b/Makefile index 68ab53f..3f502ab 100644 --- a/Makefile +++ b/Makefile @@ -27,6 +27,17 @@ clean: ## Clean binary folders test: ## Run unit tests go test ./internal/... -timeout=30s -parallel=4 -race +.PHONY: lint-fix +lint-fix: ## Fix Go linter issues + @echo "==> Fixing linters errors..." + $(shell go env GOPATH)/bin/fieldalignment -json -fix ./... + golangci-lint run --fix + +.PHONY: lint +lint: ## Check Go linter issues + @echo "==> Checking source code against linters..." + golangci-lint run + .PHONY: test-update test-update: ## Run unit tests and update the golden files go test ./internal/... -timeout=30s -parallel=4 -race -update diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index 05b3bfa..b7e8416 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -67,38 +67,101 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error if dSpec.IsPresent() { return convertDynamicRepSpecs(resourceb, dSpec, diskSizeGB) } - - var repSpecs []*hclwrite.Body + + // Collect all replication_specs blocks first + var repSpecBlocks []*hclwrite.Block for { block := resourceb.FirstMatchingBlock(nRepSpecs, nil) if block == nil { break } resourceb.RemoveBlock(block) - blockb := block.Body() - - // Check if num_shards is a variable/expression - numShardsAttr := blockb.GetAttribute(nNumShards) + repSpecBlocks = append(repSpecBlocks, block) + } + + if len(repSpecBlocks) == 0 { + return fmt.Errorf("must have at least one replication_specs") + } + + // Check if any replication_specs has a variable num_shards + hasVariableNumShards := false + for _, block := range repSpecBlocks { + numShardsAttr := block.Body().GetAttribute(nNumShards) if numShardsAttr != nil { - numShardsVal, err := hcl.GetAttrInt(numShardsAttr, errNumShards) + _, err := hcl.GetAttrInt(numShardsAttr, errNumShards) if err != nil { - // num_shards is not a literal number, it's a variable/expression - // Need to use range() in the output - numShardsExpr := hcl.GetAttrExpr(numShardsAttr) - blockb.RemoveAttribute(nNumShards) - + hasVariableNumShards = true + break + } + } + } + + // If we have any variable num_shards, we need to use concat + if hasVariableNumShards { + var concatParts []hclwrite.Tokens + hasMultipleParts := false + + for _, block := range repSpecBlocks { + blockb := block.Body() + numShardsAttr := blockb.GetAttribute(nNumShards) + + if numShardsAttr != nil { + numShardsVal, err := hcl.GetAttrInt(numShardsAttr, errNumShards) + + if err != nil { + // num_shards is a variable/expression + numShardsExpr := hcl.GetAttrExpr(numShardsAttr) + blockb.RemoveAttribute(nNumShards) + + if err := convertConfig(blockb, diskSizeGB); err != nil { + return err + } + + forExpr := fmt.Sprintf("for i in range(%s) :", numShardsExpr) + tokens := hcl.TokensFromExpr(forExpr) + tokens = append(tokens, hcl.TokensObject(blockb)...) + concatParts = append(concatParts, hcl.EncloseBracketsNewLines(tokens)) + } else { + // num_shards is a literal number - create explicit array + blockb.RemoveAttribute(nNumShards) + + if err := convertConfig(blockb, diskSizeGB); err != nil { + return err + } + + var repSpecs []*hclwrite.Body + for range numShardsVal { + repSpecs = append(repSpecs, blockb) + } + concatParts = append(concatParts, hcl.TokensArray(repSpecs)) + hasMultipleParts = true + } + } else { + // No num_shards, default to 1 if err := convertConfig(blockb, diskSizeGB); err != nil { return err } - - // Create a for expression with range - forExpr := fmt.Sprintf("for i in range(%s) :", numShardsExpr) - tokens := hcl.TokensFromExpr(forExpr) - tokens = append(tokens, hcl.TokensObject(blockb)...) - resourceb.SetAttributeRaw(nRepSpecs, hcl.EncloseBracketsNewLines(tokens)) - return nil - } else { - // num_shards is a literal number + concatParts = append(concatParts, hcl.TokensArraySingle(blockb)) + hasMultipleParts = true + } + } + + // Use concat only if we have multiple parts or mixing numerical and variable + if len(concatParts) > 1 || hasMultipleParts { + resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensFuncConcat(concatParts...)) + } else { + // Only one variable num_shards, no need for concat + resourceb.SetAttributeRaw(nRepSpecs, concatParts[0]) + } + } else { + // All num_shards are numeric or missing, use simple array + var repSpecs []*hclwrite.Body + for _, block := range repSpecBlocks { + blockb := block.Body() + numShardsAttr := blockb.GetAttribute(nNumShards) + + if numShardsAttr != nil { + numShardsVal, _ := hcl.GetAttrInt(numShardsAttr, errNumShards) blockb.RemoveAttribute(nNumShards) if err := convertConfig(blockb, diskSizeGB); err != nil { return err @@ -106,26 +169,24 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error for range numShardsVal { repSpecs = append(repSpecs, blockb) } + } else { + // No num_shards, default to 1 + if err := convertConfig(blockb, diskSizeGB); err != nil { + return err + } + repSpecs = append(repSpecs, blockb) } - } else { - // No num_shards, default to 1 - if err := convertConfig(blockb, diskSizeGB); err != nil { - return err - } - repSpecs = append(repSpecs, blockb) } + resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensArray(repSpecs)) } - if len(repSpecs) == 0 { - return fmt.Errorf("must have at least one replication_specs") - } - resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensArray(repSpecs)) + return nil } func convertDynamicRepSpecs(resourceb *hclwrite.Body, dSpec dynamicBlock, diskSizeGB hclwrite.Tokens) error { // Transform references from replication_specs.value.* to spec.* transformDynamicBlockReferences(dSpec.content.Body(), nRepSpecs, nSpec) - + // Check for dynamic region_configs within this dynamic replication_specs dConfig, err := getDynamicBlock(dSpec.content.Body(), nConfig) if err != nil { @@ -137,63 +198,63 @@ func convertDynamicRepSpecs(resourceb *hclwrite.Body, dSpec dynamicBlock, diskSi return err } } - + if dConfig.IsPresent() { // Handle nested dynamic block for region_configs return convertDynamicRepSpecsWithDynamicConfig(resourceb, dSpec, dConfig, diskSizeGB) } - + // Get num_shards from the dynamic block content numShardsAttr := dSpec.content.Body().GetAttribute(nNumShards) if numShardsAttr != nil { numShardsExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec) dSpec.content.Body().RemoveAttribute(nNumShards) - + // Convert region_configs inside the dynamic block if err := convertConfig(dSpec.content.Body(), diskSizeGB); err != nil { return err } - + // Create the for expression for the flattened replication_specs - forExpr := fmt.Sprintf("for %s in %s : [\n for i in range(%s) : ", + forExpr := fmt.Sprintf("for %s in %s : [\n for i in range(%s) : ", nSpec, hcl.GetAttrExpr(dSpec.forEach), numShardsExpr) tokens := hcl.TokensFromExpr(forExpr) tokens = append(tokens, hcl.TokensObject(dSpec.content.Body())...) tokens = append(tokens, hcl.TokensFromExpr("\n ]\n]")...) - + resourceb.RemoveBlock(dSpec.block) resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensFuncFlatten(tokens)) return nil - } else { - // No num_shards, default to 1 - dSpec.content.Body().RemoveAttribute(nNumShards) - - // Convert region_configs inside the dynamic block - if err := convertConfig(dSpec.content.Body(), diskSizeGB); err != nil { - return err - } - - // Create the for expression without num_shards - forExpr := fmt.Sprintf("for %s in %s :", nSpec, hcl.GetAttrExpr(dSpec.forEach)) - tokens := hcl.TokensFromExpr(forExpr) - tokens = append(tokens, hcl.TokensObject(dSpec.content.Body())...) - tokens = hcl.EncloseBracketsNewLines(tokens) - - resourceb.RemoveBlock(dSpec.block) - resourceb.SetAttributeRaw(nRepSpecs, tokens) - return nil } + // No num_shards, default to 1 + dSpec.content.Body().RemoveAttribute(nNumShards) + + // Convert region_configs inside the dynamic block + if err := convertConfig(dSpec.content.Body(), diskSizeGB); err != nil { + return err + } + + // Create the for expression without num_shards + forExpr := fmt.Sprintf("for %s in %s :", nSpec, hcl.GetAttrExpr(dSpec.forEach)) + tokens := hcl.TokensFromExpr(forExpr) + tokens = append(tokens, hcl.TokensObject(dSpec.content.Body())...) + tokens = hcl.EncloseBracketsNewLines(tokens) + + resourceb.RemoveBlock(dSpec.block) + resourceb.SetAttributeRaw(nRepSpecs, tokens) + return nil } -func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dConfig dynamicBlock, diskSizeGB hclwrite.Tokens) error { +func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dConfig dynamicBlock, + diskSizeGB hclwrite.Tokens) error { // Get the block name from the dynamic block configBlockName := getResourceName(dConfig.block) - + // Get num_shards expression numShardsAttr := dSpec.content.Body().GetAttribute(nNumShards) if numShardsAttr != nil { numShardsExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec) - + // Transform references in place for the dynamic config content transformDynamicBlockReferencesRecursive(dConfig.content.Body(), configBlockName, nRegion) // Also transform outer references @@ -209,16 +270,16 @@ func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dC block.Body().SetAttributeRaw(name, hcl.TokensFromExpr(expr)) } } - + // Don't convert specs blocks to attributes - we'll handle them manually in the string building - + // Build the expression using HCL functions configForEach := replaceDynamicBlockReferences(hcl.GetAttrExpr(dConfig.forEach), nRepSpecs, nSpec) zoneNameExpr := "" if zoneNameAttr := dSpec.content.Body().GetAttribute(nZoneName); zoneNameAttr != nil { zoneNameExpr = replaceDynamicBlockReferences(hcl.GetAttrExpr(zoneNameAttr), nRepSpecs, nSpec) } - + // Build the nested expression string var exprStr strings.Builder exprStr.WriteString("flatten([\n") @@ -229,7 +290,7 @@ func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dC } exprStr.WriteString(" region_configs = [\n") exprStr.WriteString(fmt.Sprintf(" for %s in %s : {\n", nRegion, configForEach)) - + // Add regular attributes first (provider_name, region_name, priority) attrs := dConfig.content.Body().Attributes() if providerName := attrs["provider_name"]; providerName != nil { @@ -241,7 +302,7 @@ func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dC if priority := attrs["priority"]; priority != nil { exprStr.WriteString(fmt.Sprintf(" priority = %s\n", hcl.GetAttrExpr(priority))) } - + // Add spec blocks as objects with correct formatting and ordering // Look for the original blocks before they were converted for _, block := range dConfig.content.Body().Blocks() { @@ -270,87 +331,87 @@ func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dC exprStr.WriteString(" }\n") } } - + exprStr.WriteString(" }\n") exprStr.WriteString(" ]\n") exprStr.WriteString(" }\n") exprStr.WriteString(" ]\n") exprStr.WriteString(" ])") - + tokens := hcl.TokensFromExpr(exprStr.String()) - + resourceb.RemoveBlock(dSpec.block) resourceb.SetAttributeRaw(nRepSpecs, tokens) return nil - } else { - // No num_shards, handle like without nested shards - // Get the block name from the dynamic block - configBlockName := getResourceName(dConfig.block) - - // Get zone_name if present - repSpecFile := hclwrite.NewEmptyFile() - repSpecb := repSpecFile.Body() - - if zoneNameAttr := dSpec.content.Body().GetAttribute(nZoneName); zoneNameAttr != nil { - zoneNameExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(zoneNameAttr), nRepSpecs, nSpec) - repSpecb.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(zoneNameExpr)) - } - - // Create config content with transformed references - configFile := hclwrite.NewEmptyFile() - configb := configFile.Body() - - // Copy and transform attributes - for name, attr := range dConfig.content.Body().Attributes() { + } + // No num_shards, handle like without nested shards + // Reuse the block name from the dynamic block that was already obtained + + // Get zone_name if present + repSpecFile := hclwrite.NewEmptyFile() + repSpecb := repSpecFile.Body() + + if zoneNameAttr := dSpec.content.Body().GetAttribute(nZoneName); zoneNameAttr != nil { + zoneNameExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(zoneNameAttr), nRepSpecs, nSpec) + repSpecb.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(zoneNameExpr)) + } + + // Create config content with transformed references + configFile := hclwrite.NewEmptyFile() + configb := configFile.Body() + + // Copy and transform attributes + for name, attr := range dConfig.content.Body().Attributes() { + expr := hcl.GetAttrExpr(attr) + // First replace nested dynamic block references + expr = replaceDynamicBlockReferences(expr, configBlockName, nRegion) + // Then replace outer dynamic block references + expr = replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) + configb.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) + } + + // Process blocks and transform their references + for _, block := range dConfig.content.Body().Blocks() { + newBlock := configb.AppendNewBlock(block.Type(), block.Labels()) + newBlockb := newBlock.Body() + for name, attr := range block.Body().Attributes() { expr := hcl.GetAttrExpr(attr) // First replace nested dynamic block references expr = replaceDynamicBlockReferences(expr, configBlockName, nRegion) // Then replace outer dynamic block references expr = replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) - configb.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) - } - - // Process blocks and transform their references - for _, block := range dConfig.content.Body().Blocks() { - newBlock := configb.AppendNewBlock(block.Type(), block.Labels()) - newBlockb := newBlock.Body() - for name, attr := range block.Body().Attributes() { - expr := hcl.GetAttrExpr(attr) - // First replace nested dynamic block references - expr = replaceDynamicBlockReferences(expr, configBlockName, nRegion) - // Then replace outer dynamic block references - expr = replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) - newBlockb.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) - } + newBlockb.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) } - - // Process specs - fillSpecOpt(configb, nElectableSpecs, diskSizeGB) - fillSpecOpt(configb, nReadOnlySpecs, diskSizeGB) - fillSpecOpt(configb, nAnalyticsSpecs, diskSizeGB) - fillSpecOpt(configb, nAutoScaling, nil) - fillSpecOpt(configb, nAnalyticsAutoScaling, nil) - - // Build the nested for expression for region_configs - configForEach := replaceDynamicBlockReferences(hcl.GetAttrExpr(dConfig.forEach), nRepSpecs, nSpec) - var regionTokens2 hclwrite.Tokens - regionTokens2 = append(regionTokens2, hcl.TokensFromExpr("[\n")...) - regionTokens2 = append(regionTokens2, hcl.TokensFromExpr(fmt.Sprintf(" for %s in %s : ", nRegion, configForEach))...) - regionTokens2 = append(regionTokens2, hcl.TokensObject(configb)...) - regionTokens2 = append(regionTokens2, hcl.TokensFromExpr("\n ]")...) - repSpecb.SetAttributeRaw(nConfig, regionTokens2) - - // Build the for expression without range - var tokens hclwrite.Tokens - tokens = append(tokens, hcl.TokensFromExpr("[\n")...) - tokens = append(tokens, hcl.TokensFromExpr(fmt.Sprintf(" for %s in %s : ", nSpec, hcl.GetAttrExpr(dSpec.forEach)))...) - tokens = append(tokens, hcl.TokensObject(repSpecb)...) - tokens = append(tokens, hcl.TokensFromExpr("\n ]")...) - - resourceb.RemoveBlock(dSpec.block) - resourceb.SetAttributeRaw(nRepSpecs, tokens) - return nil } + + // Process specs + fillSpecOpt(configb, nElectableSpecs, diskSizeGB) + fillSpecOpt(configb, nReadOnlySpecs, diskSizeGB) + fillSpecOpt(configb, nAnalyticsSpecs, diskSizeGB) + fillSpecOpt(configb, nAutoScaling, nil) + fillSpecOpt(configb, nAnalyticsAutoScaling, nil) + + // Build the nested for expression for region_configs + configForEach := replaceDynamicBlockReferences(hcl.GetAttrExpr(dConfig.forEach), nRepSpecs, nSpec) + var regionTokens2 hclwrite.Tokens + regionTokens2 = append(regionTokens2, hcl.TokensFromExpr("[\n")...) + forExpr := fmt.Sprintf(" for %s in %s : ", nRegion, configForEach) + regionTokens2 = append(regionTokens2, hcl.TokensFromExpr(forExpr)...) + regionTokens2 = append(regionTokens2, hcl.TokensObject(configb)...) + regionTokens2 = append(regionTokens2, hcl.TokensFromExpr("\n ]")...) + repSpecb.SetAttributeRaw(nConfig, regionTokens2) + + // Build the for expression without range + var tokens hclwrite.Tokens + tokens = append(tokens, hcl.TokensFromExpr("[\n")...) + specForExpr := fmt.Sprintf(" for %s in %s : ", nSpec, hcl.GetAttrExpr(dSpec.forEach)) + tokens = append(tokens, hcl.TokensFromExpr(specForExpr)...) + tokens = append(tokens, hcl.TokensObject(repSpecb)...) + tokens = append(tokens, hcl.TokensFromExpr("\n ]")...) + + resourceb.RemoveBlock(dSpec.block) + resourceb.SetAttributeRaw(nRepSpecs, tokens) + return nil } func convertConfig(repSpecs *hclwrite.Body, diskSizeGB hclwrite.Tokens) error { @@ -365,12 +426,12 @@ func convertConfig(repSpecs *hclwrite.Body, diskSizeGB hclwrite.Tokens) error { return err } } - + if dConfig.IsPresent() { // Handle dynamic region_configs return convertDynamicConfig(repSpecs, dConfig, diskSizeGB) } - + var configs []*hclwrite.Body for { block := repSpecs.FirstMatchingBlock(nConfig, nil) @@ -396,23 +457,23 @@ func convertConfig(repSpecs *hclwrite.Body, diskSizeGB hclwrite.Tokens) error { func convertDynamicConfig(repSpecs *hclwrite.Body, dConfig dynamicBlock, diskSizeGB hclwrite.Tokens) error { // Get the block name from the dynamic block itself blockName := getResourceName(dConfig.block) - + // Transform the references in attributes and blocks transformDynamicBlockReferencesRecursive(dConfig.content.Body(), blockName, nRegion) - + // Process specs fillSpecOpt(dConfig.content.Body(), nElectableSpecs, diskSizeGB) fillSpecOpt(dConfig.content.Body(), nReadOnlySpecs, diskSizeGB) fillSpecOpt(dConfig.content.Body(), nAnalyticsSpecs, diskSizeGB) fillSpecOpt(dConfig.content.Body(), nAutoScaling, nil) fillSpecOpt(dConfig.content.Body(), nAnalyticsAutoScaling, nil) - + // Build the for expression forExpr := fmt.Sprintf("for %s in %s :", nRegion, hcl.GetAttrExpr(dConfig.forEach)) tokens := hcl.TokensFromExpr(forExpr) tokens = append(tokens, hcl.TokensObject(dConfig.content.Body())...) tokens = hcl.EncloseBracketsNewLines(tokens) - + repSpecs.RemoveBlock(dConfig.block) repSpecs.SetAttributeRaw(nConfig, tokens) return nil diff --git a/internal/convert/testdata/adv2v2/num_shards.in.tf b/internal/convert/testdata/adv2v2/num_shards.in.tf index bbb4968..9019256 100644 --- a/internal/convert/testdata/adv2v2/num_shards.in.tf +++ b/internal/convert/testdata/adv2v2/num_shards.in.tf @@ -77,3 +77,85 @@ resource "mongodbatlas_advanced_cluster" "variable_num_shards" { } } } + +resource "mongodbatlas_advanced_cluster" "multiple_variable_num_shards" { + project_id = var.project_id + name = "geo" + cluster_type = "GEOSHARDED" + replication_specs { + zone_name = "Zone 1" + num_shards = var.num_shards_rep1 + region_configs { + provider_name = "AWS" + region_name = "US_EAST_1" + priority = 7 + electable_specs { + node_count = 3 + instance_size = "M10" + } + } + } + replication_specs { + zone_name = "Zone 2" + num_shards = var.num_shards_rep2 + region_configs { + provider_name = "AWS" + region_name = "US_WEST_2" + priority = 7 + electable_specs { + node_count = 2 + instance_size = "M10" + } + } + region_configs { + provider_name = "AWS" + region_name = "EU_CENTRAL_1" + priority = 6 + electable_specs { + node_count = 1 + instance_size = "M10" + } + } + } +} + +resource "mongodbatlas_advanced_cluster" "mix_variable_numerical_num_shards" { + project_id = var.project_id + name = "geo" + cluster_type = "GEOSHARDED" + replication_specs { + zone_name = "Zone 1" + num_shards = 2 + region_configs { + provider_name = "AWS" + region_name = "US_EAST_1" + priority = 7 + electable_specs { + node_count = 3 + instance_size = "M10" + } + } + } + replication_specs { + zone_name = "Zone 2" + num_shards = var.num_shards_rep2 + region_configs { + provider_name = "AWS" + region_name = "US_WEST_2" + priority = 7 + electable_specs { + node_count = 2 + instance_size = "M10" + } + } + region_configs { + provider_name = "AWS" + region_name = "EU_CENTRAL_1" + priority = 6 + electable_specs { + node_count = 1 + instance_size = "M10" + } + } + } +} diff --git a/internal/convert/testdata/adv2v2/num_shards.out.tf b/internal/convert/testdata/adv2v2/num_shards.out.tf index 67b2f90..c2cdb98 100644 --- a/internal/convert/testdata/adv2v2/num_shards.out.tf +++ b/internal/convert/testdata/adv2v2/num_shards.out.tf @@ -168,3 +168,119 @@ resource "mongodbatlas_advanced_cluster" "variable_num_shards" { # Updated by atlas-cli-plugin-terraform, please review the changes. } + +resource "mongodbatlas_advanced_cluster" "multiple_variable_num_shards" { + project_id = var.project_id + name = "geo" + cluster_type = "GEOSHARDED" + replication_specs = concat( + [ + for i in range(var.num_shards_rep1) : { + zone_name = "Zone 1" + region_configs = [ + { + provider_name = "AWS" + region_name = "US_EAST_1" + priority = 7 + electable_specs = { + node_count = 3 + instance_size = "M10" + } + } + ] + } + ], + [ + for i in range(var.num_shards_rep2) : { + zone_name = "Zone 2" + region_configs = [ + { + provider_name = "AWS" + region_name = "US_WEST_2" + priority = 7 + electable_specs = { + node_count = 2 + instance_size = "M10" + } + }, + { + provider_name = "AWS" + region_name = "EU_CENTRAL_1" + priority = 6 + electable_specs = { + node_count = 1 + instance_size = "M10" + } + } + ] + } + ] + ) + + # Updated by atlas-cli-plugin-terraform, please review the changes. +} + +resource "mongodbatlas_advanced_cluster" "mix_variable_numerical_num_shards" { + project_id = var.project_id + name = "geo" + cluster_type = "GEOSHARDED" + replication_specs = concat( + [ + { + zone_name = "Zone 1" + region_configs = [ + { + provider_name = "AWS" + region_name = "US_EAST_1" + priority = 7 + electable_specs = { + node_count = 3 + instance_size = "M10" + } + } + ] + }, + { + zone_name = "Zone 1" + region_configs = [ + { + provider_name = "AWS" + region_name = "US_EAST_1" + priority = 7 + electable_specs = { + node_count = 3 + instance_size = "M10" + } + } + ] + } + ], + [ + for i in range(var.num_shards_rep2) : { + zone_name = "Zone 2" + region_configs = [ + { + provider_name = "AWS" + region_name = "US_WEST_2" + priority = 7 + electable_specs = { + node_count = 2 + instance_size = "M10" + } + }, + { + provider_name = "AWS" + region_name = "EU_CENTRAL_1" + priority = 6 + electable_specs = { + node_count = 1 + instance_size = "M10" + } + } + ] + } + ] + ) + + # Updated by atlas-cli-plugin-terraform, please review the changes. +} diff --git a/internal/hcl/hcl.go b/internal/hcl/hcl.go index 8b71cfb..dbb818f 100644 --- a/internal/hcl/hcl.go +++ b/internal/hcl/hcl.go @@ -118,6 +118,13 @@ func TokensFuncMerge(tokens ...hclwrite.Tokens) hclwrite.Tokens { return append(ret, EncloseParens(params)...) } +// TokensFuncConcat creates the tokens for the HCL concat function. +func TokensFuncConcat(tokens ...hclwrite.Tokens) hclwrite.Tokens { + params := EncloseNewLines(joinTokens(tokens...)) + ret := TokensFromExpr("concat") + return append(ret, EncloseParens(params)...) +} + // TokensFuncFlatten creates the tokens for the HCL flatten function. func TokensFuncFlatten(tokens hclwrite.Tokens) hclwrite.Tokens { ret := TokensFromExpr("flatten") From abb154c19168dd6650ca8e3a6cc54b6f0a28bc97 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Fri, 22 Aug 2025 09:45:59 +0200 Subject: [PATCH 07/43] remove num_shard comment --- internal/convert/testdata/adv2v2/num_shards.in.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/convert/testdata/adv2v2/num_shards.in.tf b/internal/convert/testdata/adv2v2/num_shards.in.tf index 9019256..faa6efc 100644 --- a/internal/convert/testdata/adv2v2/num_shards.in.tf +++ b/internal/convert/testdata/adv2v2/num_shards.in.tf @@ -65,7 +65,7 @@ resource "mongodbatlas_advanced_cluster" "variable_num_shards" { cluster_type = "GEOSHARDED" replication_specs { zone_name = "Zone 1" - num_shards = var.num_shards # unresolved num_shards + num_shards = var.num_shards region_configs { provider_name = "AWS" region_name = "US_EAST_1" From 751a804523f2507062efd852b85e77e4ae7dfad2 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Fri, 22 Aug 2025 10:00:11 +0200 Subject: [PATCH 08/43] simplify convert code so we don't hardcode destination attributes --- internal/convert/adv2v2.go | 56 ++++++++----------- .../dynamic_replication_specs_basic.out.tf | 2 +- 2 files changed, 25 insertions(+), 33 deletions(-) diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index b7e8416..eb24368 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -291,45 +291,37 @@ func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dC exprStr.WriteString(" region_configs = [\n") exprStr.WriteString(fmt.Sprintf(" for %s in %s : {\n", nRegion, configForEach)) - // Add regular attributes first (provider_name, region_name, priority) + // Add all attributes generically in alphabetical order attrs := dConfig.content.Body().Attributes() - if providerName := attrs["provider_name"]; providerName != nil { - exprStr.WriteString(fmt.Sprintf(" provider_name = %s\n", hcl.GetAttrExpr(providerName))) + attrNames := make([]string, 0, len(attrs)) + for name := range attrs { + attrNames = append(attrNames, name) } - if regionName := attrs["region_name"]; regionName != nil { - exprStr.WriteString(fmt.Sprintf(" region_name = %s\n", hcl.GetAttrExpr(regionName))) - } - if priority := attrs["priority"]; priority != nil { - exprStr.WriteString(fmt.Sprintf(" priority = %s\n", hcl.GetAttrExpr(priority))) + slices.Sort(attrNames) + + for _, name := range attrNames { + attr := attrs[name] + exprStr.WriteString(fmt.Sprintf(" %s = %s\n", name, hcl.GetAttrExpr(attr))) } - // Add spec blocks as objects with correct formatting and ordering - // Look for the original blocks before they were converted + // Add all blocks generically as objects for _, block := range dConfig.content.Body().Blocks() { - if block.Type() == "electable_specs" { - exprStr.WriteString(" electable_specs = {\n") - blockAttrs := block.Body().Attributes() - // Write in specific order: instance_size first, then node_count - if instanceSize := blockAttrs["instance_size"]; instanceSize != nil { - exprStr.WriteString(fmt.Sprintf(" instance_size = %s\n", hcl.GetAttrExpr(instanceSize))) - } - if nodeCount := blockAttrs["node_count"]; nodeCount != nil { - exprStr.WriteString(fmt.Sprintf(" node_count = %s\n", hcl.GetAttrExpr(nodeCount))) - } - exprStr.WriteString(" }\n") + blockType := block.Type() + exprStr.WriteString(fmt.Sprintf(" %s = {\n", blockType)) + + // Add block attributes in alphabetical order + blockAttrs := block.Body().Attributes() + blockAttrNames := make([]string, 0, len(blockAttrs)) + for name := range blockAttrs { + blockAttrNames = append(blockAttrNames, name) } - if block.Type() == "read_only_specs" { - exprStr.WriteString(" read_only_specs = {\n") - blockAttrs := block.Body().Attributes() - // Write in specific order: instance_size first, then node_count - if instanceSize := blockAttrs["instance_size"]; instanceSize != nil { - exprStr.WriteString(fmt.Sprintf(" instance_size = %s\n", hcl.GetAttrExpr(instanceSize))) - } - if nodeCount := blockAttrs["node_count"]; nodeCount != nil { - exprStr.WriteString(fmt.Sprintf(" node_count = %s\n", hcl.GetAttrExpr(nodeCount))) - } - exprStr.WriteString(" }\n") + slices.Sort(blockAttrNames) + + for _, attrName := range blockAttrNames { + attr := blockAttrs[attrName] + exprStr.WriteString(fmt.Sprintf(" %s = %s\n", attrName, hcl.GetAttrExpr(attr))) } + exprStr.WriteString(" }\n") } exprStr.WriteString(" }\n") diff --git a/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.out.tf b/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.out.tf index d7024f3..ebff63a 100644 --- a/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.out.tf +++ b/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.out.tf @@ -8,9 +8,9 @@ resource "mongodbatlas_advanced_cluster" "dynamic_replication_specs" { zone_name = spec.zone_name region_configs = [ for region in spec.region_configs : { + priority = region.priority provider_name = region.provider_name region_name = region.region_name - priority = region.priority electable_specs = { instance_size = region.instance_size node_count = region.electable_node_count From 460938663da8f379ff791485c508289e3ef65e3a Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Fri, 22 Aug 2025 10:12:47 +0200 Subject: [PATCH 09/43] use hcl.TokensFuncFlatten --- internal/convert/adv2v2.go | 63 +++++++++++-------- .../dynamic_replication_specs_basic.out.tf | 14 ++--- 2 files changed, 45 insertions(+), 32 deletions(-) diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index eb24368..b5aca36 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -3,7 +3,6 @@ package convert import ( "fmt" "slices" - "strings" "github.com/hashicorp/hcl/v2/hclwrite" "github.com/mongodb-labs/atlas-cli-plugin-terraform/internal/hcl" @@ -275,21 +274,10 @@ func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dC // Build the expression using HCL functions configForEach := replaceDynamicBlockReferences(hcl.GetAttrExpr(dConfig.forEach), nRepSpecs, nSpec) - zoneNameExpr := "" - if zoneNameAttr := dSpec.content.Body().GetAttribute(nZoneName); zoneNameAttr != nil { - zoneNameExpr = replaceDynamicBlockReferences(hcl.GetAttrExpr(zoneNameAttr), nRepSpecs, nSpec) - } - // Build the nested expression string - var exprStr strings.Builder - exprStr.WriteString("flatten([\n") - exprStr.WriteString(fmt.Sprintf(" for %s in %s : [\n", nSpec, hcl.GetAttrExpr(dSpec.forEach))) - exprStr.WriteString(fmt.Sprintf(" for i in range(%s) : {\n", numShardsExpr)) - if zoneNameExpr != "" { - exprStr.WriteString(fmt.Sprintf(" zone_name = %s\n", zoneNameExpr)) - } - exprStr.WriteString(" region_configs = [\n") - exprStr.WriteString(fmt.Sprintf(" for %s in %s : {\n", nRegion, configForEach)) + // Create the inner region_configs body + regionConfigFile := hclwrite.NewEmptyFile() + regionConfigBody := regionConfigFile.Body() // Add all attributes generically in alphabetical order attrs := dConfig.content.Body().Attributes() @@ -301,13 +289,16 @@ func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dC for _, name := range attrNames { attr := attrs[name] - exprStr.WriteString(fmt.Sprintf(" %s = %s\n", name, hcl.GetAttrExpr(attr))) + regionConfigBody.SetAttributeRaw(name, hcl.TokensFromExpr(hcl.GetAttrExpr(attr))) } // Add all blocks generically as objects for _, block := range dConfig.content.Body().Blocks() { blockType := block.Type() - exprStr.WriteString(fmt.Sprintf(" %s = {\n", blockType)) + + // Create a new body for the block + blockFile := hclwrite.NewEmptyFile() + blockBody := blockFile.Body() // Add block attributes in alphabetical order blockAttrs := block.Body().Attributes() @@ -319,18 +310,40 @@ func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dC for _, attrName := range blockAttrNames { attr := blockAttrs[attrName] - exprStr.WriteString(fmt.Sprintf(" %s = %s\n", attrName, hcl.GetAttrExpr(attr))) + blockBody.SetAttributeRaw(attrName, hcl.TokensFromExpr(hcl.GetAttrExpr(attr))) } - exprStr.WriteString(" }\n") + + regionConfigBody.SetAttributeRaw(blockType, hcl.TokensObject(blockBody)) + } + + // Build the region_configs for expression + regionForExpr := fmt.Sprintf("for %s in %s :", nRegion, configForEach) + regionTokens := hcl.TokensFromExpr(regionForExpr) + regionTokens = append(regionTokens, hcl.TokensObject(regionConfigBody)...) + + // Create the replication spec body + repSpecFile := hclwrite.NewEmptyFile() + repSpecBody := repSpecFile.Body() + + if zoneNameAttr := dSpec.content.Body().GetAttribute(nZoneName); zoneNameAttr != nil { + zoneNameExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(zoneNameAttr), nRepSpecs, nSpec) + repSpecBody.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(zoneNameExpr)) } - exprStr.WriteString(" }\n") - exprStr.WriteString(" ]\n") - exprStr.WriteString(" }\n") - exprStr.WriteString(" ]\n") - exprStr.WriteString(" ])") + repSpecBody.SetAttributeRaw(nConfig, hcl.EncloseBracketsNewLines(regionTokens)) + + // Build the inner for expression with range + innerForExpr := fmt.Sprintf("for i in range(%s) :", numShardsExpr) + innerTokens := hcl.TokensFromExpr(innerForExpr) + innerTokens = append(innerTokens, hcl.TokensObject(repSpecBody)...) + + // Build the outer for expression + outerForExpr := fmt.Sprintf("for %s in %s : ", nSpec, hcl.GetAttrExpr(dSpec.forEach)) + outerTokens := hcl.TokensFromExpr(outerForExpr) + outerTokens = append(outerTokens, hcl.EncloseBracketsNewLines(innerTokens)...) - tokens := hcl.TokensFromExpr(exprStr.String()) + // Apply flatten to the entire expression + tokens := hcl.TokensFuncFlatten(outerTokens) resourceb.RemoveBlock(dSpec.block) resourceb.SetAttributeRaw(nRepSpecs, tokens) diff --git a/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.out.tf b/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.out.tf index ebff63a..ab11ef1 100644 --- a/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.out.tf +++ b/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.out.tf @@ -1,23 +1,23 @@ resource "mongodbatlas_advanced_cluster" "dynamic_replication_specs" { - project_id = var.project_id - name = var.cluster_name - cluster_type = "GEOSHARDED" + project_id = var.project_id + name = var.cluster_name + cluster_type = "GEOSHARDED" replication_specs = flatten([ for spec in var.replication_specs : [ for i in range(spec.num_shards) : { zone_name = spec.zone_name region_configs = [ for region in spec.region_configs : { - priority = region.priority + priority = region.priority provider_name = region.provider_name - region_name = region.region_name + region_name = region.region_name electable_specs = { instance_size = region.instance_size - node_count = region.electable_node_count + node_count = region.electable_node_count } read_only_specs = { instance_size = region.instance_size - node_count = region.read_only_node_count + node_count = region.read_only_node_count } } ] From 3ef174b4ce5cf4d77afc1d9fa25876f57ee4b504 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Fri, 22 Aug 2025 10:58:09 +0200 Subject: [PATCH 10/43] allow numerical and variable num_shards in clu2adv --- README.md | 1 - internal/convert/clu2adv.go | 153 ++++++++++++++---- internal/convert/testdata/clu2adv/errors.json | 3 +- .../clu2adv/multi_replication_specs.in.tf | 74 ++++++++- .../clu2adv/multi_replication_specs.out.tf | 132 ++++++++++++++- ...ication_specs_non_literal_num_shards.in.tf | 19 --- 6 files changed, 324 insertions(+), 58 deletions(-) delete mode 100644 internal/convert/testdata/clu2adv/replication_specs_non_literal_num_shards.in.tf diff --git a/README.md b/README.md index eb3839c..4e6fef0 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,6 @@ dynamic "replication_specs" { ### Limitations -- [`num_shards`](https://registry.terraform.io/providers/mongodb/mongodbatlas/latest/docs/resources/cluster#num_shards-2) in `replication_specs` must be a numeric [literal expression](https://developer.hashicorp.com/nomad/docs/job-specification/hcl2/expressions#literal-expressions), e.g. `var.num_shards` is not supported. This is to allow creating a `replication_specs` element per shard in `mongodbatlas_advanced_cluster`. This limitation doesn't apply if you're using `dynamic` blocks in `regions_config` or `replication_specs`. - `dynamic` blocks are supported with some [limitations](./docs/guide_clu2adv_dynamic_block.md). ## Feedback diff --git a/internal/convert/clu2adv.go b/internal/convert/clu2adv.go index 6897f67..0f09788 100644 --- a/internal/convert/clu2adv.go +++ b/internal/convert/clu2adv.go @@ -181,46 +181,135 @@ func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error { 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 + + // Collect all replication_specs blocks first + var repSpecBlocks []*hclwrite.Block for { - var ( - specSrc = resourceb.FirstMatchingBlock(nRepSpecs, nil) - spec = hclwrite.NewEmptyFile() - specb = spec.Body() - ) - if specSrc == nil { + block := resourceb.FirstMatchingBlock(nRepSpecs, nil) + if block == nil { break } - specbSrc := specSrc.Body() - d, err := fillWithDynamicRegionConfigs(specbSrc, root, false) - if err != nil { - return err - } - if d.IsPresent() { - resourceb.RemoveBlock(specSrc) - resourceb.SetAttributeRaw(nRepSpecs, d.tokens) - return nil - } - // ok to fail as zone_name is optional - _ = hcl.MoveAttr(specbSrc, specb, nZoneName, nZoneName, errRepSpecs) - shards := specbSrc.GetAttribute(nNumShards) - if shards == nil { - return fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) + resourceb.RemoveBlock(block) + repSpecBlocks = append(repSpecBlocks, block) + } + + if len(repSpecBlocks) == 0 { + return fmt.Errorf("%s: no replication_specs found", errRepSpecs) + } + + // Check if any replication_specs has a variable num_shards + hasVariableNumShards := false + for _, block := range repSpecBlocks { + shardsAttr := block.Body().GetAttribute(nNumShards) + if shardsAttr != nil { + _, err := hcl.GetAttrInt(shardsAttr, errNumShards) + if err != nil { + hasVariableNumShards = true + break + } } - shardsVal, err := hcl.GetAttrInt(shards, errNumShards) - if err != nil { - return err + } + + // If we have any variable num_shards, we need to use concat + if hasVariableNumShards { + var concatParts []hclwrite.Tokens + + for _, block := range repSpecBlocks { + spec := hclwrite.NewEmptyFile() + specb := spec.Body() + specbSrc := block.Body() + + // Check for dynamic region configs + d, err := fillWithDynamicRegionConfigs(specbSrc, root, false) + if err != nil { + return err + } + if d.IsPresent() { + concatParts = append(concatParts, d.tokens) + continue + } + + // Handle zone_name + _ = hcl.MoveAttr(specbSrc, specb, nZoneName, nZoneName, errRepSpecs) + + // Handle num_shards + shardsAttr := specbSrc.GetAttribute(nNumShards) + if shardsAttr == nil { + return fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) + } + + shardsVal, err := hcl.GetAttrInt(shardsAttr, errNumShards) + + if errConfig := fillRegionConfigs(specb, specbSrc, root); errConfig != nil { + return errConfig + } + + if err != nil { + // num_shards is a variable/expression + shardsExpr := hcl.GetAttrExpr(shardsAttr) + forExpr := fmt.Sprintf("for i in range(%s) :", shardsExpr) + tokens := hcl.TokensFromExpr(forExpr) + tokens = append(tokens, hcl.TokensObject(specb)...) + concatParts = append(concatParts, hcl.EncloseBracketsNewLines(tokens)) + } else { + // num_shards is a literal number - create explicit array + var specs []*hclwrite.Body + for range shardsVal { + specs = append(specs, specb) + } + concatParts = append(concatParts, hcl.TokensArray(specs)) + } } - if err := fillRegionConfigs(specb, specbSrc, root); err != nil { - return err + + // Use concat to combine all parts + if len(concatParts) > 1 { + resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensFuncConcat(concatParts...)) + } else { + resourceb.SetAttributeRaw(nRepSpecs, concatParts[0]) } - for range shardsVal { - specbs = append(specbs, specb) + } else { + // All num_shards are numeric, use simple array + var specbs []*hclwrite.Body + for _, block := range repSpecBlocks { + spec := hclwrite.NewEmptyFile() + specb := spec.Body() + specbSrc := block.Body() + + // Check for dynamic region configs + d, err := fillWithDynamicRegionConfigs(specbSrc, root, false) + if err != nil { + return err + } + if d.IsPresent() { + // For dynamic blocks that have numerical num_shards + // Extract the tokens and add to array + // This is complex, for now just return the dynamic block as is + resourceb.SetAttributeRaw(nRepSpecs, d.tokens) + return nil + } + + // Handle zone_name + _ = hcl.MoveAttr(specbSrc, specb, nZoneName, nZoneName, errRepSpecs) + + // Handle num_shards + shardsAttr := specbSrc.GetAttribute(nNumShards) + if shardsAttr == nil { + return fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) + } + + shardsVal, _ := hcl.GetAttrInt(shardsAttr, errNumShards) + + if err := fillRegionConfigs(specb, specbSrc, root); err != nil { + return err + } + + for range shardsVal { + specbs = append(specbs, specb) + } } - resourceb.RemoveBlock(specSrc) + resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensArray(specbs)) } - resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensArray(specbs)) + return nil } diff --git a/internal/convert/testdata/clu2adv/errors.json b/internal/convert/testdata/clu2adv/errors.json index 67fa980..9fa5f38 100644 --- a/internal/convert/testdata/clu2adv/errors.json +++ b/internal/convert/testdata/clu2adv/errors.json @@ -4,6 +4,5 @@ "free_cluster_missing_attribute": "free cluster (because no replication_specs): attribute backing_provider_name not found", "regions_config_missing_priority": "setting replication_specs: attribute priority 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" + "replication_specs_missing_regions_config": "setting replication_specs: regions_config not found" } diff --git a/internal/convert/testdata/clu2adv/multi_replication_specs.in.tf b/internal/convert/testdata/clu2adv/multi_replication_specs.in.tf index 0aa09b8..de838fa 100644 --- a/internal/convert/testdata/clu2adv/multi_replication_specs.in.tf +++ b/internal/convert/testdata/clu2adv/multi_replication_specs.in.tf @@ -1,4 +1,4 @@ -resource "mongodbatlas_cluster" "multirep" { +resource "mongodbatlas_cluster" "basic" { project_id = var.project_id name = "multirep" disk_size_gb = 80 @@ -27,7 +27,7 @@ resource "mongodbatlas_cluster" "multirep" { } } -resource "mongodbatlas_cluster" "geo" { +resource "mongodbatlas_cluster" "multiple_numerical_num_shards" { project_id = "1234" name = "geo" disk_size_gb = 80 @@ -57,3 +57,73 @@ resource "mongodbatlas_cluster" "geo" { } } } + +resource "mongodbatlas_cluster" "variable_num_shards" { + project_id = var.project_id + name = "multirep" + cluster_type = "GEOSHARDED" + provider_name = "AWS" + provider_instance_size_name = "M10" + replication_specs { + zone_name = "Zone 1" + num_shards = var.num_shards + regions_config { + region_name = "US_EAST_1" + electable_nodes = 3 + priority = 7 + } + } +} + +resource "mongodbatlas_cluster" "multiple_variable_num_shards" { + project_id = var.project_id + name = "multirep" + cluster_type = "GEOSHARDED" + provider_name = "AWS" + provider_instance_size_name = "M10" + replication_specs { + zone_name = "Zone 1" + num_shards = var.num_shards_rep1 + regions_config { + region_name = "US_EAST_1" + electable_nodes = 3 + priority = 7 + } + } + replication_specs { + zone_name = "Zone 2" + num_shards = var.num_shards_rep2 + regions_config { + region_name = "US_WEST_2" + electable_nodes = 3 + priority = 7 + } + } +} + +resource "mongodbatlas_cluster" "mix_variable_numerical_num_shards" { + project_id = var.project_id + name = "multirep" + cluster_type = "GEOSHARDED" + provider_name = "AWS" + provider_instance_size_name = "M10" + disk_size_gb = 80 + replication_specs { + zone_name = "Zone 1" + num_shards = 2 + regions_config { + region_name = "US_EAST_1" + electable_nodes = 3 + priority = 7 + } + } + replication_specs { + zone_name = "Zone 2" + num_shards = var.num_shards_rep2 + regions_config { + region_name = "US_WEST_2" + electable_nodes = 3 + priority = 7 + } + } +} diff --git a/internal/convert/testdata/clu2adv/multi_replication_specs.out.tf b/internal/convert/testdata/clu2adv/multi_replication_specs.out.tf index cbb3526..3544d03 100644 --- a/internal/convert/testdata/clu2adv/multi_replication_specs.out.tf +++ b/internal/convert/testdata/clu2adv/multi_replication_specs.out.tf @@ -1,4 +1,4 @@ -resource "mongodbatlas_advanced_cluster" "multirep" { +resource "mongodbatlas_advanced_cluster" "basic" { project_id = var.project_id name = "multirep" cluster_type = "GEOSHARDED" @@ -40,7 +40,7 @@ resource "mongodbatlas_advanced_cluster" "multirep" { # Please review the changes and confirm that references to this resource are updated. } -resource "mongodbatlas_advanced_cluster" "geo" { +resource "mongodbatlas_advanced_cluster" "multiple_numerical_num_shards" { project_id = "1234" name = "geo" cluster_type = "GEOSHARDED" @@ -126,3 +126,131 @@ resource "mongodbatlas_advanced_cluster" "geo" { # Generated by atlas-cli-plugin-terraform. # Please review the changes and confirm that references to this resource are updated. } + +resource "mongodbatlas_advanced_cluster" "variable_num_shards" { + project_id = var.project_id + name = "multirep" + cluster_type = "GEOSHARDED" + replication_specs = [ + for i in range(var.num_shards) : { + zone_name = "Zone 1" + region_configs = [ + { + provider_name = "AWS" + region_name = "US_EAST_1" + priority = 7 + electable_specs = { + node_count = 3 + instance_size = "M10" + } + } + ] + } + ] + + # Generated by atlas-cli-plugin-terraform. + # Please review the changes and confirm that references to this resource are updated. +} + +resource "mongodbatlas_advanced_cluster" "multiple_variable_num_shards" { + project_id = var.project_id + name = "multirep" + cluster_type = "GEOSHARDED" + replication_specs = concat( + [ + for i in range(var.num_shards_rep1) : { + zone_name = "Zone 1" + region_configs = [ + { + provider_name = "AWS" + region_name = "US_EAST_1" + priority = 7 + electable_specs = { + node_count = 3 + instance_size = "M10" + } + } + ] + } + ], + [ + for i in range(var.num_shards_rep2) : { + zone_name = "Zone 2" + region_configs = [ + { + provider_name = "AWS" + region_name = "US_WEST_2" + priority = 7 + electable_specs = { + node_count = 3 + instance_size = "M10" + } + } + ] + } + ] + ) + + # Generated by atlas-cli-plugin-terraform. + # Please review the changes and confirm that references to this resource are updated. +} + +resource "mongodbatlas_advanced_cluster" "mix_variable_numerical_num_shards" { + project_id = var.project_id + name = "multirep" + cluster_type = "GEOSHARDED" + replication_specs = concat( + [ + { + zone_name = "Zone 1" + region_configs = [ + { + provider_name = "AWS" + region_name = "US_EAST_1" + priority = 7 + electable_specs = { + node_count = 3 + instance_size = "M10" + disk_size_gb = 80 + } + } + ] + }, + { + zone_name = "Zone 1" + region_configs = [ + { + provider_name = "AWS" + region_name = "US_EAST_1" + priority = 7 + electable_specs = { + node_count = 3 + instance_size = "M10" + disk_size_gb = 80 + } + } + ] + } + ], + [ + for i in range(var.num_shards_rep2) : { + zone_name = "Zone 2" + region_configs = [ + { + provider_name = "AWS" + region_name = "US_WEST_2" + priority = 7 + electable_specs = { + node_count = 3 + instance_size = "M10" + disk_size_gb = 80 + } + } + ] + } + ] + ) + + # Generated by atlas-cli-plugin-terraform. + # Please review the changes and confirm that references to this resource are updated. +} diff --git a/internal/convert/testdata/clu2adv/replication_specs_non_literal_num_shards.in.tf b/internal/convert/testdata/clu2adv/replication_specs_non_literal_num_shards.in.tf deleted file mode 100644 index ba37e6d..0000000 --- a/internal/convert/testdata/clu2adv/replication_specs_non_literal_num_shards.in.tf +++ /dev/null @@ -1,19 +0,0 @@ -resource "mongodbatlas_cluster" "multirep" { - project_id = var.project_id - name = "multirep" - disk_size_gb = 80 - num_shards = 1 - cloud_backup = false - cluster_type = "GEOSHARDED" - provider_name = "AWS" - provider_instance_size_name = "M10" - replication_specs { - zone_name = "Zone 1" - num_shards = var.num_shards # unresolved num_shards - regions_config { - region_name = "US_EAST_1" - electable_nodes = 3 - priority = 7 - } - } -} From e2b2c9f22ff579fb3b93ccc65455fac0da4c8504 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:41:13 +0200 Subject: [PATCH 11/43] refactor common code to shared.go --- internal/convert/adv2v2.go | 78 +++++---------------- internal/convert/clu2adv.go | 113 ++----------------------------- internal/convert/shared.go | 131 ++++++++++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+), 168 deletions(-) create mode 100644 internal/convert/shared.go diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index b5aca36..88249c8 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -83,73 +83,31 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error } // Check if any replication_specs has a variable num_shards - hasVariableNumShards := false - for _, block := range repSpecBlocks { - numShardsAttr := block.Body().GetAttribute(nNumShards) - if numShardsAttr != nil { - _, err := hcl.GetAttrInt(numShardsAttr, errNumShards) - if err != nil { - hasVariableNumShards = true - break - } - } - } + hasVariableNumShards := HasVariableNumShards(repSpecBlocks) - // If we have any variable num_shards, we need to use concat if hasVariableNumShards { var concatParts []hclwrite.Tokens - hasMultipleParts := false for _, block := range repSpecBlocks { blockb := block.Body() numShardsAttr := blockb.GetAttribute(nNumShards) + blockb.RemoveAttribute(nNumShards) - if numShardsAttr != nil { - numShardsVal, err := hcl.GetAttrInt(numShardsAttr, errNumShards) - - if err != nil { - // num_shards is a variable/expression - numShardsExpr := hcl.GetAttrExpr(numShardsAttr) - blockb.RemoveAttribute(nNumShards) - - if err := convertConfig(blockb, diskSizeGB); err != nil { - return err - } - - forExpr := fmt.Sprintf("for i in range(%s) :", numShardsExpr) - tokens := hcl.TokensFromExpr(forExpr) - tokens = append(tokens, hcl.TokensObject(blockb)...) - concatParts = append(concatParts, hcl.EncloseBracketsNewLines(tokens)) - } else { - // num_shards is a literal number - create explicit array - blockb.RemoveAttribute(nNumShards) - - if err := convertConfig(blockb, diskSizeGB); err != nil { - return err - } - - var repSpecs []*hclwrite.Body - for range numShardsVal { - repSpecs = append(repSpecs, blockb) - } - concatParts = append(concatParts, hcl.TokensArray(repSpecs)) - hasMultipleParts = true - } - } else { - // No num_shards, default to 1 - if err := convertConfig(blockb, diskSizeGB); err != nil { - return err - } - concatParts = append(concatParts, hcl.TokensArraySingle(blockb)) - hasMultipleParts = true + if err := convertConfig(blockb, diskSizeGB); err != nil { + return err + } + + tokens, err := ProcessNumShards(numShardsAttr, blockb) + if err != nil { + return err } + concatParts = append(concatParts, tokens) } - // Use concat only if we have multiple parts or mixing numerical and variable - if len(concatParts) > 1 || hasMultipleParts { + // Use concat to combine all parts + if len(concatParts) > 1 { resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensFuncConcat(concatParts...)) } else { - // Only one variable num_shards, no need for concat resourceb.SetAttributeRaw(nRepSpecs, concatParts[0]) } } else { @@ -158,21 +116,19 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error for _, block := range repSpecBlocks { blockb := block.Body() numShardsAttr := blockb.GetAttribute(nNumShards) + blockb.RemoveAttribute(nNumShards) + + if err := convertConfig(blockb, diskSizeGB); err != nil { + return err + } if numShardsAttr != nil { numShardsVal, _ := hcl.GetAttrInt(numShardsAttr, errNumShards) - blockb.RemoveAttribute(nNumShards) - if err := convertConfig(blockb, diskSizeGB); err != nil { - return err - } for range numShardsVal { repSpecs = append(repSpecs, blockb) } } else { // No num_shards, default to 1 - if err := convertConfig(blockb, diskSizeGB); err != nil { - return err - } repSpecs = append(repSpecs, blockb) } } diff --git a/internal/convert/clu2adv.go b/internal/convert/clu2adv.go index 0f09788..cebdd75 100644 --- a/internal/convert/clu2adv.go +++ b/internal/convert/clu2adv.go @@ -198,19 +198,8 @@ func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error { } // Check if any replication_specs has a variable num_shards - hasVariableNumShards := false - for _, block := range repSpecBlocks { - shardsAttr := block.Body().GetAttribute(nNumShards) - if shardsAttr != nil { - _, err := hcl.GetAttrInt(shardsAttr, errNumShards) - if err != nil { - hasVariableNumShards = true - break - } - } - } + hasVariableNumShards := HasVariableNumShards(repSpecBlocks) - // If we have any variable num_shards, we need to use concat if hasVariableNumShards { var concatParts []hclwrite.Tokens @@ -238,27 +227,15 @@ func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error { return fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) } - shardsVal, err := hcl.GetAttrInt(shardsAttr, errNumShards) - if errConfig := fillRegionConfigs(specb, specbSrc, root); errConfig != nil { return errConfig } + tokens, err := ProcessNumShards(shardsAttr, specb) if err != nil { - // num_shards is a variable/expression - shardsExpr := hcl.GetAttrExpr(shardsAttr) - forExpr := fmt.Sprintf("for i in range(%s) :", shardsExpr) - tokens := hcl.TokensFromExpr(forExpr) - tokens = append(tokens, hcl.TokensObject(specb)...) - concatParts = append(concatParts, hcl.EncloseBracketsNewLines(tokens)) - } else { - // num_shards is a literal number - create explicit array - var specs []*hclwrite.Body - for range shardsVal { - specs = append(specs, specb) - } - concatParts = append(concatParts, hcl.TokensArray(specs)) + return err } + concatParts = append(concatParts, tokens) } // Use concat to combine all parts @@ -299,8 +276,8 @@ func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error { shardsVal, _ := hcl.GetAttrInt(shardsAttr, errNumShards) - if err := fillRegionConfigs(specb, specbSrc, root); err != nil { - return err + if errConfig := fillRegionConfigs(specb, specbSrc, root); errConfig != nil { + return errConfig } for range shardsVal { @@ -383,29 +360,6 @@ func extractTagsLabelsIndividual(resourceb *hclwrite.Body, name string) (hclwrit return hcl.TokensObject(fileb), nil } -func fillBlockOpt(resourceb *hclwrite.Body, name string) { - block := resourceb.FirstMatchingBlock(name, nil) - if block == nil { - return - } - resourceb.RemoveBlock(block) - resourceb.SetAttributeRaw(name, hcl.TokensObject(block.Body())) -} - -func fillAdvConfigOpt(resourceb *hclwrite.Body) { - block := resourceb.FirstMatchingBlock(nAdvConfig, nil) - if block == nil { - return - } - blockBody := block.Body() - - // Remove deprecated attributes from advanced_configuration - blockBody.RemoveAttribute(nFailIndexKeyTooLong) - blockBody.RemoveAttribute(nDefaultReadConcern) - - fillBlockOpt(resourceb, nAdvConfig) -} - // fillReplicationSpecsWithDynamicBlock used for dynamic blocks in replication_specs func fillReplicationSpecsWithDynamicBlock(resourceb *hclwrite.Body, root attrVals) (dynamicBlock, error) { dSpec, err := getDynamicBlock(resourceb, nRepSpecs) @@ -571,16 +525,6 @@ func setResourceName(resource *hclwrite.Block, name string) { resource.SetLabels(labels) } -// getResourceName returns the first label of a block, if it exists. -// e.g. in resource "mongodbatlas_cluster" "mycluster", the first label is "mongodbatlas_cluster". -func getResourceName(resource *hclwrite.Block) string { - labels := resource.Labels() - if len(labels) == 0 { - return "" - } - return labels[0] -} - // getResourceLabel returns the second label of a block, if it exists. // e.g. in resource "mongodbatlas_cluster" "mycluster", the second label is "mycluster". func getResourceLabel(resource *hclwrite.Block) string { @@ -591,17 +535,6 @@ func getResourceLabel(resource *hclwrite.Block) string { return labels[1] } -type dynamicBlock struct { - block *hclwrite.Block - forEach *hclwrite.Attribute - content *hclwrite.Block - tokens hclwrite.Tokens -} - -func (d dynamicBlock) IsPresent() bool { - return d.block != nil -} - func checkDynamicBlock(body *hclwrite.Body) error { for _, block := range body.Blocks() { name := getResourceName(block) @@ -613,25 +546,6 @@ func checkDynamicBlock(body *hclwrite.Body) error { return nil } -func getDynamicBlock(body *hclwrite.Body, name string) (dynamicBlock, error) { - for _, block := range body.Blocks() { - if block.Type() != nDynamic || name != getResourceName(block) { - continue - } - blockb := block.Body() - forEach := blockb.GetAttribute(nForEach) - if forEach == nil { - return dynamicBlock{}, fmt.Errorf("dynamic block %s: attribute %s not found", name, nForEach) - } - content := blockb.FirstMatchingBlock(nContent, nil) - if content == nil { - return dynamicBlock{}, fmt.Errorf("dynamic block %s: block %s not found", name, nContent) - } - return dynamicBlock{forEach: forEach, block: block, content: content}, nil - } - return dynamicBlock{}, nil -} - func replaceDynamicBlockExpr(attr *hclwrite.Attribute, blockName, attrName string) string { expr := hcl.GetAttrExpr(attr) return strings.ReplaceAll(expr, fmt.Sprintf("%s.%s", blockName, attrName), attrName) @@ -655,21 +569,6 @@ func getDynamicBlockRegionArray(forEach string, configSrc *hclwrite.Block, root return hcl.EncloseBracketsNewLines(tokens), nil } -func transformDynamicBlockReferences(configSrcb *hclwrite.Body, blockName, varName string) { - for name, attr := range configSrcb.Attributes() { - expr := replaceDynamicBlockReferences(hcl.GetAttrExpr(attr), blockName, varName) - configSrcb.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) - } -} - -// replaceDynamicBlockReferences changes value references, -// e.g. regions_config.value.electable_nodes to region.electable_nodes -func replaceDynamicBlockReferences(expr, blockName, varName string) string { - return strings.ReplaceAll(expr, - fmt.Sprintf("%s.%s.", blockName, nValue), - fmt.Sprintf("%s.", varName)) -} - func sortConfigsByPriority(configs []*hclwrite.Body) []*hclwrite.Body { for _, config := range configs { if _, err := hcl.GetAttrInt(config.GetAttribute(nPriority), errPriority); err != nil { diff --git a/internal/convert/shared.go b/internal/convert/shared.go new file mode 100644 index 0000000..80d0028 --- /dev/null +++ b/internal/convert/shared.go @@ -0,0 +1,131 @@ +package convert + +import ( + "fmt" + "strings" + + "github.com/hashicorp/hcl/v2/hclwrite" + "github.com/mongodb-labs/atlas-cli-plugin-terraform/internal/hcl" +) + +// HasVariableNumShards checks if any block has a variable (non-literal) num_shards attribute +func HasVariableNumShards(blocks []*hclwrite.Block) bool { + for _, block := range blocks { + if shardsAttr := block.Body().GetAttribute(nNumShards); shardsAttr != nil { + if _, err := hcl.GetAttrInt(shardsAttr, errNumShards); err != nil { + return true + } + } + } + return false +} + +// ProcessNumShards handles num_shards for a block, returning tokens for the expanded specs +// processedBody is the body with num_shards removed and other processing done +func ProcessNumShards(shardsAttr *hclwrite.Attribute, processedBody *hclwrite.Body) (hclwrite.Tokens, error) { + if shardsAttr == nil { + // No num_shards, default to 1 + return hcl.TokensArraySingle(processedBody), nil + } + + shardsVal, err := hcl.GetAttrInt(shardsAttr, errNumShards) + if err != nil { + // num_shards is a variable/expression + shardsExpr := hcl.GetAttrExpr(shardsAttr) + forExpr := fmt.Sprintf("for i in range(%s) :", shardsExpr) + tokens := hcl.TokensFromExpr(forExpr) + tokens = append(tokens, hcl.TokensObject(processedBody)...) + return hcl.EncloseBracketsNewLines(tokens), nil + } + + // num_shards is a literal number - create explicit array + var bodies []*hclwrite.Body + for i := 0; i < shardsVal; i++ { + bodies = append(bodies, processedBody) + } + return hcl.TokensArray(bodies), nil +} + +// dynamicBlock represents a Terraform dynamic block structure +type dynamicBlock struct { + block *hclwrite.Block + forEach *hclwrite.Attribute + content *hclwrite.Block + tokens hclwrite.Tokens +} + +// IsPresent returns true if the dynamic block exists +func (d dynamicBlock) IsPresent() bool { + return d.block != nil +} + +// getDynamicBlock finds and returns a dynamic block with the given name from the body +func getDynamicBlock(body *hclwrite.Body, name string) (dynamicBlock, error) { + for _, block := range body.Blocks() { + if block.Type() != nDynamic || name != getResourceName(block) { + continue + } + blockb := block.Body() + forEach := blockb.GetAttribute(nForEach) + if forEach == nil { + return dynamicBlock{}, fmt.Errorf("dynamic block %s: attribute %s not found", name, nForEach) + } + content := blockb.FirstMatchingBlock(nContent, nil) + if content == nil { + return dynamicBlock{}, fmt.Errorf("dynamic block %s: block %s not found", name, nContent) + } + return dynamicBlock{forEach: forEach, block: block, content: content}, nil + } + return dynamicBlock{}, nil +} + +// getResourceName returns the first label of a block, if it exists. +// e.g. in resource "mongodbatlas_cluster" "mycluster", the first label is "mongodbatlas_cluster". +func getResourceName(resource *hclwrite.Block) string { + labels := resource.Labels() + if len(labels) == 0 { + return "" + } + return labels[0] +} + +// replaceDynamicBlockReferences changes value references, +// e.g. regions_config.value.electable_nodes to region.electable_nodes +func replaceDynamicBlockReferences(expr, blockName, varName string) string { + return strings.ReplaceAll(expr, + fmt.Sprintf("%s.%s.", blockName, nValue), + fmt.Sprintf("%s.", varName)) +} + +// transformDynamicBlockReferences transforms all attribute references in a body from dynamic block format +func transformDynamicBlockReferences(configSrcb *hclwrite.Body, blockName, varName string) { + for name, attr := range configSrcb.Attributes() { + expr := replaceDynamicBlockReferences(hcl.GetAttrExpr(attr), blockName, varName) + configSrcb.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) + } +} + +// fillBlockOpt converts a block to an attribute with object value +func fillBlockOpt(resourceb *hclwrite.Body, name string) { + block := resourceb.FirstMatchingBlock(name, nil) + if block == nil { + return + } + resourceb.RemoveBlock(block) + resourceb.SetAttributeRaw(name, hcl.TokensObject(block.Body())) +} + +// fillAdvConfigOpt fills the advanced_configuration attribute, removing deprecated attributes +func fillAdvConfigOpt(resourceb *hclwrite.Body) { + block := resourceb.FirstMatchingBlock(nAdvConfig, nil) + if block == nil { + return + } + blockBody := block.Body() + + // Remove deprecated attributes from advanced_configuration + blockBody.RemoveAttribute(nFailIndexKeyTooLong) + blockBody.RemoveAttribute(nDefaultReadConcern) + + fillBlockOpt(resourceb, nAdvConfig) +} From 658e1247129f257a97990993105711d5635574ab Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Fri, 22 Aug 2025 13:06:18 +0200 Subject: [PATCH 12/43] allow different names --- internal/convert/adv2v2.go | 193 +++++++++++------- ...replication_specs_different_var_name.in.tf | 48 +++++ ...eplication_specs_different_var_name.out.tf | 50 +++++ 3 files changed, 221 insertions(+), 70 deletions(-) create mode 100644 internal/convert/testdata/adv2v2/dynamic_replication_specs_different_var_name.in.tf create mode 100644 internal/convert/testdata/adv2v2/dynamic_replication_specs_different_var_name.out.tf diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index 88249c8..6edf505 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -200,6 +200,55 @@ func convertDynamicRepSpecs(resourceb *hclwrite.Body, dSpec dynamicBlock, diskSi return nil } +// Helper function to add attributes in order +func addAttributesInOrder(targetBody *hclwrite.Body, sourceAttrs map[string]*hclwrite.Attribute, + orderedNames []string) { + // Add ordered attributes first + for _, name := range orderedNames { + if attr, exists := sourceAttrs[name]; exists { + targetBody.SetAttributeRaw(name, hcl.TokensFromExpr(hcl.GetAttrExpr(attr))) + } + } + + // Add any remaining attributes alphabetically + var remainingAttrs []string + for name := range sourceAttrs { + found := false + for _, orderedName := range orderedNames { + if name == orderedName { + found = true + break + } + } + if !found { + remainingAttrs = append(remainingAttrs, name) + } + } + slices.Sort(remainingAttrs) + for _, name := range remainingAttrs { + attr := sourceAttrs[name] + targetBody.SetAttributeRaw(name, hcl.TokensFromExpr(hcl.GetAttrExpr(attr))) + } +} + +// Helper function to process blocks for region configs +func processRegionConfigBlocks(targetBody *hclwrite.Body, blocks []*hclwrite.Block) { + for _, block := range blocks { + blockType := block.Type() + blockFile := hclwrite.NewEmptyFile() + blockBody := blockFile.Body() + + // For electable_specs, use specific order: instance_size, node_count + var attrOrder []string + if blockType == "electable_specs" { + attrOrder = []string{"instance_size", "node_count"} + } + + addAttributesInOrder(blockBody, block.Body().Attributes(), attrOrder) + targetBody.SetAttributeRaw(blockType, hcl.TokensObject(blockBody)) + } +} + func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dConfig dynamicBlock, diskSizeGB hclwrite.Tokens) error { // Get the block name from the dynamic block @@ -226,51 +275,20 @@ func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dC } } - // Don't convert specs blocks to attributes - we'll handle them manually in the string building - // Build the expression using HCL functions - configForEach := replaceDynamicBlockReferences(hcl.GetAttrExpr(dConfig.forEach), nRepSpecs, nSpec) + // Use standardized property name (region_configs) instead of the actual for_each collection + configForEach := fmt.Sprintf("%s.%s", nSpec, nConfig) // Create the inner region_configs body regionConfigFile := hclwrite.NewEmptyFile() regionConfigBody := regionConfigFile.Body() - // Add all attributes generically in alphabetical order - attrs := dConfig.content.Body().Attributes() - attrNames := make([]string, 0, len(attrs)) - for name := range attrs { - attrNames = append(attrNames, name) - } - slices.Sort(attrNames) - - for _, name := range attrNames { - attr := attrs[name] - regionConfigBody.SetAttributeRaw(name, hcl.TokensFromExpr(hcl.GetAttrExpr(attr))) - } + // Add attributes in a specific order for consistency + orderedAttrs := []string{"priority", "provider_name", "region_name"} + addAttributesInOrder(regionConfigBody, dConfig.content.Body().Attributes(), orderedAttrs) // Add all blocks generically as objects - for _, block := range dConfig.content.Body().Blocks() { - blockType := block.Type() - - // Create a new body for the block - blockFile := hclwrite.NewEmptyFile() - blockBody := blockFile.Body() - - // Add block attributes in alphabetical order - blockAttrs := block.Body().Attributes() - blockAttrNames := make([]string, 0, len(blockAttrs)) - for name := range blockAttrs { - blockAttrNames = append(blockAttrNames, name) - } - slices.Sort(blockAttrNames) - - for _, attrName := range blockAttrNames { - attr := blockAttrs[attrName] - blockBody.SetAttributeRaw(attrName, hcl.TokensFromExpr(hcl.GetAttrExpr(attr))) - } - - regionConfigBody.SetAttributeRaw(blockType, hcl.TokensObject(blockBody)) - } + processRegionConfigBlocks(regionConfigBody, dConfig.content.Body().Blocks()) // Build the region_configs for expression regionForExpr := fmt.Sprintf("for %s in %s :", nRegion, configForEach) @@ -306,8 +324,46 @@ func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dC return nil } // No num_shards, handle like without nested shards - // Reuse the block name from the dynamic block that was already obtained + return convertDynamicRepSpecsWithoutNumShards(resourceb, dSpec, dConfig, diskSizeGB, configBlockName) +} +// Helper function to add attributes with transformation +func addAttributesWithTransform(targetBody *hclwrite.Body, sourceAttrs map[string]*hclwrite.Attribute, + orderedNames []string, configBlockName string, transformFunc func(string) string) { + // Add ordered attributes first + for _, name := range orderedNames { + if attr, exists := sourceAttrs[name]; exists { + expr := hcl.GetAttrExpr(attr) + // First replace nested dynamic block references + expr = replaceDynamicBlockReferences(expr, configBlockName, nRegion) + // Then replace outer dynamic block references + expr = replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) + targetBody.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) + } + } + + // Add any remaining attributes + for name, attr := range sourceAttrs { + found := false + for _, orderedName := range orderedNames { + if name == orderedName { + found = true + break + } + } + if !found { + expr := hcl.GetAttrExpr(attr) + // First replace nested dynamic block references + expr = replaceDynamicBlockReferences(expr, configBlockName, nRegion) + // Then replace outer dynamic block references + expr = replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) + targetBody.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) + } + } +} + +func convertDynamicRepSpecsWithoutNumShards(resourceb *hclwrite.Body, dSpec, dConfig dynamicBlock, + diskSizeGB hclwrite.Tokens, configBlockName string) error { // Get zone_name if present repSpecFile := hclwrite.NewEmptyFile() repSpecb := repSpecFile.Body() @@ -321,28 +377,22 @@ func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dC configFile := hclwrite.NewEmptyFile() configb := configFile.Body() - // Copy and transform attributes - for name, attr := range dConfig.content.Body().Attributes() { - expr := hcl.GetAttrExpr(attr) - // First replace nested dynamic block references - expr = replaceDynamicBlockReferences(expr, configBlockName, nRegion) - // Then replace outer dynamic block references - expr = replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) - configb.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) - } + // Copy and transform attributes in a specific order for consistency + orderedAttrs := []string{"priority", "provider_name", "region_name"} + addAttributesWithTransform(configb, dConfig.content.Body().Attributes(), orderedAttrs, configBlockName, nil) // Process blocks and transform their references for _, block := range dConfig.content.Body().Blocks() { newBlock := configb.AppendNewBlock(block.Type(), block.Labels()) newBlockb := newBlock.Body() - for name, attr := range block.Body().Attributes() { - expr := hcl.GetAttrExpr(attr) - // First replace nested dynamic block references - expr = replaceDynamicBlockReferences(expr, configBlockName, nRegion) - // Then replace outer dynamic block references - expr = replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) - newBlockb.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) + + // Order attributes for consistency + var attrOrder []string + if block.Type() == "electable_specs" { + attrOrder = []string{"instance_size", "node_count"} } + + addAttributesWithTransform(newBlockb, block.Body().Attributes(), attrOrder, configBlockName, nil) } // Process specs @@ -353,22 +403,25 @@ func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dC fillSpecOpt(configb, nAnalyticsAutoScaling, nil) // Build the nested for expression for region_configs - configForEach := replaceDynamicBlockReferences(hcl.GetAttrExpr(dConfig.forEach), nRepSpecs, nSpec) - var regionTokens2 hclwrite.Tokens - regionTokens2 = append(regionTokens2, hcl.TokensFromExpr("[\n")...) - forExpr := fmt.Sprintf(" for %s in %s : ", nRegion, configForEach) - regionTokens2 = append(regionTokens2, hcl.TokensFromExpr(forExpr)...) - regionTokens2 = append(regionTokens2, hcl.TokensObject(configb)...) - regionTokens2 = append(regionTokens2, hcl.TokensFromExpr("\n ]")...) - repSpecb.SetAttributeRaw(nConfig, regionTokens2) - - // Build the for expression without range - var tokens hclwrite.Tokens - tokens = append(tokens, hcl.TokensFromExpr("[\n")...) - specForExpr := fmt.Sprintf(" for %s in %s : ", nSpec, hcl.GetAttrExpr(dSpec.forEach)) - tokens = append(tokens, hcl.TokensFromExpr(specForExpr)...) - tokens = append(tokens, hcl.TokensObject(repSpecb)...) - tokens = append(tokens, hcl.TokensFromExpr("\n ]")...) + // Use standardized property name (region_configs) instead of the actual for_each collection + configForEach := fmt.Sprintf("%s.%s", nSpec, nConfig) + + // Build the region_configs for expression + regionForExpr := fmt.Sprintf("for %s in %s :", nRegion, configForEach) + regionTokens := hcl.TokensFromExpr(regionForExpr) + regionTokens = append(regionTokens, hcl.TokensObject(configb)...) + + repSpecb.SetAttributeRaw(nConfig, hcl.EncloseBracketsNewLines(regionTokens)) + + // Build the for expression as an array wrapped in flatten + // Format: flatten([for spec in ... : [ { ... } ] ]) + forExpr := fmt.Sprintf("for %s in %s : [\n ", nSpec, hcl.GetAttrExpr(dSpec.forEach)) + innerTokens := hcl.TokensFromExpr(forExpr) + innerTokens = append(innerTokens, hcl.TokensObject(repSpecb)...) + innerTokens = append(innerTokens, hcl.TokensFromExpr("\n ]")...) + + // Apply flatten to the entire expression + tokens := hcl.TokensFuncFlatten(innerTokens) resourceb.RemoveBlock(dSpec.block) resourceb.SetAttributeRaw(nRepSpecs, tokens) diff --git a/internal/convert/testdata/adv2v2/dynamic_replication_specs_different_var_name.in.tf b/internal/convert/testdata/adv2v2/dynamic_replication_specs_different_var_name.in.tf new file mode 100644 index 0000000..e282e6c --- /dev/null +++ b/internal/convert/testdata/adv2v2/dynamic_replication_specs_different_var_name.in.tf @@ -0,0 +1,48 @@ +resource "mongodbatlas_advanced_cluster" "different_var_names" { + project_id = var.project_id + name = var.cluster_name + cluster_type = var.cluster_type + dynamic "replication_specs" { + for_each = var.my_rep_specs + content { + num_shards = replication_specs.value.my_shards + zone_name = replication_specs.value.my_zone + + dynamic "region_configs" { + for_each = replication_specs.value.my_regions + content { + priority = region_configs.value.prio + provider_name = region_configs.value.provider_name + region_name = region_configs.value.my_region_name + electable_specs { + instance_size = region_configs.value.instance_size + node_count = region_configs.value.my_electable_node_count + } + } + } + } + } +} + +resource "mongodbatlas_advanced_cluster" "different_var_names_no_zone_name_no_num_shards" { + project_id = var.project_id + name = var.cluster_name + cluster_type = var.cluster_type + dynamic "replication_specs" { + for_each = var.my_rep_specs + content { + dynamic "region_configs" { + for_each = replication_specs.value.my_regions + content { + priority = region_configs.value.prio + provider_name = region_configs.value.provider_name + region_name = region_configs.value.my_region_name + electable_specs { + instance_size = region_configs.value.instance_size + node_count = region_configs.value.my_electable_node_count + } + } + } + } + } +} diff --git a/internal/convert/testdata/adv2v2/dynamic_replication_specs_different_var_name.out.tf b/internal/convert/testdata/adv2v2/dynamic_replication_specs_different_var_name.out.tf new file mode 100644 index 0000000..485c541 --- /dev/null +++ b/internal/convert/testdata/adv2v2/dynamic_replication_specs_different_var_name.out.tf @@ -0,0 +1,50 @@ +resource "mongodbatlas_advanced_cluster" "different_var_names" { + project_id = var.project_id + name = var.cluster_name + cluster_type = var.cluster_type + replication_specs = flatten([ + for spec in var.my_rep_specs : [ + for i in range(spec.my_shards) : { + zone_name = spec.my_zone + region_configs = [ + for region in spec.region_configs : { + priority = region.prio + provider_name = region.provider_name + region_name = region.my_region_name + electable_specs = { + instance_size = region.instance_size + node_count = region.my_electable_node_count + } + } + ] + } + ] + ]) + + # Updated by atlas-cli-plugin-terraform, please review the changes. +} + +resource "mongodbatlas_advanced_cluster" "different_var_names_no_zone_name_no_num_shards" { + project_id = var.project_id + name = var.cluster_name + cluster_type = var.cluster_type + replication_specs = flatten([ + for spec in var.my_rep_specs : [ + { + region_configs = [ + for region in spec.region_configs : { + priority = region.prio + provider_name = region.provider_name + region_name = region.my_region_name + electable_specs = { + instance_size = region.instance_size + node_count = region.my_electable_node_count + } + } + ] + } + ] + ]) + + # Updated by atlas-cli-plugin-terraform, please review the changes. +} From 6d08f97e1ab7fcc65bedab82cdb21658932f26de Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Fri, 22 Aug 2025 13:29:58 +0200 Subject: [PATCH 13/43] order attributes to make tests deterministic --- internal/convert/adv2v2.go | 148 ++++++++++++++++--------------------- 1 file changed, 64 insertions(+), 84 deletions(-) diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index 6edf505..2dc344f 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -200,37 +200,6 @@ func convertDynamicRepSpecs(resourceb *hclwrite.Body, dSpec dynamicBlock, diskSi return nil } -// Helper function to add attributes in order -func addAttributesInOrder(targetBody *hclwrite.Body, sourceAttrs map[string]*hclwrite.Attribute, - orderedNames []string) { - // Add ordered attributes first - for _, name := range orderedNames { - if attr, exists := sourceAttrs[name]; exists { - targetBody.SetAttributeRaw(name, hcl.TokensFromExpr(hcl.GetAttrExpr(attr))) - } - } - - // Add any remaining attributes alphabetically - var remainingAttrs []string - for name := range sourceAttrs { - found := false - for _, orderedName := range orderedNames { - if name == orderedName { - found = true - break - } - } - if !found { - remainingAttrs = append(remainingAttrs, name) - } - } - slices.Sort(remainingAttrs) - for _, name := range remainingAttrs { - attr := sourceAttrs[name] - targetBody.SetAttributeRaw(name, hcl.TokensFromExpr(hcl.GetAttrExpr(attr))) - } -} - // Helper function to process blocks for region configs func processRegionConfigBlocks(targetBody *hclwrite.Body, blocks []*hclwrite.Block) { for _, block := range blocks { @@ -238,13 +207,18 @@ func processRegionConfigBlocks(targetBody *hclwrite.Body, blocks []*hclwrite.Blo blockFile := hclwrite.NewEmptyFile() blockBody := blockFile.Body() - // For electable_specs, use specific order: instance_size, node_count - var attrOrder []string - if blockType == "electable_specs" { - attrOrder = []string{"instance_size", "node_count"} + // Copy all attributes in deterministic order + attrs := block.Body().Attributes() + var names []string + for name := range attrs { + names = append(names, name) + } + slices.Sort(names) + for _, name := range names { + attr := attrs[name] + blockBody.SetAttributeRaw(name, hcl.TokensFromExpr(hcl.GetAttrExpr(attr))) } - addAttributesInOrder(blockBody, block.Body().Attributes(), attrOrder) targetBody.SetAttributeRaw(blockType, hcl.TokensObject(blockBody)) } } @@ -261,14 +235,28 @@ func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dC // Transform references in place for the dynamic config content transformDynamicBlockReferencesRecursive(dConfig.content.Body(), configBlockName, nRegion) - // Also transform outer references - for name, attr := range dConfig.content.Body().Attributes() { + // Also transform outer references (with deterministic ordering) + attrs := dConfig.content.Body().Attributes() + var attrNames []string + for name := range attrs { + attrNames = append(attrNames, name) + } + slices.Sort(attrNames) + for _, name := range attrNames { + attr := attrs[name] expr := hcl.GetAttrExpr(attr) expr = replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) dConfig.content.Body().SetAttributeRaw(name, hcl.TokensFromExpr(expr)) } for _, block := range dConfig.content.Body().Blocks() { - for name, attr := range block.Body().Attributes() { + blockAttrs := block.Body().Attributes() + var blockAttrNames []string + for name := range blockAttrs { + blockAttrNames = append(blockAttrNames, name) + } + slices.Sort(blockAttrNames) + for _, name := range blockAttrNames { + attr := blockAttrs[name] expr := hcl.GetAttrExpr(attr) expr = replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) block.Body().SetAttributeRaw(name, hcl.TokensFromExpr(expr)) @@ -283,9 +271,17 @@ func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dC regionConfigFile := hclwrite.NewEmptyFile() regionConfigBody := regionConfigFile.Body() - // Add attributes in a specific order for consistency - orderedAttrs := []string{"priority", "provider_name", "region_name"} - addAttributesInOrder(regionConfigBody, dConfig.content.Body().Attributes(), orderedAttrs) + // Copy all attributes in deterministic order + configAttrs := dConfig.content.Body().Attributes() + var configAttrNames []string + for name := range configAttrs { + configAttrNames = append(configAttrNames, name) + } + slices.Sort(configAttrNames) + for _, name := range configAttrNames { + attr := configAttrs[name] + regionConfigBody.SetAttributeRaw(name, hcl.TokensFromExpr(hcl.GetAttrExpr(attr))) + } // Add all blocks generically as objects processRegionConfigBlocks(regionConfigBody, dConfig.content.Body().Blocks()) @@ -329,36 +325,21 @@ func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dC // Helper function to add attributes with transformation func addAttributesWithTransform(targetBody *hclwrite.Body, sourceAttrs map[string]*hclwrite.Attribute, - orderedNames []string, configBlockName string, transformFunc func(string) string) { - // Add ordered attributes first - for _, name := range orderedNames { - if attr, exists := sourceAttrs[name]; exists { - expr := hcl.GetAttrExpr(attr) - // First replace nested dynamic block references - expr = replaceDynamicBlockReferences(expr, configBlockName, nRegion) - // Then replace outer dynamic block references - expr = replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) - targetBody.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) - } + configBlockName string) { + // Sort attribute names for deterministic output + var names []string + for name := range sourceAttrs { + names = append(names, name) } - - // Add any remaining attributes - for name, attr := range sourceAttrs { - found := false - for _, orderedName := range orderedNames { - if name == orderedName { - found = true - break - } - } - if !found { - expr := hcl.GetAttrExpr(attr) - // First replace nested dynamic block references - expr = replaceDynamicBlockReferences(expr, configBlockName, nRegion) - // Then replace outer dynamic block references - expr = replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) - targetBody.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) - } + slices.Sort(names) + for _, name := range names { + attr := sourceAttrs[name] + expr := hcl.GetAttrExpr(attr) + // First replace nested dynamic block references + expr = replaceDynamicBlockReferences(expr, configBlockName, nRegion) + // Then replace outer dynamic block references + expr = replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) + targetBody.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) } } @@ -377,22 +358,14 @@ func convertDynamicRepSpecsWithoutNumShards(resourceb *hclwrite.Body, dSpec, dCo configFile := hclwrite.NewEmptyFile() configb := configFile.Body() - // Copy and transform attributes in a specific order for consistency - orderedAttrs := []string{"priority", "provider_name", "region_name"} - addAttributesWithTransform(configb, dConfig.content.Body().Attributes(), orderedAttrs, configBlockName, nil) + // Copy and transform attributes + addAttributesWithTransform(configb, dConfig.content.Body().Attributes(), configBlockName) // Process blocks and transform their references for _, block := range dConfig.content.Body().Blocks() { newBlock := configb.AppendNewBlock(block.Type(), block.Labels()) newBlockb := newBlock.Body() - - // Order attributes for consistency - var attrOrder []string - if block.Type() == "electable_specs" { - attrOrder = []string{"instance_size", "node_count"} - } - - addAttributesWithTransform(newBlockb, block.Body().Attributes(), attrOrder, configBlockName, nil) + addAttributesWithTransform(newBlockb, block.Body().Attributes(), configBlockName) } // Process specs @@ -494,8 +467,15 @@ func convertDynamicConfig(repSpecs *hclwrite.Body, dConfig dynamicBlock, diskSiz } func transformDynamicBlockReferencesRecursive(body *hclwrite.Body, blockName, varName string) { - // Transform attributes - for name, attr := range body.Attributes() { + // Transform attributes in deterministic order + attrs := body.Attributes() + var names []string + for name := range attrs { + names = append(names, name) + } + slices.Sort(names) + for _, name := range names { + attr := attrs[name] expr := replaceDynamicBlockReferences(hcl.GetAttrExpr(attr), blockName, varName) body.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) } From aa0b30cd74044e8fa9df9e81eb43688e0cb294f3 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Fri, 22 Aug 2025 13:35:43 +0200 Subject: [PATCH 14/43] include tags in rep_spec test --- .../dynamic_replication_specs_basic.in.tf | 20 +++++++++++++++++++ .../dynamic_replication_specs_basic.out.tf | 15 ++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.in.tf b/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.in.tf index dd968ef..4d8ced2 100644 --- a/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.in.tf +++ b/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.in.tf @@ -1,7 +1,27 @@ resource "mongodbatlas_advanced_cluster" "dynamic_replication_specs" { + 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 = "GEOSHARDED" + + dynamic "tags" { + for_each = var.tags + content { + key = tags.key + value = tags.value + } + } + tags { + key = "Tag 2" + value = "Value 2" + } + dynamic "replication_specs" { for_each = var.replication_specs content { diff --git a/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.out.tf b/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.out.tf index ab11ef1..c63dda0 100644 --- a/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.out.tf +++ b/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.out.tf @@ -1,7 +1,16 @@ resource "mongodbatlas_advanced_cluster" "dynamic_replication_specs" { + 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 = "GEOSHARDED" + + replication_specs = flatten([ for spec in var.replication_specs : [ for i in range(spec.num_shards) : { @@ -24,6 +33,12 @@ resource "mongodbatlas_advanced_cluster" "dynamic_replication_specs" { } ] ]) + tags = merge( + var.tags, + { + "Tag 2" = "Value 2" + } + ) # Updated by atlas-cli-plugin-terraform, please review the changes. } From 31ad9bcdcd4ac0d18d2fbcaf5ac8d47eb20d45c1 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Fri, 22 Aug 2025 13:44:50 +0200 Subject: [PATCH 15/43] disk_size_gb in dynamic blocks --- .../adv2v2/dynamic_regions_config_basic.in.tf | 23 +++++++++++++++++ .../dynamic_regions_config_basic.out.tf | 25 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/internal/convert/testdata/adv2v2/dynamic_regions_config_basic.in.tf b/internal/convert/testdata/adv2v2/dynamic_regions_config_basic.in.tf index 5a76fcf..562a2f7 100644 --- a/internal/convert/testdata/adv2v2/dynamic_regions_config_basic.in.tf +++ b/internal/convert/testdata/adv2v2/dynamic_regions_config_basic.in.tf @@ -20,6 +20,29 @@ resource "mongodbatlas_advanced_cluster" "dynamic_regions_config" { } } +resource "mongodbatlas_advanced_cluster" "using_disk_size_gb" { + project_id = var.project_id + name = "cluster" + cluster_type = "SHARDED" + disk_size_gb = 123 + replication_specs { + num_shards = var.replication_specs.num_shards + zone_name = var.zone_name + dynamic "region_configs" { + for_each = var.replication_specs.region_configs + content { + priority = region_configs.value.prio + provider_name = "AWS" + region_name = region_configs.value.region_name + electable_specs { + instance_size = region_configs.value.instance_size + node_count = region_configs.value.node_count + } + } + } + } +} + # example of variable for demostration purposes, not used in the conversion variable "replication_specs" { type = object({ diff --git a/internal/convert/testdata/adv2v2/dynamic_regions_config_basic.out.tf b/internal/convert/testdata/adv2v2/dynamic_regions_config_basic.out.tf index 0ffde3f..0ca86c7 100644 --- a/internal/convert/testdata/adv2v2/dynamic_regions_config_basic.out.tf +++ b/internal/convert/testdata/adv2v2/dynamic_regions_config_basic.out.tf @@ -22,6 +22,31 @@ resource "mongodbatlas_advanced_cluster" "dynamic_regions_config" { # Updated by atlas-cli-plugin-terraform, please review the changes. } +resource "mongodbatlas_advanced_cluster" "using_disk_size_gb" { + project_id = var.project_id + name = "cluster" + cluster_type = "SHARDED" + replication_specs = [ + for i in range(var.replication_specs.num_shards) : { + zone_name = var.zone_name + region_configs = [ + for region in var.replication_specs.region_configs : { + priority = region.prio + provider_name = "AWS" + region_name = region.region_name + electable_specs = { + instance_size = region.instance_size + node_count = region.node_count + disk_size_gb = 123 + } + } + ] + } + ] + + # Updated by atlas-cli-plugin-terraform, please review the changes. +} + # example of variable for demostration purposes, not used in the conversion variable "replication_specs" { type = object({ From 90fc849ba796098fdd254563c935e00b3bc927a1 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Fri, 22 Aug 2025 13:47:19 +0200 Subject: [PATCH 16/43] rename test files removing basic from filename --- ...ic_regions_config_basic.in.tf => dynamic_region_configs.in.tf} | 0 ..._regions_config_basic.out.tf => dynamic_region_configs.out.tf} | 0 ...lication_specs_basic.in.tf => dynamic_replication_specs.in.tf} | 0 ...cation_specs_basic.out.tf => dynamic_replication_specs.out.tf} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename internal/convert/testdata/adv2v2/{dynamic_regions_config_basic.in.tf => dynamic_region_configs.in.tf} (100%) rename internal/convert/testdata/adv2v2/{dynamic_regions_config_basic.out.tf => dynamic_region_configs.out.tf} (100%) rename internal/convert/testdata/adv2v2/{dynamic_replication_specs_basic.in.tf => dynamic_replication_specs.in.tf} (100%) rename internal/convert/testdata/adv2v2/{dynamic_replication_specs_basic.out.tf => dynamic_replication_specs.out.tf} (100%) diff --git a/internal/convert/testdata/adv2v2/dynamic_regions_config_basic.in.tf b/internal/convert/testdata/adv2v2/dynamic_region_configs.in.tf similarity index 100% rename from internal/convert/testdata/adv2v2/dynamic_regions_config_basic.in.tf rename to internal/convert/testdata/adv2v2/dynamic_region_configs.in.tf diff --git a/internal/convert/testdata/adv2v2/dynamic_regions_config_basic.out.tf b/internal/convert/testdata/adv2v2/dynamic_region_configs.out.tf similarity index 100% rename from internal/convert/testdata/adv2v2/dynamic_regions_config_basic.out.tf rename to internal/convert/testdata/adv2v2/dynamic_region_configs.out.tf diff --git a/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.in.tf b/internal/convert/testdata/adv2v2/dynamic_replication_specs.in.tf similarity index 100% rename from internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.in.tf rename to internal/convert/testdata/adv2v2/dynamic_replication_specs.in.tf diff --git a/internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.out.tf b/internal/convert/testdata/adv2v2/dynamic_replication_specs.out.tf similarity index 100% rename from internal/convert/testdata/adv2v2/dynamic_replication_specs_basic.out.tf rename to internal/convert/testdata/adv2v2/dynamic_replication_specs.out.tf From bb8ec139c2b61b9bc755bee402cf16e50891dc41 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Fri, 22 Aug 2025 14:13:40 +0200 Subject: [PATCH 17/43] support all specs with dynamic blocks --- internal/convert/adv2v2.go | 10 ++- .../adv2v2/dynamic_region_configs.in.tf | 73 +++++++++++++++--- .../adv2v2/dynamic_region_configs.out.tf | 77 ++++++++++++++++--- .../adv2v2/dynamic_replication_specs.in.tf | 53 +++++++++++++ .../adv2v2/dynamic_replication_specs.out.tf | 56 ++++++++++++++ 5 files changed, 243 insertions(+), 26 deletions(-) diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index 2dc344f..3cdc010 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -201,7 +201,7 @@ func convertDynamicRepSpecs(resourceb *hclwrite.Body, dSpec dynamicBlock, diskSi } // Helper function to process blocks for region configs -func processRegionConfigBlocks(targetBody *hclwrite.Body, blocks []*hclwrite.Block) { +func processRegionConfigBlocks(targetBody *hclwrite.Body, blocks []*hclwrite.Block, diskSizeGB hclwrite.Tokens) { for _, block := range blocks { blockType := block.Type() blockFile := hclwrite.NewEmptyFile() @@ -219,6 +219,12 @@ func processRegionConfigBlocks(targetBody *hclwrite.Body, blocks []*hclwrite.Blo blockBody.SetAttributeRaw(name, hcl.TokensFromExpr(hcl.GetAttrExpr(attr))) } + // Add disk_size_gb to specs blocks if needed + if diskSizeGB != nil && (blockType == nElectableSpecs || + blockType == nReadOnlySpecs || blockType == nAnalyticsSpecs) { + blockBody.SetAttributeRaw(nDiskSizeGB, diskSizeGB) + } + targetBody.SetAttributeRaw(blockType, hcl.TokensObject(blockBody)) } } @@ -284,7 +290,7 @@ func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dC } // Add all blocks generically as objects - processRegionConfigBlocks(regionConfigBody, dConfig.content.Body().Blocks()) + processRegionConfigBlocks(regionConfigBody, dConfig.content.Body().Blocks(), diskSizeGB) // Build the region_configs for expression regionForExpr := fmt.Sprintf("for %s in %s :", nRegion, configForEach) diff --git a/internal/convert/testdata/adv2v2/dynamic_region_configs.in.tf b/internal/convert/testdata/adv2v2/dynamic_region_configs.in.tf index 562a2f7..ddf9ba0 100644 --- a/internal/convert/testdata/adv2v2/dynamic_region_configs.in.tf +++ b/internal/convert/testdata/adv2v2/dynamic_region_configs.in.tf @@ -43,31 +43,80 @@ resource "mongodbatlas_advanced_cluster" "using_disk_size_gb" { } } +resource "mongodbatlas_advanced_cluster" "all_specs" { + project_id = var.project_id + name = "cluster" + cluster_type = "SHARDED" + disk_size_gb = 123 + replication_specs { + num_shards = var.replication_specs.num_shards + zone_name = var.zone_name + dynamic "region_configs" { + for_each = var.replication_specs.region_configs + content { + priority = region_configs.value.prio + provider_name = "AWS" + region_name = region_configs.value.region_name + electable_specs { + instance_size = region_configs.value.instance_size + node_count = region_configs.value.node_count + } + read_only_specs { + instance_size = region_configs.value.instance_size + node_count = region_configs.value.node_count_read_only + } + analytics_specs { + instance_size = region_configs.value.instance_size + node_count = region_configs.value.node_count_analytics + } + auto_scaling { + disk_gb_enabled = region_configs.value.enable_disk_gb + } + analytics_auto_scaling { + compute_enabled = region_configs.value.enable_compute + } + } + } + } +} + # example of variable for demostration purposes, not used in the conversion variable "replication_specs" { type = object({ num_shards = number region_configs = list(object({ - prio = number - region_name = string - instance_size = string - node_count = number + prio = number + region_name = string + instance_size = string + node_count = number + node_count_read_only = number + node_count_analytics = number + enable_disk_gb = bool + enable_compute = bool })) }) default = { num_shards = 3 region_configs = [ { - prio = 7 - region_name = "US_EAST_1" - instance_size = "M10" - node_count = 2 + prio = 7 + region_name = "US_EAST_1" + instance_size = "M10" + node_count = 2 + node_count_read_only = 1 + node_count_analytics = 0 + enable_disk_gb = true + enable_compute = false }, { - prio = 6 - region_name = "US_WEST_2" - instance_size = "M10" - node_count = 1 + prio = 6 + region_name = "US_WEST_2" + instance_size = "M10" + node_count = 1 + node_count_read_only = 0 + node_count_analytics = 1 + enable_disk_gb = false + enable_compute = true } ] } diff --git a/internal/convert/testdata/adv2v2/dynamic_region_configs.out.tf b/internal/convert/testdata/adv2v2/dynamic_region_configs.out.tf index 0ca86c7..0027791 100644 --- a/internal/convert/testdata/adv2v2/dynamic_region_configs.out.tf +++ b/internal/convert/testdata/adv2v2/dynamic_region_configs.out.tf @@ -47,31 +47,84 @@ resource "mongodbatlas_advanced_cluster" "using_disk_size_gb" { # Updated by atlas-cli-plugin-terraform, please review the changes. } +resource "mongodbatlas_advanced_cluster" "all_specs" { + project_id = var.project_id + name = "cluster" + cluster_type = "SHARDED" + replication_specs = [ + for i in range(var.replication_specs.num_shards) : { + zone_name = var.zone_name + region_configs = [ + for region in var.replication_specs.region_configs : { + priority = region.prio + provider_name = "AWS" + region_name = region.region_name + electable_specs = { + instance_size = region.instance_size + node_count = region.node_count + disk_size_gb = 123 + } + read_only_specs = { + instance_size = region.instance_size + node_count = region.node_count_read_only + disk_size_gb = 123 + } + analytics_specs = { + instance_size = region.instance_size + node_count = region.node_count_analytics + disk_size_gb = 123 + } + auto_scaling = { + disk_gb_enabled = region.enable_disk_gb + } + analytics_auto_scaling = { + compute_enabled = region.enable_compute + } + } + ] + } + ] + + # Updated by atlas-cli-plugin-terraform, please review the changes. +} + # example of variable for demostration purposes, not used in the conversion variable "replication_specs" { type = object({ num_shards = number region_configs = list(object({ - prio = number - region_name = string - instance_size = string - node_count = number + prio = number + region_name = string + instance_size = string + node_count = number + node_count_read_only = number + node_count_analytics = number + enable_disk_gb = bool + enable_compute = bool })) }) default = { num_shards = 3 region_configs = [ { - prio = 7 - region_name = "US_EAST_1" - instance_size = "M10" - node_count = 2 + prio = 7 + region_name = "US_EAST_1" + instance_size = "M10" + node_count = 2 + node_count_read_only = 1 + node_count_analytics = 0 + enable_disk_gb = true + enable_compute = false }, { - prio = 6 - region_name = "US_WEST_2" - instance_size = "M10" - node_count = 1 + prio = 6 + region_name = "US_WEST_2" + instance_size = "M10" + node_count = 1 + node_count_read_only = 0 + node_count_analytics = 1 + enable_disk_gb = false + enable_compute = true } ] } diff --git a/internal/convert/testdata/adv2v2/dynamic_replication_specs.in.tf b/internal/convert/testdata/adv2v2/dynamic_replication_specs.in.tf index 4d8ced2..6823e4b 100644 --- a/internal/convert/testdata/adv2v2/dynamic_replication_specs.in.tf +++ b/internal/convert/testdata/adv2v2/dynamic_replication_specs.in.tf @@ -47,6 +47,47 @@ resource "mongodbatlas_advanced_cluster" "dynamic_replication_specs" { } } +resource "mongodbatlas_advanced_cluster" "all_specs" { + project_id = var.project_id + name = var.cluster_name + cluster_type = "GEOSHARDED" + disk_size_gb = 123 + + dynamic "replication_specs" { + for_each = var.replication_specs + content { + num_shards = replication_specs.value.num_shards + zone_name = replication_specs.value.zone_name + dynamic "region_configs" { + for_each = replication_specs.value.region_configs + content { + priority = region_configs.value.priority + provider_name = region_configs.value.provider_name + region_name = region_configs.value.region_name + electable_specs { + instance_size = region_configs.value.instance_size + node_count = region_configs.value.electable_node_count + } + read_only_specs { + instance_size = region_configs.value.instance_size + node_count = region_configs.value.read_only_node_count + } + analytics_specs { + instance_size = region_configs.value.instance_size + node_count = region_configs.value.analytics_node_count + } + auto_scaling { + disk_gb_enabled = region_configs.value.enable_disk_gb + } + analytics_auto_scaling { + compute_enabled = region_configs.value.enable_compute + } + } + } + } + } +} + # example of variable for demostration purposes, not used in the conversion variable "replication_specs" { description = "List of replication specifications in mongodbatlas_advanced_cluster format" @@ -59,6 +100,9 @@ variable "replication_specs" { instance_size = string electable_node_count = number read_only_node_count = number + analytics_node_count = number + enable_disk_gb = bool + enable_compute = bool priority = number })) })) @@ -73,6 +117,9 @@ variable "replication_specs" { instance_size = "M10" electable_node_count = 3 read_only_node_count = 0 + analytics_node_count = 0 + enable_disk_gb = true + enable_compute = false priority = 7 } ] @@ -86,6 +133,9 @@ variable "replication_specs" { instance_size = "M10" electable_node_count = 2 read_only_node_count = 1 + analytics_node_count = 1 + enable_disk_gb = false + enable_compute = true priority = 7 }, { provider_name = "AWS" @@ -93,6 +143,9 @@ variable "replication_specs" { instance_size = "M10" electable_node_count = 1 read_only_node_count = 0 + analytics_node_count = 0 + enable_disk_gb = true + enable_compute = false priority = 6 } ] diff --git a/internal/convert/testdata/adv2v2/dynamic_replication_specs.out.tf b/internal/convert/testdata/adv2v2/dynamic_replication_specs.out.tf index c63dda0..513273c 100644 --- a/internal/convert/testdata/adv2v2/dynamic_replication_specs.out.tf +++ b/internal/convert/testdata/adv2v2/dynamic_replication_specs.out.tf @@ -43,6 +43,50 @@ resource "mongodbatlas_advanced_cluster" "dynamic_replication_specs" { # Updated by atlas-cli-plugin-terraform, please review the changes. } +resource "mongodbatlas_advanced_cluster" "all_specs" { + project_id = var.project_id + name = var.cluster_name + cluster_type = "GEOSHARDED" + + replication_specs = flatten([ + for spec in var.replication_specs : [ + for i in range(spec.num_shards) : { + zone_name = spec.zone_name + region_configs = [ + for region in spec.region_configs : { + priority = region.priority + provider_name = region.provider_name + region_name = region.region_name + electable_specs = { + instance_size = region.instance_size + node_count = region.electable_node_count + disk_size_gb = 123 + } + read_only_specs = { + instance_size = region.instance_size + node_count = region.read_only_node_count + disk_size_gb = 123 + } + analytics_specs = { + instance_size = region.instance_size + node_count = region.analytics_node_count + disk_size_gb = 123 + } + auto_scaling = { + disk_gb_enabled = region.enable_disk_gb + } + analytics_auto_scaling = { + compute_enabled = region.enable_compute + } + } + ] + } + ] + ]) + + # Updated by atlas-cli-plugin-terraform, please review the changes. +} + # example of variable for demostration purposes, not used in the conversion variable "replication_specs" { description = "List of replication specifications in mongodbatlas_advanced_cluster format" @@ -55,6 +99,9 @@ variable "replication_specs" { instance_size = string electable_node_count = number read_only_node_count = number + analytics_node_count = number + enable_disk_gb = bool + enable_compute = bool priority = number })) })) @@ -69,6 +116,9 @@ variable "replication_specs" { instance_size = "M10" electable_node_count = 3 read_only_node_count = 0 + analytics_node_count = 0 + enable_disk_gb = true + enable_compute = false priority = 7 } ] @@ -82,6 +132,9 @@ variable "replication_specs" { instance_size = "M10" electable_node_count = 2 read_only_node_count = 1 + analytics_node_count = 1 + enable_disk_gb = false + enable_compute = true priority = 7 }, { provider_name = "AWS" @@ -89,6 +142,9 @@ variable "replication_specs" { instance_size = "M10" electable_node_count = 1 read_only_node_count = 0 + analytics_node_count = 0 + enable_disk_gb = true + enable_compute = false priority = 6 } ] From 12b8e05262e88908218aa2e806385b49992168cb Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Fri, 22 Aug 2025 14:23:31 +0200 Subject: [PATCH 18/43] terraform fmt --- internal/convert/adv2v2.go | 5 ++-- ...eplication_specs_different_var_name.out.tf | 24 +++++++++---------- .../testdata/clu2adv/autoscaling.in.tf | 2 +- .../autoscaling_missing_attribute.in.tf | 2 +- .../free_cluster_missing_attribute.in.tf | 6 ++--- ...ication_specs_missing_regions_config.in.tf | 4 ++-- 6 files changed, 21 insertions(+), 22 deletions(-) diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index 3cdc010..ae68f9b 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -394,10 +394,9 @@ func convertDynamicRepSpecsWithoutNumShards(resourceb *hclwrite.Body, dSpec, dCo // Build the for expression as an array wrapped in flatten // Format: flatten([for spec in ... : [ { ... } ] ]) - forExpr := fmt.Sprintf("for %s in %s : [\n ", nSpec, hcl.GetAttrExpr(dSpec.forEach)) + forExpr := fmt.Sprintf("for %s in %s : ", nSpec, hcl.GetAttrExpr(dSpec.forEach)) innerTokens := hcl.TokensFromExpr(forExpr) - innerTokens = append(innerTokens, hcl.TokensObject(repSpecb)...) - innerTokens = append(innerTokens, hcl.TokensFromExpr("\n ]")...) + innerTokens = append(innerTokens, hcl.TokensArraySingle(repSpecb)...) // Apply flatten to the entire expression tokens := hcl.TokensFuncFlatten(innerTokens) diff --git a/internal/convert/testdata/adv2v2/dynamic_replication_specs_different_var_name.out.tf b/internal/convert/testdata/adv2v2/dynamic_replication_specs_different_var_name.out.tf index 485c541..65db111 100644 --- a/internal/convert/testdata/adv2v2/dynamic_replication_specs_different_var_name.out.tf +++ b/internal/convert/testdata/adv2v2/dynamic_replication_specs_different_var_name.out.tf @@ -30,19 +30,19 @@ resource "mongodbatlas_advanced_cluster" "different_var_names_no_zone_name_no_nu cluster_type = var.cluster_type replication_specs = flatten([ for spec in var.my_rep_specs : [ - { - region_configs = [ - for region in spec.region_configs : { - priority = region.prio - provider_name = region.provider_name - region_name = region.my_region_name - electable_specs = { - instance_size = region.instance_size - node_count = region.my_electable_node_count + { + region_configs = [ + for region in spec.region_configs : { + priority = region.prio + provider_name = region.provider_name + region_name = region.my_region_name + electable_specs = { + instance_size = region.instance_size + node_count = region.my_electable_node_count + } } - } - ] - } + ] + } ] ]) diff --git a/internal/convert/testdata/clu2adv/autoscaling.in.tf b/internal/convert/testdata/clu2adv/autoscaling.in.tf index 2936c41..e43bcd6 100644 --- a/internal/convert/testdata/clu2adv/autoscaling.in.tf +++ b/internal/convert/testdata/clu2adv/autoscaling.in.tf @@ -2,7 +2,7 @@ resource "mongodbatlas_cluster" "autoscaling" { project_id = var.project_id name = var.cluster_name disk_size_gb = 100 - num_shards = 1 + num_shards = 1 cluster_type = "REPLICASET" replication_specs { diff --git a/internal/convert/testdata/clu2adv/autoscaling_missing_attribute.in.tf b/internal/convert/testdata/clu2adv/autoscaling_missing_attribute.in.tf index f8dd8d8..123731b 100644 --- a/internal/convert/testdata/clu2adv/autoscaling_missing_attribute.in.tf +++ b/internal/convert/testdata/clu2adv/autoscaling_missing_attribute.in.tf @@ -2,7 +2,7 @@ resource "mongodbatlas_cluster" "autoscaling" { project_id = var.project_id name = var.cluster_name disk_size_gb = 100 - num_shards = 1 + num_shards = 1 cluster_type = "REPLICASET" replication_specs { diff --git a/internal/convert/testdata/clu2adv/free_cluster_missing_attribute.in.tf b/internal/convert/testdata/clu2adv/free_cluster_missing_attribute.in.tf index 2c3f37b..2f17b21 100644 --- a/internal/convert/testdata/clu2adv/free_cluster_missing_attribute.in.tf +++ b/internal/convert/testdata/clu2adv/free_cluster_missing_attribute.in.tf @@ -4,9 +4,9 @@ resource "resource1" "res1" { resource "mongodbatlas_cluster" "free_cluster" { # comment in the resource # comment in own line in the beginning - count = local.use_free_cluster ? 1 : 0 - project_id = var.project_id # inline comment kept - name = var.cluster_name + count = local.use_free_cluster ? 1 : 0 + project_id = var.project_id # inline comment kept + name = var.cluster_name # comment in own line in the middle is deleted provider_name = "TENANT" # inline comment for attribute moved is not kept provider_region_name = var.region diff --git a/internal/convert/testdata/clu2adv/replication_specs_missing_regions_config.in.tf b/internal/convert/testdata/clu2adv/replication_specs_missing_regions_config.in.tf index b62adf1..f1fb1b4 100644 --- a/internal/convert/testdata/clu2adv/replication_specs_missing_regions_config.in.tf +++ b/internal/convert/testdata/clu2adv/replication_specs_missing_regions_config.in.tf @@ -2,13 +2,13 @@ resource "mongodbatlas_cluster" "autoscaling" { project_id = var.project_id name = var.cluster_name disk_size_gb = 100 - num_shards = 1 + num_shards = 1 cluster_type = "REPLICASET" replication_specs { num_shards = 1 } - + //Provider Settings "block" provider_name = "AWS" provider_auto_scaling_compute_min_instance_size = "M10" From 1bdacc536b1b4585fbb055a3e5b99441c38b7e7f Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Fri, 22 Aug 2025 15:10:05 +0200 Subject: [PATCH 19/43] reduce duplication --- internal/convert/adv2v2.go | 134 ++++++++---------------------------- internal/convert/clu2adv.go | 8 +-- internal/convert/shared.go | 66 ++++++++++++++++++ 3 files changed, 100 insertions(+), 108 deletions(-) diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index ae68f9b..70c0957 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -171,8 +171,9 @@ func convertDynamicRepSpecs(resourceb *hclwrite.Body, dSpec dynamicBlock, diskSi } // Create the for expression for the flattened replication_specs - forExpr := fmt.Sprintf("for %s in %s : [\n for i in range(%s) : ", - nSpec, hcl.GetAttrExpr(dSpec.forEach), numShardsExpr) + outerFor := buildForExpression(nSpec, hcl.GetAttrExpr(dSpec.forEach)) + innerFor := buildForExpressionWithIndex("i", fmt.Sprintf("range(%s)", numShardsExpr)) + forExpr := fmt.Sprintf("%s [\n %s", outerFor, innerFor) tokens := hcl.TokensFromExpr(forExpr) tokens = append(tokens, hcl.TokensObject(dSpec.content.Body())...) tokens = append(tokens, hcl.TokensFromExpr("\n ]\n]")...) @@ -190,7 +191,7 @@ func convertDynamicRepSpecs(resourceb *hclwrite.Body, dSpec dynamicBlock, diskSi } // Create the for expression without num_shards - forExpr := fmt.Sprintf("for %s in %s :", nSpec, hcl.GetAttrExpr(dSpec.forEach)) + forExpr := buildForExpression(nSpec, hcl.GetAttrExpr(dSpec.forEach)) tokens := hcl.TokensFromExpr(forExpr) tokens = append(tokens, hcl.TokensObject(dSpec.content.Body())...) tokens = hcl.EncloseBracketsNewLines(tokens) @@ -208,16 +209,7 @@ func processRegionConfigBlocks(targetBody *hclwrite.Body, blocks []*hclwrite.Blo blockBody := blockFile.Body() // Copy all attributes in deterministic order - attrs := block.Body().Attributes() - var names []string - for name := range attrs { - names = append(names, name) - } - slices.Sort(names) - for _, name := range names { - attr := attrs[name] - blockBody.SetAttributeRaw(name, hcl.TokensFromExpr(hcl.GetAttrExpr(attr))) - } + copyAttributesSorted(blockBody, block.Body().Attributes()) // Add disk_size_gb to specs blocks if needed if diskSizeGB != nil && (blockType == nElectableSpecs || @@ -242,31 +234,12 @@ func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dC // Transform references in place for the dynamic config content transformDynamicBlockReferencesRecursive(dConfig.content.Body(), configBlockName, nRegion) // Also transform outer references (with deterministic ordering) - attrs := dConfig.content.Body().Attributes() - var attrNames []string - for name := range attrs { - attrNames = append(attrNames, name) - } - slices.Sort(attrNames) - for _, name := range attrNames { - attr := attrs[name] - expr := hcl.GetAttrExpr(attr) - expr = replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) - dConfig.content.Body().SetAttributeRaw(name, hcl.TokensFromExpr(expr)) + transform := func(expr string) string { + return replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) } + transformAttributesSorted(dConfig.content.Body(), dConfig.content.Body().Attributes(), transform) for _, block := range dConfig.content.Body().Blocks() { - blockAttrs := block.Body().Attributes() - var blockAttrNames []string - for name := range blockAttrs { - blockAttrNames = append(blockAttrNames, name) - } - slices.Sort(blockAttrNames) - for _, name := range blockAttrNames { - attr := blockAttrs[name] - expr := hcl.GetAttrExpr(attr) - expr = replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) - block.Body().SetAttributeRaw(name, hcl.TokensFromExpr(expr)) - } + transformAttributesSorted(block.Body(), block.Body().Attributes(), transform) } // Build the expression using HCL functions @@ -278,22 +251,13 @@ func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dC regionConfigBody := regionConfigFile.Body() // Copy all attributes in deterministic order - configAttrs := dConfig.content.Body().Attributes() - var configAttrNames []string - for name := range configAttrs { - configAttrNames = append(configAttrNames, name) - } - slices.Sort(configAttrNames) - for _, name := range configAttrNames { - attr := configAttrs[name] - regionConfigBody.SetAttributeRaw(name, hcl.TokensFromExpr(hcl.GetAttrExpr(attr))) - } + copyAttributesSorted(regionConfigBody, dConfig.content.Body().Attributes()) // Add all blocks generically as objects processRegionConfigBlocks(regionConfigBody, dConfig.content.Body().Blocks(), diskSizeGB) // Build the region_configs for expression - regionForExpr := fmt.Sprintf("for %s in %s :", nRegion, configForEach) + regionForExpr := buildForExpression(nRegion, configForEach) regionTokens := hcl.TokensFromExpr(regionForExpr) regionTokens = append(regionTokens, hcl.TokensObject(regionConfigBody)...) @@ -309,12 +273,12 @@ func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dC repSpecBody.SetAttributeRaw(nConfig, hcl.EncloseBracketsNewLines(regionTokens)) // Build the inner for expression with range - innerForExpr := fmt.Sprintf("for i in range(%s) :", numShardsExpr) + innerForExpr := buildForExpressionWithIndex("i", fmt.Sprintf("range(%s)", numShardsExpr)) innerTokens := hcl.TokensFromExpr(innerForExpr) innerTokens = append(innerTokens, hcl.TokensObject(repSpecBody)...) // Build the outer for expression - outerForExpr := fmt.Sprintf("for %s in %s : ", nSpec, hcl.GetAttrExpr(dSpec.forEach)) + outerForExpr := buildForExpression(nSpec, hcl.GetAttrExpr(dSpec.forEach)) outerTokens := hcl.TokensFromExpr(outerForExpr) outerTokens = append(outerTokens, hcl.EncloseBracketsNewLines(innerTokens)...) @@ -332,21 +296,14 @@ func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dC // Helper function to add attributes with transformation func addAttributesWithTransform(targetBody *hclwrite.Body, sourceAttrs map[string]*hclwrite.Attribute, configBlockName string) { - // Sort attribute names for deterministic output - var names []string - for name := range sourceAttrs { - names = append(names, name) - } - slices.Sort(names) - for _, name := range names { - attr := sourceAttrs[name] - expr := hcl.GetAttrExpr(attr) - // First replace nested dynamic block references - expr = replaceDynamicBlockReferences(expr, configBlockName, nRegion) - // Then replace outer dynamic block references - expr = replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) - targetBody.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) + // Apply transformations in order + transform1 := func(expr string) string { + return replaceDynamicBlockReferences(expr, configBlockName, nRegion) + } + transform2 := func(expr string) string { + return replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) } + transformAttributesSorted(targetBody, sourceAttrs, transform1, transform2) } func convertDynamicRepSpecsWithoutNumShards(resourceb *hclwrite.Body, dSpec, dConfig dynamicBlock, @@ -375,18 +332,14 @@ func convertDynamicRepSpecsWithoutNumShards(resourceb *hclwrite.Body, dSpec, dCo } // Process specs - fillSpecOpt(configb, nElectableSpecs, diskSizeGB) - fillSpecOpt(configb, nReadOnlySpecs, diskSizeGB) - fillSpecOpt(configb, nAnalyticsSpecs, diskSizeGB) - fillSpecOpt(configb, nAutoScaling, nil) - fillSpecOpt(configb, nAnalyticsAutoScaling, nil) + processAllSpecs(configb, diskSizeGB) // Build the nested for expression for region_configs // Use standardized property name (region_configs) instead of the actual for_each collection configForEach := fmt.Sprintf("%s.%s", nSpec, nConfig) // Build the region_configs for expression - regionForExpr := fmt.Sprintf("for %s in %s :", nRegion, configForEach) + regionForExpr := buildForExpression(nRegion, configForEach) regionTokens := hcl.TokensFromExpr(regionForExpr) regionTokens = append(regionTokens, hcl.TokensObject(configb)...) @@ -394,7 +347,7 @@ func convertDynamicRepSpecsWithoutNumShards(resourceb *hclwrite.Body, dSpec, dCo // Build the for expression as an array wrapped in flatten // Format: flatten([for spec in ... : [ { ... } ] ]) - forExpr := fmt.Sprintf("for %s in %s : ", nSpec, hcl.GetAttrExpr(dSpec.forEach)) + forExpr := buildForExpression(nSpec, hcl.GetAttrExpr(dSpec.forEach)) innerTokens := hcl.TokensFromExpr(forExpr) innerTokens = append(innerTokens, hcl.TokensArraySingle(repSpecb)...) @@ -432,11 +385,7 @@ func convertConfig(repSpecs *hclwrite.Body, diskSizeGB hclwrite.Tokens) error { } repSpecs.RemoveBlock(block) blockb := block.Body() - fillSpecOpt(blockb, nElectableSpecs, diskSizeGB) - fillSpecOpt(blockb, nReadOnlySpecs, diskSizeGB) - fillSpecOpt(blockb, nAnalyticsSpecs, diskSizeGB) - fillSpecOpt(blockb, nAutoScaling, nil) // auto_scaling doesn't need disk_size_gb - fillSpecOpt(blockb, nAnalyticsAutoScaling, nil) // analytics_auto_scaling doesn't need disk_size_gb + processAllSpecs(blockb, diskSizeGB) configs = append(configs, blockb) } if len(configs) == 0 { @@ -454,14 +403,10 @@ func convertDynamicConfig(repSpecs *hclwrite.Body, dConfig dynamicBlock, diskSiz transformDynamicBlockReferencesRecursive(dConfig.content.Body(), blockName, nRegion) // Process specs - fillSpecOpt(dConfig.content.Body(), nElectableSpecs, diskSizeGB) - fillSpecOpt(dConfig.content.Body(), nReadOnlySpecs, diskSizeGB) - fillSpecOpt(dConfig.content.Body(), nAnalyticsSpecs, diskSizeGB) - fillSpecOpt(dConfig.content.Body(), nAutoScaling, nil) - fillSpecOpt(dConfig.content.Body(), nAnalyticsAutoScaling, nil) + processAllSpecs(dConfig.content.Body(), diskSizeGB) // Build the for expression - forExpr := fmt.Sprintf("for %s in %s :", nRegion, hcl.GetAttrExpr(dConfig.forEach)) + forExpr := buildForExpression(nRegion, hcl.GetAttrExpr(dConfig.forEach)) tokens := hcl.TokensFromExpr(forExpr) tokens = append(tokens, hcl.TokensObject(dConfig.content.Body())...) tokens = hcl.EncloseBracketsNewLines(tokens) @@ -473,36 +418,17 @@ func convertDynamicConfig(repSpecs *hclwrite.Body, dConfig dynamicBlock, diskSiz func transformDynamicBlockReferencesRecursive(body *hclwrite.Body, blockName, varName string) { // Transform attributes in deterministic order - attrs := body.Attributes() - var names []string - for name := range attrs { - names = append(names, name) - } - slices.Sort(names) - for _, name := range names { - attr := attrs[name] - expr := replaceDynamicBlockReferences(hcl.GetAttrExpr(attr), blockName, varName) - body.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) + transform := func(expr string) string { + return replaceDynamicBlockReferences(expr, blockName, varName) } + transformAttributesSorted(body, body.Attributes(), transform) + // Transform nested blocks for _, block := range body.Blocks() { transformDynamicBlockReferencesRecursive(block.Body(), blockName, varName) } } -func fillSpecOpt(resourceb *hclwrite.Body, name string, diskSizeGBTokens hclwrite.Tokens) { - block := resourceb.FirstMatchingBlock(name, nil) - if block == nil { - return - } - if diskSizeGBTokens != nil { - blockb := block.Body() - blockb.RemoveAttribute(nDiskSizeGB) - blockb.SetAttributeRaw(nDiskSizeGB, diskSizeGBTokens) - } - fillBlockOpt(resourceb, name) -} - // hasExpectedBlocksAsAttributes checks if any of the expected block names // exist as attributes in the resource body. In that case conversion is not done // as advanced cluster is not in a valid SDKv2 configuration. diff --git a/internal/convert/clu2adv.go b/internal/convert/clu2adv.go index cebdd75..d4abe3e 100644 --- a/internal/convert/clu2adv.go +++ b/internal/convert/clu2adv.go @@ -371,7 +371,7 @@ func fillReplicationSpecsWithDynamicBlock(resourceb *hclwrite.Body, root attrVal if err != nil { return dynamicBlock{}, err } - forSpec := hcl.TokensFromExpr(fmt.Sprintf("for %s in %s : ", nSpec, hcl.GetAttrExpr(dSpec.forEach))) + forSpec := hcl.TokensFromExpr(buildForExpression(nSpec, hcl.GetAttrExpr(dSpec.forEach))) forSpec = append(forSpec, dConfig.tokens...) tokens := hcl.TokensFuncFlatten(forSpec) dSpec.tokens = tokens @@ -397,7 +397,7 @@ func fillWithDynamicRegionConfigs(specbSrc *hclwrite.Body, root attrVals, change if err != nil { return dynamicBlock{}, err } - priorityForStr := fmt.Sprintf("for %s in range(%d, %d, -1) : ", nPriority, valMaxPriority, valMinPriority) + priorityForStr := buildForExpression(nPriority, fmt.Sprintf("range(%d, %d, -1)", valMaxPriority, valMinPriority)) priorityFor := hcl.TokensComment(commentPriorityFor) priorityFor = append(priorityFor, hcl.TokensFromExpr(priorityForStr)...) priorityFor = append(priorityFor, regionFor...) @@ -407,7 +407,7 @@ func fillWithDynamicRegionConfigs(specbSrc *hclwrite.Body, root attrVals, change if shards == nil { return dynamicBlock{}, fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) } - tokens := hcl.TokensFromExpr(fmt.Sprintf("for i in range(%s) :", hcl.GetAttrExpr(shards))) + tokens := hcl.TokensFromExpr(buildForExpressionWithIndex("i", fmt.Sprintf("range(%s)", hcl.GetAttrExpr(shards)))) tokens = append(tokens, hcl.EncloseBraces(repSpec.BuildTokens(nil), true)...) d.tokens = hcl.EncloseBracketsNewLines(tokens) return d, nil @@ -563,7 +563,7 @@ func getDynamicBlockRegionArray(forEach string, configSrc *hclwrite.Block, root if err != nil { return nil, err } - tokens := hcl.TokensFromExpr(fmt.Sprintf("for %s in %s :", nRegion, forEach)) + tokens := hcl.TokensFromExpr(buildForExpression(nRegion, forEach)) tokens = append(tokens, hcl.EncloseBraces(region.BuildTokens(nil), true)...) tokens = append(tokens, hcl.TokensFromExpr(fmt.Sprintf("if %s == %s", nPriority, priorityStr))...) return hcl.EncloseBracketsNewLines(tokens), nil diff --git a/internal/convert/shared.go b/internal/convert/shared.go index 80d0028..8a6954b 100644 --- a/internal/convert/shared.go +++ b/internal/convert/shared.go @@ -2,6 +2,7 @@ package convert import ( "fmt" + "slices" "strings" "github.com/hashicorp/hcl/v2/hclwrite" @@ -129,3 +130,68 @@ func fillAdvConfigOpt(resourceb *hclwrite.Body) { fillBlockOpt(resourceb, nAdvConfig) } + +// copyAttributesSorted copies attributes from source to target in sorted order for deterministic output +func copyAttributesSorted(targetBody *hclwrite.Body, sourceAttrs map[string]*hclwrite.Attribute) { + var names []string + for name := range sourceAttrs { + names = append(names, name) + } + slices.Sort(names) + for _, name := range names { + attr := sourceAttrs[name] + targetBody.SetAttributeRaw(name, hcl.TokensFromExpr(hcl.GetAttrExpr(attr))) + } +} + +// transformAttributesSorted transforms and copies attributes in sorted order +func transformAttributesSorted(targetBody *hclwrite.Body, sourceAttrs map[string]*hclwrite.Attribute, + transforms ...func(string) string) { + var names []string + for name := range sourceAttrs { + names = append(names, name) + } + slices.Sort(names) + for _, name := range names { + attr := sourceAttrs[name] + expr := hcl.GetAttrExpr(attr) + // Apply all transformations + for _, transform := range transforms { + expr = transform(expr) + } + targetBody.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) + } +} + +// processAllSpecs processes all spec blocks (electable, read_only, analytics) and auto_scaling blocks +func processAllSpecs(body *hclwrite.Body, diskSizeGB hclwrite.Tokens) { + fillSpecOpt(body, nElectableSpecs, diskSizeGB) + fillSpecOpt(body, nReadOnlySpecs, diskSizeGB) + fillSpecOpt(body, nAnalyticsSpecs, diskSizeGB) + fillSpecOpt(body, nAutoScaling, nil) + fillSpecOpt(body, nAnalyticsAutoScaling, nil) +} + +// fillSpecOpt converts a spec block to an attribute with object value and optionally adds disk_size_gb +func fillSpecOpt(resourceb *hclwrite.Body, name string, diskSizeGBTokens hclwrite.Tokens) { + block := resourceb.FirstMatchingBlock(name, nil) + if block == nil { + return + } + if diskSizeGBTokens != nil { + blockb := block.Body() + blockb.RemoveAttribute(nDiskSizeGB) + blockb.SetAttributeRaw(nDiskSizeGB, diskSizeGBTokens) + } + fillBlockOpt(resourceb, name) +} + +// buildForExpression builds a for expression with the given variable and collection +func buildForExpression(varName, collection string) string { + return fmt.Sprintf("for %s in %s : ", varName, collection) +} + +// buildForExpressionWithIndex builds a for expression with an index variable +func buildForExpressionWithIndex(indexVar, collection string) string { + return fmt.Sprintf("for %s in %s : ", indexVar, collection) +} From 472bac3de64b406ebf756cce3c3a3ec7e43c76db Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Fri, 22 Aug 2025 16:06:56 +0200 Subject: [PATCH 20/43] refactor buildForExpr --- internal/convert/adv2v2.go | 24 ++++++++++++------------ internal/convert/clu2adv.go | 10 +++++----- internal/convert/shared.go | 11 +++-------- 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index 70c0957..5a03225 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -171,9 +171,9 @@ func convertDynamicRepSpecs(resourceb *hclwrite.Body, dSpec dynamicBlock, diskSi } // Create the for expression for the flattened replication_specs - outerFor := buildForExpression(nSpec, hcl.GetAttrExpr(dSpec.forEach)) - innerFor := buildForExpressionWithIndex("i", fmt.Sprintf("range(%s)", numShardsExpr)) - forExpr := fmt.Sprintf("%s [\n %s", outerFor, innerFor) + outerFor := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach)) + innerFor := buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr)) + forExpr := fmt.Sprintf("%s [\n %s ", outerFor, innerFor) tokens := hcl.TokensFromExpr(forExpr) tokens = append(tokens, hcl.TokensObject(dSpec.content.Body())...) tokens = append(tokens, hcl.TokensFromExpr("\n ]\n]")...) @@ -191,7 +191,7 @@ func convertDynamicRepSpecs(resourceb *hclwrite.Body, dSpec dynamicBlock, diskSi } // Create the for expression without num_shards - forExpr := buildForExpression(nSpec, hcl.GetAttrExpr(dSpec.forEach)) + forExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach)) tokens := hcl.TokensFromExpr(forExpr) tokens = append(tokens, hcl.TokensObject(dSpec.content.Body())...) tokens = hcl.EncloseBracketsNewLines(tokens) @@ -257,7 +257,7 @@ func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dC processRegionConfigBlocks(regionConfigBody, dConfig.content.Body().Blocks(), diskSizeGB) // Build the region_configs for expression - regionForExpr := buildForExpression(nRegion, configForEach) + regionForExpr := buildForExpr(nRegion, configForEach) regionTokens := hcl.TokensFromExpr(regionForExpr) regionTokens = append(regionTokens, hcl.TokensObject(regionConfigBody)...) @@ -273,13 +273,13 @@ func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dC repSpecBody.SetAttributeRaw(nConfig, hcl.EncloseBracketsNewLines(regionTokens)) // Build the inner for expression with range - innerForExpr := buildForExpressionWithIndex("i", fmt.Sprintf("range(%s)", numShardsExpr)) + innerForExpr := buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr)) innerTokens := hcl.TokensFromExpr(innerForExpr) innerTokens = append(innerTokens, hcl.TokensObject(repSpecBody)...) // Build the outer for expression - outerForExpr := buildForExpression(nSpec, hcl.GetAttrExpr(dSpec.forEach)) - outerTokens := hcl.TokensFromExpr(outerForExpr) + outerForExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach)) + outerTokens := hcl.TokensFromExpr(fmt.Sprintf("%s ", outerForExpr)) outerTokens = append(outerTokens, hcl.EncloseBracketsNewLines(innerTokens)...) // Apply flatten to the entire expression @@ -339,7 +339,7 @@ func convertDynamicRepSpecsWithoutNumShards(resourceb *hclwrite.Body, dSpec, dCo configForEach := fmt.Sprintf("%s.%s", nSpec, nConfig) // Build the region_configs for expression - regionForExpr := buildForExpression(nRegion, configForEach) + regionForExpr := buildForExpr(nRegion, configForEach) regionTokens := hcl.TokensFromExpr(regionForExpr) regionTokens = append(regionTokens, hcl.TokensObject(configb)...) @@ -347,8 +347,8 @@ func convertDynamicRepSpecsWithoutNumShards(resourceb *hclwrite.Body, dSpec, dCo // Build the for expression as an array wrapped in flatten // Format: flatten([for spec in ... : [ { ... } ] ]) - forExpr := buildForExpression(nSpec, hcl.GetAttrExpr(dSpec.forEach)) - innerTokens := hcl.TokensFromExpr(forExpr) + forExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach)) + innerTokens := hcl.TokensFromExpr(fmt.Sprintf("%s ", forExpr)) innerTokens = append(innerTokens, hcl.TokensArraySingle(repSpecb)...) // Apply flatten to the entire expression @@ -406,7 +406,7 @@ func convertDynamicConfig(repSpecs *hclwrite.Body, dConfig dynamicBlock, diskSiz processAllSpecs(dConfig.content.Body(), diskSizeGB) // Build the for expression - forExpr := buildForExpression(nRegion, hcl.GetAttrExpr(dConfig.forEach)) + forExpr := buildForExpr(nRegion, hcl.GetAttrExpr(dConfig.forEach)) tokens := hcl.TokensFromExpr(forExpr) tokens = append(tokens, hcl.TokensObject(dConfig.content.Body())...) tokens = hcl.EncloseBracketsNewLines(tokens) diff --git a/internal/convert/clu2adv.go b/internal/convert/clu2adv.go index d4abe3e..d673e61 100644 --- a/internal/convert/clu2adv.go +++ b/internal/convert/clu2adv.go @@ -371,7 +371,7 @@ func fillReplicationSpecsWithDynamicBlock(resourceb *hclwrite.Body, root attrVal if err != nil { return dynamicBlock{}, err } - forSpec := hcl.TokensFromExpr(buildForExpression(nSpec, hcl.GetAttrExpr(dSpec.forEach))) + forSpec := hcl.TokensFromExpr(fmt.Sprintf("%s ", buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach)))) forSpec = append(forSpec, dConfig.tokens...) tokens := hcl.TokensFuncFlatten(forSpec) dSpec.tokens = tokens @@ -397,9 +397,9 @@ func fillWithDynamicRegionConfigs(specbSrc *hclwrite.Body, root attrVals, change if err != nil { return dynamicBlock{}, err } - priorityForStr := buildForExpression(nPriority, fmt.Sprintf("range(%d, %d, -1)", valMaxPriority, valMinPriority)) + priorityForStr := buildForExpr(nPriority, fmt.Sprintf("range(%d, %d, -1)", valMaxPriority, valMinPriority)) priorityFor := hcl.TokensComment(commentPriorityFor) - priorityFor = append(priorityFor, hcl.TokensFromExpr(priorityForStr)...) + priorityFor = append(priorityFor, hcl.TokensFromExpr(fmt.Sprintf("%s ", priorityForStr))...) priorityFor = append(priorityFor, regionFor...) repSpecb.SetAttributeRaw(nConfig, hcl.TokensFuncFlatten(priorityFor)) @@ -407,7 +407,7 @@ func fillWithDynamicRegionConfigs(specbSrc *hclwrite.Body, root attrVals, change if shards == nil { return dynamicBlock{}, fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) } - tokens := hcl.TokensFromExpr(buildForExpressionWithIndex("i", fmt.Sprintf("range(%s)", hcl.GetAttrExpr(shards)))) + tokens := hcl.TokensFromExpr(buildForExpr("i", fmt.Sprintf("range(%s)", hcl.GetAttrExpr(shards)))) tokens = append(tokens, hcl.EncloseBraces(repSpec.BuildTokens(nil), true)...) d.tokens = hcl.EncloseBracketsNewLines(tokens) return d, nil @@ -563,7 +563,7 @@ func getDynamicBlockRegionArray(forEach string, configSrc *hclwrite.Block, root if err != nil { return nil, err } - tokens := hcl.TokensFromExpr(buildForExpression(nRegion, forEach)) + tokens := hcl.TokensFromExpr(buildForExpr(nRegion, forEach)) tokens = append(tokens, hcl.EncloseBraces(region.BuildTokens(nil), true)...) tokens = append(tokens, hcl.TokensFromExpr(fmt.Sprintf("if %s == %s", nPriority, priorityStr))...) return hcl.EncloseBracketsNewLines(tokens), nil diff --git a/internal/convert/shared.go b/internal/convert/shared.go index 8a6954b..bda0601 100644 --- a/internal/convert/shared.go +++ b/internal/convert/shared.go @@ -186,12 +186,7 @@ func fillSpecOpt(resourceb *hclwrite.Body, name string, diskSizeGBTokens hclwrit fillBlockOpt(resourceb, name) } -// buildForExpression builds a for expression with the given variable and collection -func buildForExpression(varName, collection string) string { - return fmt.Sprintf("for %s in %s : ", varName, collection) -} - -// buildForExpressionWithIndex builds a for expression with an index variable -func buildForExpressionWithIndex(indexVar, collection string) string { - return fmt.Sprintf("for %s in %s : ", indexVar, collection) +// buildForExpr builds a for expression with the given variable and collection +func buildForExpr(varName, collection string) string { + return fmt.Sprintf("for %s in %s :", varName, collection) } From a425f539c30cde9ca6268fd494100566fdf920ff Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Fri, 22 Aug 2025 16:11:34 +0200 Subject: [PATCH 21/43] simplify use of TokensFuncConcat --- internal/convert/adv2v2.go | 8 +------- internal/convert/clu2adv.go | 8 +------- internal/hcl/hcl.go | 3 +++ 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index 5a03225..ece1b1e 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -103,13 +103,7 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error } concatParts = append(concatParts, tokens) } - - // Use concat to combine all parts - if len(concatParts) > 1 { - resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensFuncConcat(concatParts...)) - } else { - resourceb.SetAttributeRaw(nRepSpecs, concatParts[0]) - } + resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensFuncConcat(concatParts...)) } else { // All num_shards are numeric or missing, use simple array var repSpecs []*hclwrite.Body diff --git a/internal/convert/clu2adv.go b/internal/convert/clu2adv.go index d673e61..43e0bb7 100644 --- a/internal/convert/clu2adv.go +++ b/internal/convert/clu2adv.go @@ -237,13 +237,7 @@ func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error { } concatParts = append(concatParts, tokens) } - - // Use concat to combine all parts - if len(concatParts) > 1 { - resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensFuncConcat(concatParts...)) - } else { - resourceb.SetAttributeRaw(nRepSpecs, concatParts[0]) - } + resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensFuncConcat(concatParts...)) } else { // All num_shards are numeric, use simple array var specbs []*hclwrite.Body diff --git a/internal/hcl/hcl.go b/internal/hcl/hcl.go index dbb818f..23fcc2d 100644 --- a/internal/hcl/hcl.go +++ b/internal/hcl/hcl.go @@ -121,6 +121,9 @@ func TokensFuncMerge(tokens ...hclwrite.Tokens) hclwrite.Tokens { // TokensFuncConcat creates the tokens for the HCL concat function. func TokensFuncConcat(tokens ...hclwrite.Tokens) hclwrite.Tokens { params := EncloseNewLines(joinTokens(tokens...)) + if len(tokens) == 1 { + return tokens[0] // no need to concat if there's only one element + } ret := TokensFromExpr("concat") return append(ret, EncloseParens(params)...) } From 4d843b9202cd539e6c3e5143e430ac6262ff49a8 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Fri, 22 Aug 2025 16:21:57 +0200 Subject: [PATCH 22/43] collectBlocks --- internal/convert/adv2v2.go | 30 ++---------------------------- internal/convert/clu2adv.go | 10 +--------- internal/convert/shared.go | 29 +++++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 37 deletions(-) diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index ece1b1e..8dd03e2 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -68,15 +68,7 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error } // Collect all replication_specs blocks first - var repSpecBlocks []*hclwrite.Block - for { - block := resourceb.FirstMatchingBlock(nRepSpecs, nil) - if block == nil { - break - } - resourceb.RemoveBlock(block) - repSpecBlocks = append(repSpecBlocks, block) - } + repSpecBlocks := collectBlocks(resourceb, nRepSpecs) if len(repSpecBlocks) == 0 { return fmt.Errorf("must have at least one replication_specs") @@ -372,12 +364,7 @@ func convertConfig(repSpecs *hclwrite.Body, diskSizeGB hclwrite.Tokens) error { } var configs []*hclwrite.Body - for { - block := repSpecs.FirstMatchingBlock(nConfig, nil) - if block == nil { - break - } - repSpecs.RemoveBlock(block) + for _, block := range collectBlocks(repSpecs, nConfig) { blockb := block.Body() processAllSpecs(blockb, diskSizeGB) configs = append(configs, blockb) @@ -410,19 +397,6 @@ func convertDynamicConfig(repSpecs *hclwrite.Body, dConfig dynamicBlock, diskSiz return nil } -func transformDynamicBlockReferencesRecursive(body *hclwrite.Body, blockName, varName string) { - // Transform attributes in deterministic order - transform := func(expr string) string { - return replaceDynamicBlockReferences(expr, blockName, varName) - } - transformAttributesSorted(body, body.Attributes(), transform) - - // Transform nested blocks - for _, block := range body.Blocks() { - transformDynamicBlockReferencesRecursive(block.Body(), blockName, varName) - } -} - // hasExpectedBlocksAsAttributes checks if any of the expected block names // exist as attributes in the resource body. In that case conversion is not done // as advanced cluster is not in a valid SDKv2 configuration. diff --git a/internal/convert/clu2adv.go b/internal/convert/clu2adv.go index 43e0bb7..6798610 100644 --- a/internal/convert/clu2adv.go +++ b/internal/convert/clu2adv.go @@ -183,15 +183,7 @@ func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error { } // Collect all replication_specs blocks first - var repSpecBlocks []*hclwrite.Block - for { - block := resourceb.FirstMatchingBlock(nRepSpecs, nil) - if block == nil { - break - } - resourceb.RemoveBlock(block) - repSpecBlocks = append(repSpecBlocks, block) - } + repSpecBlocks := collectBlocks(resourceb, nRepSpecs) if len(repSpecBlocks) == 0 { return fmt.Errorf("%s: no replication_specs found", errRepSpecs) diff --git a/internal/convert/shared.go b/internal/convert/shared.go index bda0601..a3293de 100644 --- a/internal/convert/shared.go +++ b/internal/convert/shared.go @@ -106,6 +106,35 @@ func transformDynamicBlockReferences(configSrcb *hclwrite.Body, blockName, varNa } } +// transformDynamicBlockReferencesRecursive transforms attributes and nested blocks recursively +// replacing references from dynamic block format, e.g. regions_config.value.* to region.* +func transformDynamicBlockReferencesRecursive(body *hclwrite.Body, blockName, varName string) { + // Transform attributes in deterministic order + transform := func(expr string) string { + return replaceDynamicBlockReferences(expr, blockName, varName) + } + transformAttributesSorted(body, body.Attributes(), transform) + + // Transform nested blocks + for _, block := range body.Blocks() { + transformDynamicBlockReferencesRecursive(block.Body(), blockName, varName) + } +} + +// collectBlocks removes and returns all blocks of the given name from body in order of appearance. +func collectBlocks(body *hclwrite.Body, name string) []*hclwrite.Block { + var blocks []*hclwrite.Block + for { + block := body.FirstMatchingBlock(name, nil) + if block == nil { + break + } + body.RemoveBlock(block) + blocks = append(blocks, block) + } + return blocks +} + // fillBlockOpt converts a block to an attribute with object value func fillBlockOpt(resourceb *hclwrite.Body, name string) { block := resourceb.FirstMatchingBlock(name, nil) From feeff66b1039a0f10ae5414080c8200357c8677b Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Fri, 22 Aug 2025 18:41:26 +0200 Subject: [PATCH 23/43] some refactors --- internal/convert/adv2v2.go | 110 +----------------------------------- internal/convert/clu2adv.go | 20 +------ internal/convert/shared.go | 41 ++++++-------- 3 files changed, 21 insertions(+), 150 deletions(-) diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index 8dd03e2..659c65c 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -11,7 +11,6 @@ import ( // AdvancedClusterToV2 transforms all mongodbatlas_advanced_cluster resource definitions in a // Terraform configuration file from SDKv2 schema to TPF (Terraform Plugin Framework) schema. // All other resources and data sources are left untouched. -// TODO: Not implemented yet. func AdvancedClusterToV2(config []byte) ([]byte, error) { parser, err := hcl.GetParser(config) if err != nil { @@ -58,7 +57,6 @@ func updateResource(resource *hclwrite.Block) (bool, error) { } func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error { - // Handle dynamic blocks for replication_specs dSpec, err := getDynamicBlock(resourceb, nRepSpecs) if err != nil { return err @@ -67,17 +65,12 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error return convertDynamicRepSpecs(resourceb, dSpec, diskSizeGB) } - // Collect all replication_specs blocks first repSpecBlocks := collectBlocks(resourceb, nRepSpecs) - if len(repSpecBlocks) == 0 { return fmt.Errorf("must have at least one replication_specs") } - // Check if any replication_specs has a variable num_shards - hasVariableNumShards := HasVariableNumShards(repSpecBlocks) - - if hasVariableNumShards { + if hasVariableNumShards(repSpecBlocks) { var concatParts []hclwrite.Tokens for _, block := range repSpecBlocks { @@ -89,7 +82,7 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error return err } - tokens, err := ProcessNumShards(numShardsAttr, blockb) + tokens, err := processNumShards(numShardsAttr, blockb) if err != nil { return err } @@ -125,10 +118,7 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error } func convertDynamicRepSpecs(resourceb *hclwrite.Body, dSpec dynamicBlock, diskSizeGB hclwrite.Tokens) error { - // Transform references from replication_specs.value.* to spec.* transformDynamicBlockReferences(dSpec.content.Body(), nRepSpecs, nSpec) - - // Check for dynamic region_configs within this dynamic replication_specs dConfig, err := getDynamicBlock(dSpec.content.Body(), nConfig) if err != nil { return err @@ -139,87 +129,60 @@ func convertDynamicRepSpecs(resourceb *hclwrite.Body, dSpec dynamicBlock, diskSi return err } } - if dConfig.IsPresent() { - // Handle nested dynamic block for region_configs return convertDynamicRepSpecsWithDynamicConfig(resourceb, dSpec, dConfig, diskSizeGB) } - - // Get num_shards from the dynamic block content numShardsAttr := dSpec.content.Body().GetAttribute(nNumShards) if numShardsAttr != nil { numShardsExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec) dSpec.content.Body().RemoveAttribute(nNumShards) - - // Convert region_configs inside the dynamic block if err := convertConfig(dSpec.content.Body(), diskSizeGB); err != nil { return err } - - // Create the for expression for the flattened replication_specs outerFor := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach)) innerFor := buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr)) forExpr := fmt.Sprintf("%s [\n %s ", outerFor, innerFor) tokens := hcl.TokensFromExpr(forExpr) tokens = append(tokens, hcl.TokensObject(dSpec.content.Body())...) tokens = append(tokens, hcl.TokensFromExpr("\n ]\n]")...) - resourceb.RemoveBlock(dSpec.block) resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensFuncFlatten(tokens)) return nil } - // No num_shards, default to 1 dSpec.content.Body().RemoveAttribute(nNumShards) - - // Convert region_configs inside the dynamic block if err := convertConfig(dSpec.content.Body(), diskSizeGB); err != nil { return err } - - // Create the for expression without num_shards forExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach)) tokens := hcl.TokensFromExpr(forExpr) tokens = append(tokens, hcl.TokensObject(dSpec.content.Body())...) tokens = hcl.EncloseBracketsNewLines(tokens) - resourceb.RemoveBlock(dSpec.block) resourceb.SetAttributeRaw(nRepSpecs, tokens) return nil } -// Helper function to process blocks for region configs func processRegionConfigBlocks(targetBody *hclwrite.Body, blocks []*hclwrite.Block, diskSizeGB hclwrite.Tokens) { for _, block := range blocks { blockType := block.Type() blockFile := hclwrite.NewEmptyFile() blockBody := blockFile.Body() - - // Copy all attributes in deterministic order copyAttributesSorted(blockBody, block.Body().Attributes()) - - // Add disk_size_gb to specs blocks if needed if diskSizeGB != nil && (blockType == nElectableSpecs || blockType == nReadOnlySpecs || blockType == nAnalyticsSpecs) { blockBody.SetAttributeRaw(nDiskSizeGB, diskSizeGB) } - targetBody.SetAttributeRaw(blockType, hcl.TokensObject(blockBody)) } } func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dConfig dynamicBlock, diskSizeGB hclwrite.Tokens) error { - // Get the block name from the dynamic block configBlockName := getResourceName(dConfig.block) - - // Get num_shards expression numShardsAttr := dSpec.content.Body().GetAttribute(nNumShards) if numShardsAttr != nil { numShardsExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec) - - // Transform references in place for the dynamic config content transformDynamicBlockReferencesRecursive(dConfig.content.Body(), configBlockName, nRegion) - // Also transform outer references (with deterministic ordering) transform := func(expr string) string { return replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) } @@ -227,62 +190,37 @@ func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dC for _, block := range dConfig.content.Body().Blocks() { transformAttributesSorted(block.Body(), block.Body().Attributes(), transform) } - - // Build the expression using HCL functions - // Use standardized property name (region_configs) instead of the actual for_each collection configForEach := fmt.Sprintf("%s.%s", nSpec, nConfig) - - // Create the inner region_configs body regionConfigFile := hclwrite.NewEmptyFile() regionConfigBody := regionConfigFile.Body() - - // Copy all attributes in deterministic order copyAttributesSorted(regionConfigBody, dConfig.content.Body().Attributes()) - - // Add all blocks generically as objects processRegionConfigBlocks(regionConfigBody, dConfig.content.Body().Blocks(), diskSizeGB) - - // Build the region_configs for expression regionForExpr := buildForExpr(nRegion, configForEach) regionTokens := hcl.TokensFromExpr(regionForExpr) regionTokens = append(regionTokens, hcl.TokensObject(regionConfigBody)...) - - // Create the replication spec body repSpecFile := hclwrite.NewEmptyFile() repSpecBody := repSpecFile.Body() - if zoneNameAttr := dSpec.content.Body().GetAttribute(nZoneName); zoneNameAttr != nil { zoneNameExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(zoneNameAttr), nRepSpecs, nSpec) repSpecBody.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(zoneNameExpr)) } - repSpecBody.SetAttributeRaw(nConfig, hcl.EncloseBracketsNewLines(regionTokens)) - - // Build the inner for expression with range innerForExpr := buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr)) innerTokens := hcl.TokensFromExpr(innerForExpr) innerTokens = append(innerTokens, hcl.TokensObject(repSpecBody)...) - - // Build the outer for expression outerForExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach)) outerTokens := hcl.TokensFromExpr(fmt.Sprintf("%s ", outerForExpr)) outerTokens = append(outerTokens, hcl.EncloseBracketsNewLines(innerTokens)...) - - // Apply flatten to the entire expression tokens := hcl.TokensFuncFlatten(outerTokens) - resourceb.RemoveBlock(dSpec.block) resourceb.SetAttributeRaw(nRepSpecs, tokens) return nil } - // No num_shards, handle like without nested shards return convertDynamicRepSpecsWithoutNumShards(resourceb, dSpec, dConfig, diskSizeGB, configBlockName) } -// Helper function to add attributes with transformation func addAttributesWithTransform(targetBody *hclwrite.Body, sourceAttrs map[string]*hclwrite.Attribute, configBlockName string) { - // Apply transformations in order transform1 := func(expr string) string { return replaceDynamicBlockReferences(expr, configBlockName, nRegion) } @@ -294,59 +232,36 @@ func addAttributesWithTransform(targetBody *hclwrite.Body, sourceAttrs map[strin func convertDynamicRepSpecsWithoutNumShards(resourceb *hclwrite.Body, dSpec, dConfig dynamicBlock, diskSizeGB hclwrite.Tokens, configBlockName string) error { - // Get zone_name if present repSpecFile := hclwrite.NewEmptyFile() repSpecb := repSpecFile.Body() - if zoneNameAttr := dSpec.content.Body().GetAttribute(nZoneName); zoneNameAttr != nil { zoneNameExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(zoneNameAttr), nRepSpecs, nSpec) repSpecb.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(zoneNameExpr)) } - - // Create config content with transformed references configFile := hclwrite.NewEmptyFile() configb := configFile.Body() - - // Copy and transform attributes addAttributesWithTransform(configb, dConfig.content.Body().Attributes(), configBlockName) - - // Process blocks and transform their references for _, block := range dConfig.content.Body().Blocks() { newBlock := configb.AppendNewBlock(block.Type(), block.Labels()) newBlockb := newBlock.Body() addAttributesWithTransform(newBlockb, block.Body().Attributes(), configBlockName) } - - // Process specs processAllSpecs(configb, diskSizeGB) - - // Build the nested for expression for region_configs - // Use standardized property name (region_configs) instead of the actual for_each collection configForEach := fmt.Sprintf("%s.%s", nSpec, nConfig) - - // Build the region_configs for expression regionForExpr := buildForExpr(nRegion, configForEach) regionTokens := hcl.TokensFromExpr(regionForExpr) regionTokens = append(regionTokens, hcl.TokensObject(configb)...) - repSpecb.SetAttributeRaw(nConfig, hcl.EncloseBracketsNewLines(regionTokens)) - - // Build the for expression as an array wrapped in flatten - // Format: flatten([for spec in ... : [ { ... } ] ]) forExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach)) innerTokens := hcl.TokensFromExpr(fmt.Sprintf("%s ", forExpr)) innerTokens = append(innerTokens, hcl.TokensArraySingle(repSpecb)...) - - // Apply flatten to the entire expression tokens := hcl.TokensFuncFlatten(innerTokens) - resourceb.RemoveBlock(dSpec.block) resourceb.SetAttributeRaw(nRepSpecs, tokens) return nil } func convertConfig(repSpecs *hclwrite.Body, diskSizeGB hclwrite.Tokens) error { - // Check for dynamic region_configs block (can be either "region_configs" or "regions_config") dConfig, err := getDynamicBlock(repSpecs, nConfig) if err != nil { return err @@ -357,12 +272,9 @@ func convertConfig(repSpecs *hclwrite.Body, diskSizeGB hclwrite.Tokens) error { return err } } - if dConfig.IsPresent() { - // Handle dynamic region_configs return convertDynamicConfig(repSpecs, dConfig, diskSizeGB) } - var configs []*hclwrite.Body for _, block := range collectBlocks(repSpecs, nConfig) { blockb := block.Body() @@ -377,21 +289,13 @@ func convertConfig(repSpecs *hclwrite.Body, diskSizeGB hclwrite.Tokens) error { } func convertDynamicConfig(repSpecs *hclwrite.Body, dConfig dynamicBlock, diskSizeGB hclwrite.Tokens) error { - // Get the block name from the dynamic block itself blockName := getResourceName(dConfig.block) - - // Transform the references in attributes and blocks transformDynamicBlockReferencesRecursive(dConfig.content.Body(), blockName, nRegion) - - // Process specs processAllSpecs(dConfig.content.Body(), diskSizeGB) - - // Build the for expression forExpr := buildForExpr(nRegion, hcl.GetAttrExpr(dConfig.forEach)) tokens := hcl.TokensFromExpr(forExpr) tokens = append(tokens, hcl.TokensObject(dConfig.content.Body())...) tokens = hcl.EncloseBracketsNewLines(tokens) - repSpecs.RemoveBlock(dConfig.block) repSpecs.SetAttributeRaw(nConfig, tokens) return nil @@ -401,15 +305,7 @@ func convertDynamicConfig(repSpecs *hclwrite.Body, dConfig dynamicBlock, diskSiz // exist as attributes in the resource body. In that case conversion is not done // as advanced cluster is not in a valid SDKv2 configuration. func hasExpectedBlocksAsAttributes(resourceb *hclwrite.Body) bool { - expectedBlocks := []string{ - nRepSpecs, - nTags, - nLabels, - nAdvConfig, - nBiConnector, - nPinnedFCV, - nTimeouts, - } + expectedBlocks := []string{nRepSpecs, nTags, nLabels, nAdvConfig, nBiConnector, nPinnedFCV, nTimeouts} for name := range resourceb.Attributes() { if slices.Contains(expectedBlocks, name) { return true diff --git a/internal/convert/clu2adv.go b/internal/convert/clu2adv.go index 6798610..37069da 100644 --- a/internal/convert/clu2adv.go +++ b/internal/convert/clu2adv.go @@ -181,26 +181,16 @@ func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error { resourceb.SetAttributeRaw(nRepSpecs, d.tokens) return nil } - - // Collect all replication_specs blocks first repSpecBlocks := collectBlocks(resourceb, nRepSpecs) - if len(repSpecBlocks) == 0 { return fmt.Errorf("%s: no replication_specs found", errRepSpecs) } - - // Check if any replication_specs has a variable num_shards - hasVariableNumShards := HasVariableNumShards(repSpecBlocks) - - if hasVariableNumShards { + if hasVariableNumShards(repSpecBlocks) { var concatParts []hclwrite.Tokens - for _, block := range repSpecBlocks { spec := hclwrite.NewEmptyFile() specb := spec.Body() specbSrc := block.Body() - - // Check for dynamic region configs d, err := fillWithDynamicRegionConfigs(specbSrc, root, false) if err != nil { return err @@ -209,21 +199,15 @@ func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error { concatParts = append(concatParts, d.tokens) continue } - - // Handle zone_name _ = hcl.MoveAttr(specbSrc, specb, nZoneName, nZoneName, errRepSpecs) - - // Handle num_shards shardsAttr := specbSrc.GetAttribute(nNumShards) if shardsAttr == nil { return fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) } - if errConfig := fillRegionConfigs(specb, specbSrc, root); errConfig != nil { return errConfig } - - tokens, err := ProcessNumShards(shardsAttr, specb) + tokens, err := processNumShards(shardsAttr, specb) if err != nil { return err } diff --git a/internal/convert/shared.go b/internal/convert/shared.go index a3293de..054f793 100644 --- a/internal/convert/shared.go +++ b/internal/convert/shared.go @@ -9,8 +9,8 @@ import ( "github.com/mongodb-labs/atlas-cli-plugin-terraform/internal/hcl" ) -// HasVariableNumShards checks if any block has a variable (non-literal) num_shards attribute -func HasVariableNumShards(blocks []*hclwrite.Block) bool { +// hasVariableNumShards checks if any block has a variable (non-literal) num_shards attribute +func hasVariableNumShards(blocks []*hclwrite.Block) bool { for _, block := range blocks { if shardsAttr := block.Body().GetAttribute(nNumShards); shardsAttr != nil { if _, err := hcl.GetAttrInt(shardsAttr, errNumShards); err != nil { @@ -21,33 +21,25 @@ func HasVariableNumShards(blocks []*hclwrite.Block) bool { return false } -// ProcessNumShards handles num_shards for a block, returning tokens for the expanded specs -// processedBody is the body with num_shards removed and other processing done -func ProcessNumShards(shardsAttr *hclwrite.Attribute, processedBody *hclwrite.Body) (hclwrite.Tokens, error) { +// processNumShards handles num_shards for a block, returning tokens for the expanded specs. +// processedBody is the body with num_shards removed and other processing done. +func processNumShards(shardsAttr *hclwrite.Attribute, processedBody *hclwrite.Body) (hclwrite.Tokens, error) { if shardsAttr == nil { - // No num_shards, default to 1 - return hcl.TokensArraySingle(processedBody), nil + return hcl.TokensArraySingle(processedBody), nil // Default 1 if no num_shards specified } - - shardsVal, err := hcl.GetAttrInt(shardsAttr, errNumShards) - if err != nil { - // num_shards is a variable/expression - shardsExpr := hcl.GetAttrExpr(shardsAttr) - forExpr := fmt.Sprintf("for i in range(%s) :", shardsExpr) - tokens := hcl.TokensFromExpr(forExpr) - tokens = append(tokens, hcl.TokensObject(processedBody)...) - return hcl.EncloseBracketsNewLines(tokens), nil - } - - // num_shards is a literal number - create explicit array - var bodies []*hclwrite.Body - for i := 0; i < shardsVal; i++ { - bodies = append(bodies, processedBody) + if shardsVal, err := hcl.GetAttrInt(shardsAttr, errNumShards); err == nil { + var bodies []*hclwrite.Body + for range shardsVal { + bodies = append(bodies, processedBody) + } + return hcl.TokensArray(bodies), nil } - return hcl.TokensArray(bodies), nil + shardsExpr := hcl.GetAttrExpr(shardsAttr) + tokens := hcl.TokensFromExpr(buildForExpr("i", fmt.Sprintf("range(%s)", shardsExpr))) + tokens = append(tokens, hcl.TokensObject(processedBody)...) + return hcl.EncloseBracketsNewLines(tokens), nil } -// dynamicBlock represents a Terraform dynamic block structure type dynamicBlock struct { block *hclwrite.Block forEach *hclwrite.Attribute @@ -55,7 +47,6 @@ type dynamicBlock struct { tokens hclwrite.Tokens } -// IsPresent returns true if the dynamic block exists func (d dynamicBlock) IsPresent() bool { return d.block != nil } From be5d01a22e615c1932376f86352c38baa66911e8 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Fri, 22 Aug 2025 19:03:37 +0200 Subject: [PATCH 24/43] reduce long funcs --- internal/convert/adv2v2.go | 258 +++++++++++++++++++++++------------- internal/convert/clu2adv.go | 152 ++++++++++++--------- 2 files changed, 250 insertions(+), 160 deletions(-) diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index 659c65c..f6f10f9 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -71,47 +71,17 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error } if hasVariableNumShards(repSpecBlocks) { - var concatParts []hclwrite.Tokens - - for _, block := range repSpecBlocks { - blockb := block.Body() - numShardsAttr := blockb.GetAttribute(nNumShards) - blockb.RemoveAttribute(nNumShards) - - if err := convertConfig(blockb, diskSizeGB); err != nil { - return err - } - - tokens, err := processNumShards(numShardsAttr, blockb) - if err != nil { - return err - } - concatParts = append(concatParts, tokens) + tokens, err := processVariableNumShards(repSpecBlocks, diskSizeGB) + if err != nil { + return err } - resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensFuncConcat(concatParts...)) + resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensFuncConcat(tokens...)) } else { - // All num_shards are numeric or missing, use simple array - var repSpecs []*hclwrite.Body - for _, block := range repSpecBlocks { - blockb := block.Body() - numShardsAttr := blockb.GetAttribute(nNumShards) - blockb.RemoveAttribute(nNumShards) - - if err := convertConfig(blockb, diskSizeGB); err != nil { - return err - } - - if numShardsAttr != nil { - numShardsVal, _ := hcl.GetAttrInt(numShardsAttr, errNumShards) - for range numShardsVal { - repSpecs = append(repSpecs, blockb) - } - } else { - // No num_shards, default to 1 - repSpecs = append(repSpecs, blockb) - } + tokens, err := processStaticNumShards(repSpecBlocks, diskSizeGB) + if err != nil { + return err } - resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensArray(repSpecs)) + resourceb.SetAttributeRaw(nRepSpecs, tokens) } return nil @@ -119,47 +89,174 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error func convertDynamicRepSpecs(resourceb *hclwrite.Body, dSpec dynamicBlock, diskSizeGB hclwrite.Tokens) error { transformDynamicBlockReferences(dSpec.content.Body(), nRepSpecs, nSpec) - dConfig, err := getDynamicBlock(dSpec.content.Body(), nConfig) + + dConfig, err := findDynamicConfigBlock(dSpec.content.Body()) if err != nil { return err } - if !dConfig.IsPresent() { - dConfig, err = getDynamicBlock(dSpec.content.Body(), nConfigSrc) + if dConfig.IsPresent() { + return convertDynamicRepSpecsWithDynamicConfig(resourceb, dSpec, dConfig, diskSizeGB) + } + + tokens, err := processDynamicRepSpecsWithoutConfig(dSpec, diskSizeGB) + if err != nil { + return err + } + + resourceb.RemoveBlock(dSpec.block) + resourceb.SetAttributeRaw(nRepSpecs, tokens) + return nil +} + +func processVariableNumShards(repSpecBlocks []*hclwrite.Block, diskSizeGB hclwrite.Tokens) ([]hclwrite.Tokens, error) { + var concatParts []hclwrite.Tokens + for _, block := range repSpecBlocks { + blockb := block.Body() + numShardsAttr := blockb.GetAttribute(nNumShards) + blockb.RemoveAttribute(nNumShards) + + if err := convertConfig(blockb, diskSizeGB); err != nil { + return nil, err + } + + tokens, err := processNumShards(numShardsAttr, blockb) if err != nil { - return err + return nil, err } + concatParts = append(concatParts, tokens) } - if dConfig.IsPresent() { - return convertDynamicRepSpecsWithDynamicConfig(resourceb, dSpec, dConfig, diskSizeGB) + return concatParts, nil +} + +func processStaticNumShards(repSpecBlocks []*hclwrite.Block, diskSizeGB hclwrite.Tokens) (hclwrite.Tokens, error) { + var repSpecs []*hclwrite.Body + for _, block := range repSpecBlocks { + blockb := block.Body() + numShardsAttr := blockb.GetAttribute(nNumShards) + blockb.RemoveAttribute(nNumShards) + + if err := convertConfig(blockb, diskSizeGB); err != nil { + return nil, err + } + + if numShardsAttr != nil { + numShardsVal, _ := hcl.GetAttrInt(numShardsAttr, errNumShards) + for range numShardsVal { + repSpecs = append(repSpecs, blockb) + } + } else { + repSpecs = append(repSpecs, blockb) + } } - numShardsAttr := dSpec.content.Body().GetAttribute(nNumShards) - if numShardsAttr != nil { - numShardsExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec) - dSpec.content.Body().RemoveAttribute(nNumShards) - if err := convertConfig(dSpec.content.Body(), diskSizeGB); err != nil { - return err + return hcl.TokensArray(repSpecs), nil +} + +func findDynamicConfigBlock(body *hclwrite.Body) (dynamicBlock, error) { + dConfig, err := getDynamicBlock(body, nConfig) + if err != nil { + return dynamicBlock{}, err + } + if !dConfig.IsPresent() { + dConfig, err = getDynamicBlock(body, nConfigSrc) + if err != nil { + return dynamicBlock{}, err } - outerFor := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach)) - innerFor := buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr)) - forExpr := fmt.Sprintf("%s [\n %s ", outerFor, innerFor) - tokens := hcl.TokensFromExpr(forExpr) - tokens = append(tokens, hcl.TokensObject(dSpec.content.Body())...) - tokens = append(tokens, hcl.TokensFromExpr("\n ]\n]")...) - resourceb.RemoveBlock(dSpec.block) - resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensFuncFlatten(tokens)) - return nil } + return dConfig, nil +} + +func processDynamicRepSpecsWithoutConfig(dSpec dynamicBlock, diskSizeGB hclwrite.Tokens) (hclwrite.Tokens, error) { + numShardsAttr := dSpec.content.Body().GetAttribute(nNumShards) dSpec.content.Body().RemoveAttribute(nNumShards) + if err := convertConfig(dSpec.content.Body(), diskSizeGB); err != nil { - return err + return nil, err + } + + if numShardsAttr != nil { + return buildDynamicRepSpecsWithShards(dSpec, numShardsAttr) } + + return buildSimpleDynamicRepSpecs(dSpec) +} + +func buildDynamicRepSpecsWithShards(dSpec dynamicBlock, numShardsAttr *hclwrite.Attribute) (hclwrite.Tokens, error) { + numShardsExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec) + outerFor := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach)) + innerFor := buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr)) + forExpr := fmt.Sprintf("%s [\n %s ", outerFor, innerFor) + tokens := hcl.TokensFromExpr(forExpr) + tokens = append(tokens, hcl.TokensObject(dSpec.content.Body())...) + tokens = append(tokens, hcl.TokensFromExpr("\n ]\n]")...) + return hcl.TokensFuncFlatten(tokens), nil +} + +func buildSimpleDynamicRepSpecs(dSpec dynamicBlock) (hclwrite.Tokens, error) { forExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach)) tokens := hcl.TokensFromExpr(forExpr) tokens = append(tokens, hcl.TokensObject(dSpec.content.Body())...) - tokens = hcl.EncloseBracketsNewLines(tokens) - resourceb.RemoveBlock(dSpec.block) - resourceb.SetAttributeRaw(nRepSpecs, tokens) - return nil + return hcl.EncloseBracketsNewLines(tokens), nil +} + +func buildDynamicRepSpecsWithNumShards(dSpec, dConfig dynamicBlock, diskSizeGB hclwrite.Tokens, + configBlockName string, numShardsAttr *hclwrite.Attribute) (hclwrite.Tokens, error) { + numShardsExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec) + transformDynamicBlockReferencesRecursive(dConfig.content.Body(), configBlockName, nRegion) + + transform := func(expr string) string { + return replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) + } + transformAttributesSorted(dConfig.content.Body(), dConfig.content.Body().Attributes(), transform) + for _, block := range dConfig.content.Body().Blocks() { + transformAttributesSorted(block.Body(), block.Body().Attributes(), transform) + } + + regionConfigBody := buildRegionConfigBody(dConfig, diskSizeGB) + regionTokens := buildRegionForExpr(nSpec, regionConfigBody) + + repSpecBody := buildRepSpecBody(dSpec, regionTokens) + innerTokens := buildInnerForExpr(numShardsExpr, repSpecBody) + outerTokens := buildOuterForExpr(dSpec, innerTokens) + + return hcl.TokensFuncFlatten(outerTokens), nil +} + +func buildRegionConfigBody(dConfig dynamicBlock, diskSizeGB hclwrite.Tokens) *hclwrite.Body { + regionConfigFile := hclwrite.NewEmptyFile() + regionConfigBody := regionConfigFile.Body() + copyAttributesSorted(regionConfigBody, dConfig.content.Body().Attributes()) + processRegionConfigBlocks(regionConfigBody, dConfig.content.Body().Blocks(), diskSizeGB) + return regionConfigBody +} + +func buildRegionForExpr(spec string, regionConfigBody *hclwrite.Body) hclwrite.Tokens { + configForEach := fmt.Sprintf("%s.%s", spec, nConfig) + regionForExpr := buildForExpr(nRegion, configForEach) + regionTokens := hcl.TokensFromExpr(regionForExpr) + return append(regionTokens, hcl.TokensObject(regionConfigBody)...) +} + +func buildRepSpecBody(dSpec dynamicBlock, regionTokens hclwrite.Tokens) *hclwrite.Body { + repSpecFile := hclwrite.NewEmptyFile() + repSpecBody := repSpecFile.Body() + if zoneNameAttr := dSpec.content.Body().GetAttribute(nZoneName); zoneNameAttr != nil { + zoneNameExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(zoneNameAttr), nRepSpecs, nSpec) + repSpecBody.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(zoneNameExpr)) + } + repSpecBody.SetAttributeRaw(nConfig, hcl.EncloseBracketsNewLines(regionTokens)) + return repSpecBody +} + +func buildInnerForExpr(numShardsExpr string, repSpecBody *hclwrite.Body) hclwrite.Tokens { + innerForExpr := buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr)) + innerTokens := hcl.TokensFromExpr(innerForExpr) + return append(innerTokens, hcl.TokensObject(repSpecBody)...) +} + +func buildOuterForExpr(dSpec dynamicBlock, innerTokens hclwrite.Tokens) hclwrite.Tokens { + outerForExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach)) + outerTokens := hcl.TokensFromExpr(fmt.Sprintf("%s ", outerForExpr)) + return append(outerTokens, hcl.EncloseBracketsNewLines(innerTokens)...) } func processRegionConfigBlocks(targetBody *hclwrite.Body, blocks []*hclwrite.Block, diskSizeGB hclwrite.Tokens) { @@ -181,37 +278,10 @@ func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dC configBlockName := getResourceName(dConfig.block) numShardsAttr := dSpec.content.Body().GetAttribute(nNumShards) if numShardsAttr != nil { - numShardsExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec) - transformDynamicBlockReferencesRecursive(dConfig.content.Body(), configBlockName, nRegion) - transform := func(expr string) string { - return replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) - } - transformAttributesSorted(dConfig.content.Body(), dConfig.content.Body().Attributes(), transform) - for _, block := range dConfig.content.Body().Blocks() { - transformAttributesSorted(block.Body(), block.Body().Attributes(), transform) - } - configForEach := fmt.Sprintf("%s.%s", nSpec, nConfig) - regionConfigFile := hclwrite.NewEmptyFile() - regionConfigBody := regionConfigFile.Body() - copyAttributesSorted(regionConfigBody, dConfig.content.Body().Attributes()) - processRegionConfigBlocks(regionConfigBody, dConfig.content.Body().Blocks(), diskSizeGB) - regionForExpr := buildForExpr(nRegion, configForEach) - regionTokens := hcl.TokensFromExpr(regionForExpr) - regionTokens = append(regionTokens, hcl.TokensObject(regionConfigBody)...) - repSpecFile := hclwrite.NewEmptyFile() - repSpecBody := repSpecFile.Body() - if zoneNameAttr := dSpec.content.Body().GetAttribute(nZoneName); zoneNameAttr != nil { - zoneNameExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(zoneNameAttr), nRepSpecs, nSpec) - repSpecBody.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(zoneNameExpr)) + tokens, err := buildDynamicRepSpecsWithNumShards(dSpec, dConfig, diskSizeGB, configBlockName, numShardsAttr) + if err != nil { + return err } - repSpecBody.SetAttributeRaw(nConfig, hcl.EncloseBracketsNewLines(regionTokens)) - innerForExpr := buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr)) - innerTokens := hcl.TokensFromExpr(innerForExpr) - innerTokens = append(innerTokens, hcl.TokensObject(repSpecBody)...) - outerForExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach)) - outerTokens := hcl.TokensFromExpr(fmt.Sprintf("%s ", outerForExpr)) - outerTokens = append(outerTokens, hcl.EncloseBracketsNewLines(innerTokens)...) - tokens := hcl.TokensFuncFlatten(outerTokens) resourceb.RemoveBlock(dSpec.block) resourceb.SetAttributeRaw(nRepSpecs, tokens) return nil diff --git a/internal/convert/clu2adv.go b/internal/convert/clu2adv.go index 37069da..98d7b32 100644 --- a/internal/convert/clu2adv.go +++ b/internal/convert/clu2adv.go @@ -181,80 +181,24 @@ func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error { resourceb.SetAttributeRaw(nRepSpecs, d.tokens) return nil } + repSpecBlocks := collectBlocks(resourceb, nRepSpecs) if len(repSpecBlocks) == 0 { return fmt.Errorf("%s: no replication_specs found", errRepSpecs) } + if hasVariableNumShards(repSpecBlocks) { - var concatParts []hclwrite.Tokens - for _, block := range repSpecBlocks { - spec := hclwrite.NewEmptyFile() - specb := spec.Body() - specbSrc := block.Body() - d, err := fillWithDynamicRegionConfigs(specbSrc, root, false) - if err != nil { - return err - } - if d.IsPresent() { - concatParts = append(concatParts, d.tokens) - continue - } - _ = hcl.MoveAttr(specbSrc, specb, nZoneName, nZoneName, errRepSpecs) - shardsAttr := specbSrc.GetAttribute(nNumShards) - if shardsAttr == nil { - return fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) - } - if errConfig := fillRegionConfigs(specb, specbSrc, root); errConfig != nil { - return errConfig - } - tokens, err := processNumShards(shardsAttr, specb) - if err != nil { - return err - } - concatParts = append(concatParts, tokens) + tokens, err := processVariableReplicationSpecs(repSpecBlocks, root) + if err != nil { + return err } - resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensFuncConcat(concatParts...)) + resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensFuncConcat(tokens...)) } else { - // All num_shards are numeric, use simple array - var specbs []*hclwrite.Body - for _, block := range repSpecBlocks { - spec := hclwrite.NewEmptyFile() - specb := spec.Body() - specbSrc := block.Body() - - // Check for dynamic region configs - d, err := fillWithDynamicRegionConfigs(specbSrc, root, false) - if err != nil { - return err - } - if d.IsPresent() { - // For dynamic blocks that have numerical num_shards - // Extract the tokens and add to array - // This is complex, for now just return the dynamic block as is - resourceb.SetAttributeRaw(nRepSpecs, d.tokens) - return nil - } - - // Handle zone_name - _ = hcl.MoveAttr(specbSrc, specb, nZoneName, nZoneName, errRepSpecs) - - // Handle num_shards - shardsAttr := specbSrc.GetAttribute(nNumShards) - if shardsAttr == nil { - return fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) - } - - shardsVal, _ := hcl.GetAttrInt(shardsAttr, errNumShards) - - if errConfig := fillRegionConfigs(specb, specbSrc, root); errConfig != nil { - return errConfig - } - - for range shardsVal { - specbs = append(specbs, specb) - } + tokens, err := processStaticReplicationSpecs(resourceb, repSpecBlocks, root) + if err != nil { + return err } - resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensArray(specbs)) + resourceb.SetAttributeRaw(nRepSpecs, tokens) } return nil @@ -539,6 +483,82 @@ func getDynamicBlockRegionArray(forEach string, configSrc *hclwrite.Block, root return hcl.EncloseBracketsNewLines(tokens), nil } +func processVariableReplicationSpecs(repSpecBlocks []*hclwrite.Block, root attrVals) ([]hclwrite.Tokens, error) { + var concatParts []hclwrite.Tokens + for _, block := range repSpecBlocks { + tokens, err := processReplicationSpecBlock(block, root, true) + if err != nil { + return nil, err + } + concatParts = append(concatParts, tokens) + } + return concatParts, nil +} + +func processStaticReplicationSpecs(resourceb *hclwrite.Body, repSpecBlocks []*hclwrite.Block, + root attrVals) (hclwrite.Tokens, error) { + var specbs []*hclwrite.Body + for _, block := range repSpecBlocks { + spec := hclwrite.NewEmptyFile() + specb := spec.Body() + specbSrc := block.Body() + + d, err := fillWithDynamicRegionConfigs(specbSrc, root, false) + if err != nil { + return nil, err + } + if d.IsPresent() { + // For dynamic blocks that have numerical num_shards + // This is complex, return the dynamic block as is + return d.tokens, nil + } + + _ = hcl.MoveAttr(specbSrc, specb, nZoneName, nZoneName, errRepSpecs) + + shardsAttr := specbSrc.GetAttribute(nNumShards) + if shardsAttr == nil { + return nil, fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) + } + + shardsVal, _ := hcl.GetAttrInt(shardsAttr, errNumShards) + + if errConfig := fillRegionConfigs(specb, specbSrc, root); errConfig != nil { + return nil, errConfig + } + + for range shardsVal { + specbs = append(specbs, specb) + } + } + return hcl.TokensArray(specbs), nil +} + +func processReplicationSpecBlock(block *hclwrite.Block, root attrVals, isVariable bool) (hclwrite.Tokens, error) { + spec := hclwrite.NewEmptyFile() + specb := spec.Body() + specbSrc := block.Body() + + d, err := fillWithDynamicRegionConfigs(specbSrc, root, false) + if err != nil { + return nil, err + } + if d.IsPresent() { + return d.tokens, nil + } + + _ = hcl.MoveAttr(specbSrc, specb, nZoneName, nZoneName, errRepSpecs) + shardsAttr := specbSrc.GetAttribute(nNumShards) + if shardsAttr == nil { + return nil, fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) + } + + if errConfig := fillRegionConfigs(specb, specbSrc, root); errConfig != nil { + return nil, errConfig + } + + return processNumShards(shardsAttr, specb) +} + func sortConfigsByPriority(configs []*hclwrite.Body) []*hclwrite.Body { for _, config := range configs { if _, err := hcl.GetAttrInt(config.GetAttribute(nPriority), errPriority); err != nil { From 0ef832192a01e35bf1d08623ccca7edb32b54397 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Fri, 22 Aug 2025 19:16:30 +0200 Subject: [PATCH 25/43] reduce empty lines --- internal/convert/adv2v2.go | 38 +++++++++---------------------------- internal/convert/clu2adv.go | 4 ---- internal/convert/shared.go | 3 --- 3 files changed, 9 insertions(+), 36 deletions(-) diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index f6f10f9..3afb52c 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -64,32 +64,25 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error if dSpec.IsPresent() { return convertDynamicRepSpecs(resourceb, dSpec, diskSizeGB) } - repSpecBlocks := collectBlocks(resourceb, nRepSpecs) if len(repSpecBlocks) == 0 { return fmt.Errorf("must have at least one replication_specs") } - + var tokens hclwrite.Tokens if hasVariableNumShards(repSpecBlocks) { - tokens, err := processVariableNumShards(repSpecBlocks, diskSizeGB) - if err != nil { - return err - } - resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensFuncConcat(tokens...)) + tokens, err = processVariableNumShards(repSpecBlocks, diskSizeGB) } else { - tokens, err := processStaticNumShards(repSpecBlocks, diskSizeGB) - if err != nil { - return err - } - resourceb.SetAttributeRaw(nRepSpecs, tokens) + tokens, err = processStaticNumShards(repSpecBlocks, diskSizeGB) } - + if err != nil { + return err + } + resourceb.SetAttributeRaw(nRepSpecs, tokens) return nil } func convertDynamicRepSpecs(resourceb *hclwrite.Body, dSpec dynamicBlock, diskSizeGB hclwrite.Tokens) error { transformDynamicBlockReferences(dSpec.content.Body(), nRepSpecs, nSpec) - dConfig, err := findDynamicConfigBlock(dSpec.content.Body()) if err != nil { return err @@ -97,35 +90,31 @@ func convertDynamicRepSpecs(resourceb *hclwrite.Body, dSpec dynamicBlock, diskSi if dConfig.IsPresent() { return convertDynamicRepSpecsWithDynamicConfig(resourceb, dSpec, dConfig, diskSizeGB) } - tokens, err := processDynamicRepSpecsWithoutConfig(dSpec, diskSizeGB) if err != nil { return err } - resourceb.RemoveBlock(dSpec.block) resourceb.SetAttributeRaw(nRepSpecs, tokens) return nil } -func processVariableNumShards(repSpecBlocks []*hclwrite.Block, diskSizeGB hclwrite.Tokens) ([]hclwrite.Tokens, error) { +func processVariableNumShards(repSpecBlocks []*hclwrite.Block, diskSizeGB hclwrite.Tokens) (hclwrite.Tokens, error) { var concatParts []hclwrite.Tokens for _, block := range repSpecBlocks { blockb := block.Body() numShardsAttr := blockb.GetAttribute(nNumShards) blockb.RemoveAttribute(nNumShards) - if err := convertConfig(blockb, diskSizeGB); err != nil { return nil, err } - tokens, err := processNumShards(numShardsAttr, blockb) if err != nil { return nil, err } concatParts = append(concatParts, tokens) } - return concatParts, nil + return hcl.TokensFuncConcat(concatParts...), nil } func processStaticNumShards(repSpecBlocks []*hclwrite.Block, diskSizeGB hclwrite.Tokens) (hclwrite.Tokens, error) { @@ -134,11 +123,9 @@ func processStaticNumShards(repSpecBlocks []*hclwrite.Block, diskSizeGB hclwrite blockb := block.Body() numShardsAttr := blockb.GetAttribute(nNumShards) blockb.RemoveAttribute(nNumShards) - if err := convertConfig(blockb, diskSizeGB); err != nil { return nil, err } - if numShardsAttr != nil { numShardsVal, _ := hcl.GetAttrInt(numShardsAttr, errNumShards) for range numShardsVal { @@ -168,15 +155,12 @@ func findDynamicConfigBlock(body *hclwrite.Body) (dynamicBlock, error) { func processDynamicRepSpecsWithoutConfig(dSpec dynamicBlock, diskSizeGB hclwrite.Tokens) (hclwrite.Tokens, error) { numShardsAttr := dSpec.content.Body().GetAttribute(nNumShards) dSpec.content.Body().RemoveAttribute(nNumShards) - if err := convertConfig(dSpec.content.Body(), diskSizeGB); err != nil { return nil, err } - if numShardsAttr != nil { return buildDynamicRepSpecsWithShards(dSpec, numShardsAttr) } - return buildSimpleDynamicRepSpecs(dSpec) } @@ -202,7 +186,6 @@ func buildDynamicRepSpecsWithNumShards(dSpec, dConfig dynamicBlock, diskSizeGB h configBlockName string, numShardsAttr *hclwrite.Attribute) (hclwrite.Tokens, error) { numShardsExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec) transformDynamicBlockReferencesRecursive(dConfig.content.Body(), configBlockName, nRegion) - transform := func(expr string) string { return replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) } @@ -210,14 +193,11 @@ func buildDynamicRepSpecsWithNumShards(dSpec, dConfig dynamicBlock, diskSizeGB h for _, block := range dConfig.content.Body().Blocks() { transformAttributesSorted(block.Body(), block.Body().Attributes(), transform) } - regionConfigBody := buildRegionConfigBody(dConfig, diskSizeGB) regionTokens := buildRegionForExpr(nSpec, regionConfigBody) - repSpecBody := buildRepSpecBody(dSpec, regionTokens) innerTokens := buildInnerForExpr(numShardsExpr, repSpecBody) outerTokens := buildOuterForExpr(dSpec, innerTokens) - return hcl.TokensFuncFlatten(outerTokens), nil } diff --git a/internal/convert/clu2adv.go b/internal/convert/clu2adv.go index 98d7b32..5adec31 100644 --- a/internal/convert/clu2adv.go +++ b/internal/convert/clu2adv.go @@ -67,7 +67,6 @@ func convertResource(block *hclwrite.Block) (bool, error) { if errDyn := checkDynamicBlock(blockb); errDyn != nil { return false, errDyn } - var err error if isFreeTierCluster(blockb) { err = fillFreeTierCluster(blockb) @@ -139,7 +138,6 @@ func fillFreeTierCluster(resourceb *hclwrite.Body) error { return err } configb.SetAttributeRaw(nElectableSpecs, hcl.TokensObject(electableSpec.Body())) - repSpecs := hclwrite.NewEmptyFile() repSpecs.Body().SetAttributeRaw(nConfig, hcl.TokensArraySingle(configb)) resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensArraySingle(repSpecs.Body())) @@ -181,12 +179,10 @@ func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error { resourceb.SetAttributeRaw(nRepSpecs, d.tokens) return nil } - repSpecBlocks := collectBlocks(resourceb, nRepSpecs) if len(repSpecBlocks) == 0 { return fmt.Errorf("%s: no replication_specs found", errRepSpecs) } - if hasVariableNumShards(repSpecBlocks) { tokens, err := processVariableReplicationSpecs(repSpecBlocks, root) if err != nil { diff --git a/internal/convert/shared.go b/internal/convert/shared.go index 054f793..b76f5c4 100644 --- a/internal/convert/shared.go +++ b/internal/convert/shared.go @@ -100,13 +100,10 @@ func transformDynamicBlockReferences(configSrcb *hclwrite.Body, blockName, varNa // transformDynamicBlockReferencesRecursive transforms attributes and nested blocks recursively // replacing references from dynamic block format, e.g. regions_config.value.* to region.* func transformDynamicBlockReferencesRecursive(body *hclwrite.Body, blockName, varName string) { - // Transform attributes in deterministic order transform := func(expr string) string { return replaceDynamicBlockReferences(expr, blockName, varName) } transformAttributesSorted(body, body.Attributes(), transform) - - // Transform nested blocks for _, block := range body.Blocks() { transformDynamicBlockReferencesRecursive(block.Body(), blockName, varName) } From 383396a77a08e24475dcb492c57e81d472996848 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Sat, 23 Aug 2025 10:18:04 +0200 Subject: [PATCH 26/43] trailingSpace in buildForExpr --- internal/convert/adv2v2.go | 22 +++++++++++----------- internal/convert/clu2adv.go | 10 +++++----- internal/convert/shared.go | 10 +++++++--- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index 3afb52c..7b41997 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -166,8 +166,8 @@ func processDynamicRepSpecsWithoutConfig(dSpec dynamicBlock, diskSizeGB hclwrite func buildDynamicRepSpecsWithShards(dSpec dynamicBlock, numShardsAttr *hclwrite.Attribute) (hclwrite.Tokens, error) { numShardsExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec) - outerFor := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach)) - innerFor := buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr)) + outerFor := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach), false) + innerFor := buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr), false) forExpr := fmt.Sprintf("%s [\n %s ", outerFor, innerFor) tokens := hcl.TokensFromExpr(forExpr) tokens = append(tokens, hcl.TokensObject(dSpec.content.Body())...) @@ -176,7 +176,7 @@ func buildDynamicRepSpecsWithShards(dSpec dynamicBlock, numShardsAttr *hclwrite. } func buildSimpleDynamicRepSpecs(dSpec dynamicBlock) (hclwrite.Tokens, error) { - forExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach)) + forExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach), false) tokens := hcl.TokensFromExpr(forExpr) tokens = append(tokens, hcl.TokensObject(dSpec.content.Body())...) return hcl.EncloseBracketsNewLines(tokens), nil @@ -211,7 +211,7 @@ func buildRegionConfigBody(dConfig dynamicBlock, diskSizeGB hclwrite.Tokens) *hc func buildRegionForExpr(spec string, regionConfigBody *hclwrite.Body) hclwrite.Tokens { configForEach := fmt.Sprintf("%s.%s", spec, nConfig) - regionForExpr := buildForExpr(nRegion, configForEach) + regionForExpr := buildForExpr(nRegion, configForEach, false) regionTokens := hcl.TokensFromExpr(regionForExpr) return append(regionTokens, hcl.TokensObject(regionConfigBody)...) } @@ -228,14 +228,14 @@ func buildRepSpecBody(dSpec dynamicBlock, regionTokens hclwrite.Tokens) *hclwrit } func buildInnerForExpr(numShardsExpr string, repSpecBody *hclwrite.Body) hclwrite.Tokens { - innerForExpr := buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr)) + innerForExpr := buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr), false) innerTokens := hcl.TokensFromExpr(innerForExpr) return append(innerTokens, hcl.TokensObject(repSpecBody)...) } func buildOuterForExpr(dSpec dynamicBlock, innerTokens hclwrite.Tokens) hclwrite.Tokens { - outerForExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach)) - outerTokens := hcl.TokensFromExpr(fmt.Sprintf("%s ", outerForExpr)) + outerForExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach), true) + outerTokens := hcl.TokensFromExpr(outerForExpr) return append(outerTokens, hcl.EncloseBracketsNewLines(innerTokens)...) } @@ -298,12 +298,12 @@ func convertDynamicRepSpecsWithoutNumShards(resourceb *hclwrite.Body, dSpec, dCo } processAllSpecs(configb, diskSizeGB) configForEach := fmt.Sprintf("%s.%s", nSpec, nConfig) - regionForExpr := buildForExpr(nRegion, configForEach) + regionForExpr := buildForExpr(nRegion, configForEach, false) regionTokens := hcl.TokensFromExpr(regionForExpr) regionTokens = append(regionTokens, hcl.TokensObject(configb)...) repSpecb.SetAttributeRaw(nConfig, hcl.EncloseBracketsNewLines(regionTokens)) - forExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach)) - innerTokens := hcl.TokensFromExpr(fmt.Sprintf("%s ", forExpr)) + forExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach), true) + innerTokens := hcl.TokensFromExpr(forExpr) innerTokens = append(innerTokens, hcl.TokensArraySingle(repSpecb)...) tokens := hcl.TokensFuncFlatten(innerTokens) resourceb.RemoveBlock(dSpec.block) @@ -342,7 +342,7 @@ func convertDynamicConfig(repSpecs *hclwrite.Body, dConfig dynamicBlock, diskSiz blockName := getResourceName(dConfig.block) transformDynamicBlockReferencesRecursive(dConfig.content.Body(), blockName, nRegion) processAllSpecs(dConfig.content.Body(), diskSizeGB) - forExpr := buildForExpr(nRegion, hcl.GetAttrExpr(dConfig.forEach)) + forExpr := buildForExpr(nRegion, hcl.GetAttrExpr(dConfig.forEach), false) tokens := hcl.TokensFromExpr(forExpr) tokens = append(tokens, hcl.TokensObject(dConfig.content.Body())...) tokens = hcl.EncloseBracketsNewLines(tokens) diff --git a/internal/convert/clu2adv.go b/internal/convert/clu2adv.go index 5adec31..29bdc73 100644 --- a/internal/convert/clu2adv.go +++ b/internal/convert/clu2adv.go @@ -281,7 +281,7 @@ func fillReplicationSpecsWithDynamicBlock(resourceb *hclwrite.Body, root attrVal if err != nil { return dynamicBlock{}, err } - forSpec := hcl.TokensFromExpr(fmt.Sprintf("%s ", buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach)))) + forSpec := hcl.TokensFromExpr(buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach), true)) forSpec = append(forSpec, dConfig.tokens...) tokens := hcl.TokensFuncFlatten(forSpec) dSpec.tokens = tokens @@ -307,9 +307,9 @@ func fillWithDynamicRegionConfigs(specbSrc *hclwrite.Body, root attrVals, change if err != nil { return dynamicBlock{}, err } - priorityForStr := buildForExpr(nPriority, fmt.Sprintf("range(%d, %d, -1)", valMaxPriority, valMinPriority)) + priorityForStr := buildForExpr(nPriority, fmt.Sprintf("range(%d, %d, -1)", valMaxPriority, valMinPriority), true) priorityFor := hcl.TokensComment(commentPriorityFor) - priorityFor = append(priorityFor, hcl.TokensFromExpr(fmt.Sprintf("%s ", priorityForStr))...) + priorityFor = append(priorityFor, hcl.TokensFromExpr(priorityForStr)...) priorityFor = append(priorityFor, regionFor...) repSpecb.SetAttributeRaw(nConfig, hcl.TokensFuncFlatten(priorityFor)) @@ -317,7 +317,7 @@ func fillWithDynamicRegionConfigs(specbSrc *hclwrite.Body, root attrVals, change if shards == nil { return dynamicBlock{}, fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) } - tokens := hcl.TokensFromExpr(buildForExpr("i", fmt.Sprintf("range(%s)", hcl.GetAttrExpr(shards)))) + tokens := hcl.TokensFromExpr(buildForExpr("i", fmt.Sprintf("range(%s)", hcl.GetAttrExpr(shards)), false)) tokens = append(tokens, hcl.EncloseBraces(repSpec.BuildTokens(nil), true)...) d.tokens = hcl.EncloseBracketsNewLines(tokens) return d, nil @@ -473,7 +473,7 @@ func getDynamicBlockRegionArray(forEach string, configSrc *hclwrite.Block, root if err != nil { return nil, err } - tokens := hcl.TokensFromExpr(buildForExpr(nRegion, forEach)) + tokens := hcl.TokensFromExpr(buildForExpr(nRegion, forEach, false)) tokens = append(tokens, hcl.EncloseBraces(region.BuildTokens(nil), true)...) tokens = append(tokens, hcl.TokensFromExpr(fmt.Sprintf("if %s == %s", nPriority, priorityStr))...) return hcl.EncloseBracketsNewLines(tokens), nil diff --git a/internal/convert/shared.go b/internal/convert/shared.go index b76f5c4..6105e08 100644 --- a/internal/convert/shared.go +++ b/internal/convert/shared.go @@ -35,7 +35,7 @@ func processNumShards(shardsAttr *hclwrite.Attribute, processedBody *hclwrite.Bo return hcl.TokensArray(bodies), nil } shardsExpr := hcl.GetAttrExpr(shardsAttr) - tokens := hcl.TokensFromExpr(buildForExpr("i", fmt.Sprintf("range(%s)", shardsExpr))) + tokens := hcl.TokensFromExpr(buildForExpr("i", fmt.Sprintf("range(%s)", shardsExpr), false)) tokens = append(tokens, hcl.TokensObject(processedBody)...) return hcl.EncloseBracketsNewLines(tokens), nil } @@ -204,6 +204,10 @@ func fillSpecOpt(resourceb *hclwrite.Body, name string, diskSizeGBTokens hclwrit } // buildForExpr builds a for expression with the given variable and collection -func buildForExpr(varName, collection string) string { - return fmt.Sprintf("for %s in %s :", varName, collection) +func buildForExpr(varName, collection string, trailingSpace bool) string { + expr := fmt.Sprintf("for %s in %s :", varName, collection) + if trailingSpace { + expr += " " + } + return expr } From 26fe17d5bb155b720a66a6b248af26493ac552dc Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Sat, 23 Aug 2025 11:21:21 +0200 Subject: [PATCH 27/43] checkDynamicBlock --- internal/convert/adv2v2.go | 42 +++++++++---------- internal/convert/clu2adv.go | 20 +-------- internal/convert/shared.go | 12 ++++++ .../adv2v2/dynamic_unsupported_tag.in.tf | 25 +++++++++++ internal/convert/testdata/adv2v2/errors.json | 3 +- .../clu2adv/dynamic_unsupported_tag.in.tf | 16 +++++++ internal/convert/testdata/clu2adv/errors.json | 3 +- internal/hcl/hcl.go | 13 ++---- 8 files changed, 83 insertions(+), 51 deletions(-) create mode 100644 internal/convert/testdata/adv2v2/dynamic_unsupported_tag.in.tf create mode 100644 internal/convert/testdata/clu2adv/dynamic_unsupported_tag.in.tf diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index 7b41997..4019994 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -36,6 +36,9 @@ func updateResource(resource *hclwrite.Block) (bool, error) { return false, nil } resourceb := resource.Body() + if errDyn := checkDynamicBlock(resourceb); errDyn != nil { + return false, errDyn + } if hasExpectedBlocksAsAttributes(resourceb) { return false, nil } @@ -194,10 +197,24 @@ func buildDynamicRepSpecsWithNumShards(dSpec, dConfig dynamicBlock, diskSizeGB h transformAttributesSorted(block.Body(), block.Body().Attributes(), transform) } regionConfigBody := buildRegionConfigBody(dConfig, diskSizeGB) - regionTokens := buildRegionForExpr(nSpec, regionConfigBody) + + configForEach := fmt.Sprintf("%s.%s", nSpec, nConfig) + regionForExpr := buildForExpr(nRegion, configForEach, false) + regionTokens := hcl.TokensFromExpr(regionForExpr) + regionTokens = append(regionTokens, hcl.TokensObject(regionConfigBody)...) + repSpecBody := buildRepSpecBody(dSpec, regionTokens) - innerTokens := buildInnerForExpr(numShardsExpr, repSpecBody) - outerTokens := buildOuterForExpr(dSpec, innerTokens) + + // Inline buildInnerForExpr + innerForExpr := buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr), false) + innerTokens := hcl.TokensFromExpr(innerForExpr) + innerTokens = append(innerTokens, hcl.TokensObject(repSpecBody)...) + + // Inline buildOuterForExpr + outerForExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach), true) + outerTokens := hcl.TokensFromExpr(outerForExpr) + outerTokens = append(outerTokens, hcl.EncloseBracketsNewLines(innerTokens)...) + return hcl.TokensFuncFlatten(outerTokens), nil } @@ -209,13 +226,6 @@ func buildRegionConfigBody(dConfig dynamicBlock, diskSizeGB hclwrite.Tokens) *hc return regionConfigBody } -func buildRegionForExpr(spec string, regionConfigBody *hclwrite.Body) hclwrite.Tokens { - configForEach := fmt.Sprintf("%s.%s", spec, nConfig) - regionForExpr := buildForExpr(nRegion, configForEach, false) - regionTokens := hcl.TokensFromExpr(regionForExpr) - return append(regionTokens, hcl.TokensObject(regionConfigBody)...) -} - func buildRepSpecBody(dSpec dynamicBlock, regionTokens hclwrite.Tokens) *hclwrite.Body { repSpecFile := hclwrite.NewEmptyFile() repSpecBody := repSpecFile.Body() @@ -227,18 +237,6 @@ func buildRepSpecBody(dSpec dynamicBlock, regionTokens hclwrite.Tokens) *hclwrit return repSpecBody } -func buildInnerForExpr(numShardsExpr string, repSpecBody *hclwrite.Body) hclwrite.Tokens { - innerForExpr := buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr), false) - innerTokens := hcl.TokensFromExpr(innerForExpr) - return append(innerTokens, hcl.TokensObject(repSpecBody)...) -} - -func buildOuterForExpr(dSpec dynamicBlock, innerTokens hclwrite.Tokens) hclwrite.Tokens { - outerForExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach), true) - outerTokens := hcl.TokensFromExpr(outerForExpr) - return append(outerTokens, hcl.EncloseBracketsNewLines(innerTokens)...) -} - func processRegionConfigBlocks(targetBody *hclwrite.Body, blocks []*hclwrite.Block, diskSizeGB hclwrite.Tokens) { for _, block := range blocks { blockType := block.Type() diff --git a/internal/convert/clu2adv.go b/internal/convert/clu2adv.go index 29bdc73..4c632fb 100644 --- a/internal/convert/clu2adv.go +++ b/internal/convert/clu2adv.go @@ -2,7 +2,6 @@ package convert import ( "fmt" - "slices" "sort" "strconv" "strings" @@ -13,10 +12,6 @@ import ( "github.com/zclconf/go-cty/cty" ) -var ( - dynamicBlockAllowList = []string{nTags, nLabels, nConfigSrc, nRepSpecs} -) - type attrVals struct { req map[string]hclwrite.Tokens opt map[string]hclwrite.Tokens @@ -236,7 +231,7 @@ func extractTagsLabelsDynamicBlock(resourceb *hclwrite.Body, name string) (hclwr valueExpr := replaceDynamicBlockExpr(value, name, nValue) collectionExpr := hcl.GetAttrExpr(d.forEach) forExpr := fmt.Sprintf("for key, value in %s : %s => %s", collectionExpr, keyExpr, valueExpr) - tokens := hcl.TokensObjectFromExpr(forExpr) + tokens := hcl.EncloseBraces(hcl.EncloseNewLines(hcl.TokensFromExpr(forExpr)), false) if keyExpr == nKey && valueExpr == nValue { // expression can be simplified and use for_each expression tokens = hcl.TokensFromExpr(collectionExpr) } @@ -445,17 +440,6 @@ func getResourceLabel(resource *hclwrite.Block) string { return labels[1] } -func checkDynamicBlock(body *hclwrite.Body) error { - for _, block := range body.Blocks() { - name := getResourceName(block) - if block.Type() != nDynamic || slices.Contains(dynamicBlockAllowList, name) { - continue - } - return fmt.Errorf("dynamic blocks are not supported for %s", name) - } - return nil -} - func replaceDynamicBlockExpr(attr *hclwrite.Attribute, blockName, attrName string) string { expr := hcl.GetAttrExpr(attr) return strings.ReplaceAll(expr, fmt.Sprintf("%s.%s", blockName, attrName), attrName) @@ -570,7 +554,7 @@ func sortConfigsByPriority(configs []*hclwrite.Body) []*hclwrite.Body { } func setKeyValue(body *hclwrite.Body, key, value *hclwrite.Attribute) { - keyStr, err := hcl.GetAttrString(key, "") + keyStr, err := hcl.GetAttrString(key) if err == nil { if !hclsyntax.ValidIdentifier(keyStr) { // wrap in quotes so invalid identifiers (e.g. with blanks) can be used as attribute names diff --git a/internal/convert/shared.go b/internal/convert/shared.go index 6105e08..d1501b6 100644 --- a/internal/convert/shared.go +++ b/internal/convert/shared.go @@ -71,6 +71,18 @@ func getDynamicBlock(body *hclwrite.Body, name string) (dynamicBlock, error) { return dynamicBlock{}, nil } +func checkDynamicBlock(body *hclwrite.Body) error { + dynamicBlockAllowList := []string{nTags, nLabels, nRepSpecs} + for _, block := range body.Blocks() { + name := getResourceName(block) + if block.Type() != nDynamic || slices.Contains(dynamicBlockAllowList, name) { + continue + } + return fmt.Errorf("dynamic blocks are not supported for %s", name) + } + return nil +} + // getResourceName returns the first label of a block, if it exists. // e.g. in resource "mongodbatlas_cluster" "mycluster", the first label is "mongodbatlas_cluster". func getResourceName(resource *hclwrite.Block) string { diff --git a/internal/convert/testdata/adv2v2/dynamic_unsupported_tag.in.tf b/internal/convert/testdata/adv2v2/dynamic_unsupported_tag.in.tf new file mode 100644 index 0000000..85884a7 --- /dev/null +++ b/internal/convert/testdata/adv2v2/dynamic_unsupported_tag.in.tf @@ -0,0 +1,25 @@ +resource "mongodbatlas_advanced_cluster" "this" { + project_id = var.project_id + name = "cluster" + cluster_type = "REPLICASET" + + # dynamic blocks are only supported for tags, labels, replication_specs and region_configs + dynamic "advanced_configuration" { + for_each = var.advanced_configuration + content { + javascript_enabled = advanced_configuration.value.javascript_enabled + } + } + + replication_specs { + region_configs { + priority = 7 + provider_name = "AWS" + region_name = "EU_WEST_1" + electable_specs { + instance_size = "M10" + node_count = 3 + } + } + } +} diff --git a/internal/convert/testdata/adv2v2/errors.json b/internal/convert/testdata/adv2v2/errors.json index 1a758de..dae5a07 100644 --- a/internal/convert/testdata/adv2v2/errors.json +++ b/internal/convert/testdata/adv2v2/errors.json @@ -1,5 +1,6 @@ { "configuration_file_error": "failed to parse Terraform config file", "replication_specs_missing_region_configs": "replication_specs must have at least one region_configs", - "missing_replication_specs": "must have at least one replication_specs" + "missing_replication_specs": "must have at least one replication_specs", + "dynamic_unsupported_tag": "dynamic blocks are not supported for advanced_configuration" } diff --git a/internal/convert/testdata/clu2adv/dynamic_unsupported_tag.in.tf b/internal/convert/testdata/clu2adv/dynamic_unsupported_tag.in.tf new file mode 100644 index 0000000..1b0c752 --- /dev/null +++ b/internal/convert/testdata/clu2adv/dynamic_unsupported_tag.in.tf @@ -0,0 +1,16 @@ +resource "mongodbatlas_cluster" "this" { + project_id = var.project_id + name = var.cluster_name + cluster_type = var.cluster_type + mongo_db_major_version = var.mongo_db_major_version + provider_instance_size_name = var.instance_size + provider_name = var.provider_name + + # dynamic blocks are only supported for tags, labels, replication_specs and regions_config + dynamic "advanced_configuration" { + for_each = var.advanced_configuration + content { + javascript_enabled = advanced_configuration.value.javascript_enabled + } + } +} diff --git a/internal/convert/testdata/clu2adv/errors.json b/internal/convert/testdata/clu2adv/errors.json index 9fa5f38..266aa5e 100644 --- a/internal/convert/testdata/clu2adv/errors.json +++ b/internal/convert/testdata/clu2adv/errors.json @@ -4,5 +4,6 @@ "free_cluster_missing_attribute": "free cluster (because no replication_specs): attribute backing_provider_name not found", "regions_config_missing_priority": "setting replication_specs: attribute priority 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_missing_regions_config": "setting replication_specs: regions_config not found", + "dynamic_unsupported_tag": "dynamic blocks are not supported for advanced_configuration" } diff --git a/internal/hcl/hcl.go b/internal/hcl/hcl.go index 23fcc2d..0338ce7 100644 --- a/internal/hcl/hcl.go +++ b/internal/hcl/hcl.go @@ -65,17 +65,17 @@ func GetAttrInt(attr *hclwrite.Attribute, errPrefix string) (int, error) { } // GetAttrString tries to get an attribute value as a string. -func GetAttrString(attr *hclwrite.Attribute, errPrefix string) (string, error) { +func GetAttrString(attr *hclwrite.Attribute) (string, error) { expr, diags := hclsyntax.ParseExpression(attr.Expr().BuildTokens(nil).Bytes(), "", hcl.InitialPos) if diags.HasErrors() { - return "", fmt.Errorf("%s: failed to parse string: %s", errPrefix, diags.Error()) + return "", fmt.Errorf("failed to parse string: %s", diags.Error()) } val, diags := expr.Value(nil) if diags.HasErrors() { - return "", fmt.Errorf("%s: failed to evaluate string: %s", errPrefix, diags.Error()) + return "", fmt.Errorf("failed to evaluate string: %s", diags.Error()) } if !val.Type().Equals(cty.String) { - return "", fmt.Errorf("%s: attribute is not a string", errPrefix) + return "", fmt.Errorf("attribute is not a string") } return val.AsString(), nil } @@ -106,11 +106,6 @@ func TokensFromExpr(expr string) hclwrite.Tokens { return hclwrite.Tokens{{Type: hclsyntax.TokenIdent, Bytes: []byte(expr)}} } -// TokensObjectFromExpr creates an object with an expression. -func TokensObjectFromExpr(expr string) hclwrite.Tokens { - return EncloseBraces(EncloseNewLines(TokensFromExpr(expr)), false) -} - // TokensFuncMerge creates the tokens for the HCL merge function. func TokensFuncMerge(tokens ...hclwrite.Tokens) hclwrite.Tokens { params := EncloseNewLines(joinTokens(tokens...)) From 4acd041031a9cff462e40b243b063580ca32b615 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Sat, 23 Aug 2025 11:33:46 +0200 Subject: [PATCH 28/43] move tags and labels funcs to shared.go --- internal/convert/clu2adv.go | 94 +++---------------------------------- internal/convert/shared.go | 70 +++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 88 deletions(-) diff --git a/internal/convert/clu2adv.go b/internal/convert/clu2adv.go index 4c632fb..8ddc777 100644 --- a/internal/convert/clu2adv.go +++ b/internal/convert/clu2adv.go @@ -179,13 +179,13 @@ func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error { return fmt.Errorf("%s: no replication_specs found", errRepSpecs) } if hasVariableNumShards(repSpecBlocks) { - tokens, err := processVariableReplicationSpecs(repSpecBlocks, root) + tokens, err := processVariableRepSpecs(repSpecBlocks, root) if err != nil { return err } resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensFuncConcat(tokens...)) } else { - tokens, err := processStaticReplicationSpecs(resourceb, repSpecBlocks, root) + tokens, err := processStaticRepSpecs(repSpecBlocks, root) if err != nil { return err } @@ -195,76 +195,6 @@ func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error { return nil } -func fillTagsLabelsOpt(resourceb *hclwrite.Body, name string) error { - tokensDynamic, err := extractTagsLabelsDynamicBlock(resourceb, name) - if err != nil { - return err - } - tokensIndividual, err := extractTagsLabelsIndividual(resourceb, name) - if err != nil { - return err - } - if tokensDynamic != nil && tokensIndividual != nil { - resourceb.SetAttributeRaw(name, hcl.TokensFuncMerge(tokensDynamic, tokensIndividual)) - return nil - } - if tokensDynamic != nil { - resourceb.SetAttributeRaw(name, tokensDynamic) - } - if tokensIndividual != nil { - resourceb.SetAttributeRaw(name, tokensIndividual) - } - return nil -} - -func extractTagsLabelsDynamicBlock(resourceb *hclwrite.Body, name string) (hclwrite.Tokens, error) { - d, err := getDynamicBlock(resourceb, name) - if err != nil || !d.IsPresent() { - return nil, err - } - key := d.content.Body().GetAttribute(nKey) - value := d.content.Body().GetAttribute(nValue) - if key == nil || value == nil { - return nil, fmt.Errorf("dynamic block %s: %s or %s not found", name, nKey, nValue) - } - keyExpr := replaceDynamicBlockExpr(key, name, nKey) - valueExpr := replaceDynamicBlockExpr(value, name, nValue) - collectionExpr := hcl.GetAttrExpr(d.forEach) - forExpr := fmt.Sprintf("for key, value in %s : %s => %s", collectionExpr, keyExpr, valueExpr) - tokens := hcl.EncloseBraces(hcl.EncloseNewLines(hcl.TokensFromExpr(forExpr)), false) - if keyExpr == nKey && valueExpr == nValue { // expression can be simplified and use for_each expression - tokens = hcl.TokensFromExpr(collectionExpr) - } - resourceb.RemoveBlock(d.block) - return tokens, nil -} - -func extractTagsLabelsIndividual(resourceb *hclwrite.Body, name string) (hclwrite.Tokens, error) { - var ( - file = hclwrite.NewEmptyFile() - fileb = file.Body() - found = false - ) - for { - block := resourceb.FirstMatchingBlock(name, nil) - if block == nil { - break - } - key := block.Body().GetAttribute(nKey) - value := block.Body().GetAttribute(nValue) - if key == nil || value == nil { - return nil, fmt.Errorf("%s: %s or %s not found", name, nKey, nValue) - } - setKeyValue(fileb, key, value) - resourceb.RemoveBlock(block) - found = true - } - if !found { - return nil, nil - } - return hcl.TokensObject(fileb), nil -} - // fillReplicationSpecsWithDynamicBlock used for dynamic blocks in replication_specs func fillReplicationSpecsWithDynamicBlock(resourceb *hclwrite.Body, root attrVals) (dynamicBlock, error) { dSpec, err := getDynamicBlock(resourceb, nRepSpecs) @@ -307,7 +237,6 @@ func fillWithDynamicRegionConfigs(specbSrc *hclwrite.Body, root attrVals, change priorityFor = append(priorityFor, hcl.TokensFromExpr(priorityForStr)...) priorityFor = append(priorityFor, regionFor...) repSpecb.SetAttributeRaw(nConfig, hcl.TokensFuncFlatten(priorityFor)) - shards := specbSrc.GetAttribute(nNumShards) if shards == nil { return dynamicBlock{}, fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) @@ -463,10 +392,10 @@ func getDynamicBlockRegionArray(forEach string, configSrc *hclwrite.Block, root return hcl.EncloseBracketsNewLines(tokens), nil } -func processVariableReplicationSpecs(repSpecBlocks []*hclwrite.Block, root attrVals) ([]hclwrite.Tokens, error) { +func processVariableRepSpecs(repSpecBlocks []*hclwrite.Block, root attrVals) ([]hclwrite.Tokens, error) { var concatParts []hclwrite.Tokens for _, block := range repSpecBlocks { - tokens, err := processReplicationSpecBlock(block, root, true) + tokens, err := processReplicationSpecBlock(block, root) if err != nil { return nil, err } @@ -475,14 +404,12 @@ func processVariableReplicationSpecs(repSpecBlocks []*hclwrite.Block, root attrV return concatParts, nil } -func processStaticReplicationSpecs(resourceb *hclwrite.Body, repSpecBlocks []*hclwrite.Block, - root attrVals) (hclwrite.Tokens, error) { +func processStaticRepSpecs(repSpecBlocks []*hclwrite.Block, root attrVals) (hclwrite.Tokens, error) { var specbs []*hclwrite.Body for _, block := range repSpecBlocks { spec := hclwrite.NewEmptyFile() specb := spec.Body() specbSrc := block.Body() - d, err := fillWithDynamicRegionConfigs(specbSrc, root, false) if err != nil { return nil, err @@ -492,20 +419,15 @@ func processStaticReplicationSpecs(resourceb *hclwrite.Body, repSpecBlocks []*hc // This is complex, return the dynamic block as is return d.tokens, nil } - _ = hcl.MoveAttr(specbSrc, specb, nZoneName, nZoneName, errRepSpecs) - shardsAttr := specbSrc.GetAttribute(nNumShards) if shardsAttr == nil { return nil, fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) } - shardsVal, _ := hcl.GetAttrInt(shardsAttr, errNumShards) - if errConfig := fillRegionConfigs(specb, specbSrc, root); errConfig != nil { return nil, errConfig } - for range shardsVal { specbs = append(specbs, specb) } @@ -513,11 +435,10 @@ func processStaticReplicationSpecs(resourceb *hclwrite.Body, repSpecBlocks []*hc return hcl.TokensArray(specbs), nil } -func processReplicationSpecBlock(block *hclwrite.Block, root attrVals, isVariable bool) (hclwrite.Tokens, error) { +func processReplicationSpecBlock(block *hclwrite.Block, root attrVals) (hclwrite.Tokens, error) { spec := hclwrite.NewEmptyFile() specb := spec.Body() specbSrc := block.Body() - d, err := fillWithDynamicRegionConfigs(specbSrc, root, false) if err != nil { return nil, err @@ -525,17 +446,14 @@ func processReplicationSpecBlock(block *hclwrite.Block, root attrVals, isVariabl if d.IsPresent() { return d.tokens, nil } - _ = hcl.MoveAttr(specbSrc, specb, nZoneName, nZoneName, errRepSpecs) shardsAttr := specbSrc.GetAttribute(nNumShards) if shardsAttr == nil { return nil, fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) } - if errConfig := fillRegionConfigs(specb, specbSrc, root); errConfig != nil { return nil, errConfig } - return processNumShards(shardsAttr, specb) } diff --git a/internal/convert/shared.go b/internal/convert/shared.go index d1501b6..3e6563b 100644 --- a/internal/convert/shared.go +++ b/internal/convert/shared.go @@ -223,3 +223,73 @@ func buildForExpr(varName, collection string, trailingSpace bool) string { } return expr } + +func fillTagsLabelsOpt(resourceb *hclwrite.Body, name string) error { + tokensDynamic, err := extractTagsLabelsDynamicBlock(resourceb, name) + if err != nil { + return err + } + tokensIndividual, err := extractTagsLabelsIndividual(resourceb, name) + if err != nil { + return err + } + if tokensDynamic != nil && tokensIndividual != nil { + resourceb.SetAttributeRaw(name, hcl.TokensFuncMerge(tokensDynamic, tokensIndividual)) + return nil + } + if tokensDynamic != nil { + resourceb.SetAttributeRaw(name, tokensDynamic) + } + if tokensIndividual != nil { + resourceb.SetAttributeRaw(name, tokensIndividual) + } + return nil +} + +func extractTagsLabelsDynamicBlock(resourceb *hclwrite.Body, name string) (hclwrite.Tokens, error) { + d, err := getDynamicBlock(resourceb, name) + if err != nil || !d.IsPresent() { + return nil, err + } + key := d.content.Body().GetAttribute(nKey) + value := d.content.Body().GetAttribute(nValue) + if key == nil || value == nil { + return nil, fmt.Errorf("dynamic block %s: %s or %s not found", name, nKey, nValue) + } + keyExpr := replaceDynamicBlockExpr(key, name, nKey) + valueExpr := replaceDynamicBlockExpr(value, name, nValue) + collectionExpr := hcl.GetAttrExpr(d.forEach) + forExpr := fmt.Sprintf("for key, value in %s : %s => %s", collectionExpr, keyExpr, valueExpr) + tokens := hcl.EncloseBraces(hcl.EncloseNewLines(hcl.TokensFromExpr(forExpr)), false) + if keyExpr == nKey && valueExpr == nValue { // expression can be simplified and use for_each expression + tokens = hcl.TokensFromExpr(collectionExpr) + } + resourceb.RemoveBlock(d.block) + return tokens, nil +} + +func extractTagsLabelsIndividual(resourceb *hclwrite.Body, name string) (hclwrite.Tokens, error) { + var ( + file = hclwrite.NewEmptyFile() + fileb = file.Body() + found = false + ) + for { + block := resourceb.FirstMatchingBlock(name, nil) + if block == nil { + break + } + key := block.Body().GetAttribute(nKey) + value := block.Body().GetAttribute(nValue) + if key == nil || value == nil { + return nil, fmt.Errorf("%s: %s or %s not found", name, nKey, nValue) + } + setKeyValue(fileb, key, value) + resourceb.RemoveBlock(block) + found = true + } + if !found { + return nil, nil + } + return hcl.TokensObject(fileb), nil +} From 6255619d5f54e330cc0bdc3f386993849bdfaa75 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Sat, 23 Aug 2025 12:05:47 +0200 Subject: [PATCH 29/43] refactor fillReplicationSpecs --- internal/convert/clu2adv.go | 113 +++++++++++++----------------------- 1 file changed, 39 insertions(+), 74 deletions(-) diff --git a/internal/convert/clu2adv.go b/internal/convert/clu2adv.go index 8ddc777..8976f75 100644 --- a/internal/convert/clu2adv.go +++ b/internal/convert/clu2adv.go @@ -178,20 +178,50 @@ func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error { if len(repSpecBlocks) == 0 { return fmt.Errorf("%s: no replication_specs found", errRepSpecs) } - if hasVariableNumShards(repSpecBlocks) { - tokens, err := processVariableRepSpecs(repSpecBlocks, root) - if err != nil { - return err + dConfig, err := fillWithDynamicRegionConfigs(repSpecBlocks[0].Body(), root, false) + if err != nil { + return err + } + if dConfig.IsPresent() { + resourceb.SetAttributeRaw(nRepSpecs, dConfig.tokens) + return nil + } + hasVariableShards := hasVariableNumShards(repSpecBlocks) + var resultTokens []hclwrite.Tokens + var resultBodies []*hclwrite.Body + for _, block := range repSpecBlocks { + spec := hclwrite.NewEmptyFile() + specb := spec.Body() + specbSrc := block.Body() + _ = hcl.MoveAttr(specbSrc, specb, nZoneName, nZoneName, errRepSpecs) + shardsAttr := specbSrc.GetAttribute(nNumShards) + if shardsAttr == nil { + return fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) } - resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensFuncConcat(tokens...)) - } else { - tokens, err := processStaticRepSpecs(repSpecBlocks, root) + if errConfig := fillRegionConfigs(specb, specbSrc, root); errConfig != nil { + return errConfig + } + if hasVariableShards { + tokens, err := processNumShards(shardsAttr, specb) + if err != nil { + return err + } + resultTokens = append(resultTokens, tokens) + continue + } + shardsVal, err := hcl.GetAttrInt(shardsAttr, errNumShards) if err != nil { return err } - resourceb.SetAttributeRaw(nRepSpecs, tokens) + for range shardsVal { + resultBodies = append(resultBodies, specb) + } + } + if hasVariableShards { + resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensFuncConcat(resultTokens...)) + } else { + resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensArray(resultBodies)) } - return nil } @@ -392,71 +422,6 @@ func getDynamicBlockRegionArray(forEach string, configSrc *hclwrite.Block, root return hcl.EncloseBracketsNewLines(tokens), nil } -func processVariableRepSpecs(repSpecBlocks []*hclwrite.Block, root attrVals) ([]hclwrite.Tokens, error) { - var concatParts []hclwrite.Tokens - for _, block := range repSpecBlocks { - tokens, err := processReplicationSpecBlock(block, root) - if err != nil { - return nil, err - } - concatParts = append(concatParts, tokens) - } - return concatParts, nil -} - -func processStaticRepSpecs(repSpecBlocks []*hclwrite.Block, root attrVals) (hclwrite.Tokens, error) { - var specbs []*hclwrite.Body - for _, block := range repSpecBlocks { - spec := hclwrite.NewEmptyFile() - specb := spec.Body() - specbSrc := block.Body() - d, err := fillWithDynamicRegionConfigs(specbSrc, root, false) - if err != nil { - return nil, err - } - if d.IsPresent() { - // For dynamic blocks that have numerical num_shards - // This is complex, return the dynamic block as is - return d.tokens, nil - } - _ = hcl.MoveAttr(specbSrc, specb, nZoneName, nZoneName, errRepSpecs) - shardsAttr := specbSrc.GetAttribute(nNumShards) - if shardsAttr == nil { - return nil, fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) - } - shardsVal, _ := hcl.GetAttrInt(shardsAttr, errNumShards) - if errConfig := fillRegionConfigs(specb, specbSrc, root); errConfig != nil { - return nil, errConfig - } - for range shardsVal { - specbs = append(specbs, specb) - } - } - return hcl.TokensArray(specbs), nil -} - -func processReplicationSpecBlock(block *hclwrite.Block, root attrVals) (hclwrite.Tokens, error) { - spec := hclwrite.NewEmptyFile() - specb := spec.Body() - specbSrc := block.Body() - d, err := fillWithDynamicRegionConfigs(specbSrc, root, false) - if err != nil { - return nil, err - } - if d.IsPresent() { - return d.tokens, nil - } - _ = hcl.MoveAttr(specbSrc, specb, nZoneName, nZoneName, errRepSpecs) - shardsAttr := specbSrc.GetAttribute(nNumShards) - if shardsAttr == nil { - return nil, fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) - } - if errConfig := fillRegionConfigs(specb, specbSrc, root); errConfig != nil { - return nil, errConfig - } - return processNumShards(shardsAttr, specb) -} - func sortConfigsByPriority(configs []*hclwrite.Body) []*hclwrite.Body { for _, config := range configs { if _, err := hcl.GetAttrInt(config.GetAttribute(nPriority), errPriority); err != nil { From 54645dfe34e112b8be08b969a15ce5ca1ee2fb4e Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Sat, 23 Aug 2025 21:17:10 +0200 Subject: [PATCH 30/43] refactor fillReplicationSpecs --- internal/convert/adv2v2.go | 6 +----- internal/convert/clu2adv.go | 6 +----- internal/convert/shared.go | 8 ++++---- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index 4019994..74c860a 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -111,11 +111,7 @@ func processVariableNumShards(repSpecBlocks []*hclwrite.Block, diskSizeGB hclwri if err := convertConfig(blockb, diskSizeGB); err != nil { return nil, err } - tokens, err := processNumShards(numShardsAttr, blockb) - if err != nil { - return nil, err - } - concatParts = append(concatParts, tokens) + concatParts = append(concatParts, processNumShards(numShardsAttr, blockb)) } return hcl.TokensFuncConcat(concatParts...), nil } diff --git a/internal/convert/clu2adv.go b/internal/convert/clu2adv.go index 8976f75..da41b64 100644 --- a/internal/convert/clu2adv.go +++ b/internal/convert/clu2adv.go @@ -202,11 +202,7 @@ func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error { return errConfig } if hasVariableShards { - tokens, err := processNumShards(shardsAttr, specb) - if err != nil { - return err - } - resultTokens = append(resultTokens, tokens) + resultTokens = append(resultTokens, processNumShards(shardsAttr, specb)) continue } shardsVal, err := hcl.GetAttrInt(shardsAttr, errNumShards) diff --git a/internal/convert/shared.go b/internal/convert/shared.go index 3e6563b..1acffee 100644 --- a/internal/convert/shared.go +++ b/internal/convert/shared.go @@ -23,21 +23,21 @@ func hasVariableNumShards(blocks []*hclwrite.Block) bool { // processNumShards handles num_shards for a block, returning tokens for the expanded specs. // processedBody is the body with num_shards removed and other processing done. -func processNumShards(shardsAttr *hclwrite.Attribute, processedBody *hclwrite.Body) (hclwrite.Tokens, error) { +func processNumShards(shardsAttr *hclwrite.Attribute, processedBody *hclwrite.Body) hclwrite.Tokens { if shardsAttr == nil { - return hcl.TokensArraySingle(processedBody), nil // Default 1 if no num_shards specified + return hcl.TokensArraySingle(processedBody) // Default 1 if no num_shards specified } if shardsVal, err := hcl.GetAttrInt(shardsAttr, errNumShards); err == nil { var bodies []*hclwrite.Body for range shardsVal { bodies = append(bodies, processedBody) } - return hcl.TokensArray(bodies), nil + return hcl.TokensArray(bodies) } shardsExpr := hcl.GetAttrExpr(shardsAttr) tokens := hcl.TokensFromExpr(buildForExpr("i", fmt.Sprintf("range(%s)", shardsExpr), false)) tokens = append(tokens, hcl.TokensObject(processedBody)...) - return hcl.EncloseBracketsNewLines(tokens), nil + return hcl.EncloseBracketsNewLines(tokens) } type dynamicBlock struct { From 4a1bb60762eff30b18ebac7550b82f032f4a34fb Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Sat, 23 Aug 2025 22:17:27 +0200 Subject: [PATCH 31/43] inline back some functions in adv2v2 --- internal/convert/adv2v2.go | 448 +++++++++++++++++++------------------ internal/convert/shared.go | 67 ------ 2 files changed, 229 insertions(+), 286 deletions(-) diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index 74c860a..678e047 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -65,202 +65,186 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error return err } if dSpec.IsPresent() { - return convertDynamicRepSpecs(resourceb, dSpec, diskSizeGB) - } - repSpecBlocks := collectBlocks(resourceb, nRepSpecs) - if len(repSpecBlocks) == 0 { - return fmt.Errorf("must have at least one replication_specs") - } - var tokens hclwrite.Tokens - if hasVariableNumShards(repSpecBlocks) { - tokens, err = processVariableNumShards(repSpecBlocks, diskSizeGB) - } else { - tokens, err = processStaticNumShards(repSpecBlocks, diskSizeGB) - } - if err != nil { - return err - } - resourceb.SetAttributeRaw(nRepSpecs, tokens) - return nil -} - -func convertDynamicRepSpecs(resourceb *hclwrite.Body, dSpec dynamicBlock, diskSizeGB hclwrite.Tokens) error { - transformDynamicBlockReferences(dSpec.content.Body(), nRepSpecs, nSpec) - dConfig, err := findDynamicConfigBlock(dSpec.content.Body()) - if err != nil { - return err - } - if dConfig.IsPresent() { - return convertDynamicRepSpecsWithDynamicConfig(resourceb, dSpec, dConfig, diskSizeGB) - } - tokens, err := processDynamicRepSpecsWithoutConfig(dSpec, diskSizeGB) - if err != nil { - return err - } - resourceb.RemoveBlock(dSpec.block) - resourceb.SetAttributeRaw(nRepSpecs, tokens) - return nil -} - -func processVariableNumShards(repSpecBlocks []*hclwrite.Block, diskSizeGB hclwrite.Tokens) (hclwrite.Tokens, error) { - var concatParts []hclwrite.Tokens - for _, block := range repSpecBlocks { - blockb := block.Body() - numShardsAttr := blockb.GetAttribute(nNumShards) - blockb.RemoveAttribute(nNumShards) - if err := convertConfig(blockb, diskSizeGB); err != nil { - return nil, err - } - concatParts = append(concatParts, processNumShards(numShardsAttr, blockb)) - } - return hcl.TokensFuncConcat(concatParts...), nil -} - -func processStaticNumShards(repSpecBlocks []*hclwrite.Block, diskSizeGB hclwrite.Tokens) (hclwrite.Tokens, error) { - var repSpecs []*hclwrite.Body - for _, block := range repSpecBlocks { - blockb := block.Body() - numShardsAttr := blockb.GetAttribute(nNumShards) - blockb.RemoveAttribute(nNumShards) - if err := convertConfig(blockb, diskSizeGB); err != nil { - return nil, err + // Inline convertDynamicRepSpecs + transformDynamicBlockReferences(dSpec.content.Body(), nRepSpecs, nSpec) + // Inline findDynamicConfigBlock + dConfig, err := getDynamicBlock(dSpec.content.Body(), nConfig) + if err != nil { + return err } - if numShardsAttr != nil { - numShardsVal, _ := hcl.GetAttrInt(numShardsAttr, errNumShards) - for range numShardsVal { - repSpecs = append(repSpecs, blockb) + if !dConfig.IsPresent() { + dConfig, err = getDynamicBlock(dSpec.content.Body(), nConfigSrc) + if err != nil { + return err } - } else { - repSpecs = append(repSpecs, blockb) } - } - return hcl.TokensArray(repSpecs), nil -} - -func findDynamicConfigBlock(body *hclwrite.Body) (dynamicBlock, error) { - dConfig, err := getDynamicBlock(body, nConfig) - if err != nil { - return dynamicBlock{}, err - } - if !dConfig.IsPresent() { - dConfig, err = getDynamicBlock(body, nConfigSrc) - if err != nil { - return dynamicBlock{}, err - } - } - return dConfig, nil -} - -func processDynamicRepSpecsWithoutConfig(dSpec dynamicBlock, diskSizeGB hclwrite.Tokens) (hclwrite.Tokens, error) { - numShardsAttr := dSpec.content.Body().GetAttribute(nNumShards) - dSpec.content.Body().RemoveAttribute(nNumShards) - if err := convertConfig(dSpec.content.Body(), diskSizeGB); err != nil { - return nil, err - } - if numShardsAttr != nil { - return buildDynamicRepSpecsWithShards(dSpec, numShardsAttr) - } - return buildSimpleDynamicRepSpecs(dSpec) -} - -func buildDynamicRepSpecsWithShards(dSpec dynamicBlock, numShardsAttr *hclwrite.Attribute) (hclwrite.Tokens, error) { - numShardsExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec) - outerFor := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach), false) - innerFor := buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr), false) - forExpr := fmt.Sprintf("%s [\n %s ", outerFor, innerFor) - tokens := hcl.TokensFromExpr(forExpr) - tokens = append(tokens, hcl.TokensObject(dSpec.content.Body())...) - tokens = append(tokens, hcl.TokensFromExpr("\n ]\n]")...) - return hcl.TokensFuncFlatten(tokens), nil -} - -func buildSimpleDynamicRepSpecs(dSpec dynamicBlock) (hclwrite.Tokens, error) { - forExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach), false) - tokens := hcl.TokensFromExpr(forExpr) - tokens = append(tokens, hcl.TokensObject(dSpec.content.Body())...) - return hcl.EncloseBracketsNewLines(tokens), nil -} - -func buildDynamicRepSpecsWithNumShards(dSpec, dConfig dynamicBlock, diskSizeGB hclwrite.Tokens, - configBlockName string, numShardsAttr *hclwrite.Attribute) (hclwrite.Tokens, error) { - numShardsExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec) - transformDynamicBlockReferencesRecursive(dConfig.content.Body(), configBlockName, nRegion) - transform := func(expr string) string { - return replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) - } - transformAttributesSorted(dConfig.content.Body(), dConfig.content.Body().Attributes(), transform) - for _, block := range dConfig.content.Body().Blocks() { - transformAttributesSorted(block.Body(), block.Body().Attributes(), transform) - } - regionConfigBody := buildRegionConfigBody(dConfig, diskSizeGB) - - configForEach := fmt.Sprintf("%s.%s", nSpec, nConfig) - regionForExpr := buildForExpr(nRegion, configForEach, false) - regionTokens := hcl.TokensFromExpr(regionForExpr) - regionTokens = append(regionTokens, hcl.TokensObject(regionConfigBody)...) + if dConfig.IsPresent() { + // Will inline convertDynamicRepSpecsWithDynamicConfig + configBlockName := getResourceName(dConfig.block) + numShardsAttr := dSpec.content.Body().GetAttribute(nNumShards) + if numShardsAttr != nil { + // Inline buildDynamicRepSpecsWithNumShards + numShardsExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec) + // Inline transformDynamicBlockReferencesRecursive for dConfig + transform1 := func(expr string) string { + return replaceDynamicBlockReferences(expr, configBlockName, nRegion) + } + transformAttributesSorted(dConfig.content.Body(), dConfig.content.Body().Attributes(), transform1) + for _, block := range dConfig.content.Body().Blocks() { + // Recursive call inlined + transformAttributesSorted(block.Body(), block.Body().Attributes(), transform1) + } + transform2 := func(expr string) string { + return replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) + } + transformAttributesSorted(dConfig.content.Body(), dConfig.content.Body().Attributes(), transform2) + for _, block := range dConfig.content.Body().Blocks() { + transformAttributesSorted(block.Body(), block.Body().Attributes(), transform2) + } + // Inline buildRegionConfigBody + regionConfigFile := hclwrite.NewEmptyFile() + regionConfigBody := regionConfigFile.Body() + copyAttributesSorted(regionConfigBody, dConfig.content.Body().Attributes()) + // Inline processRegionConfigBlocks + for _, block := range dConfig.content.Body().Blocks() { + blockType := block.Type() + blockFile := hclwrite.NewEmptyFile() + blockBody := blockFile.Body() + copyAttributesSorted(blockBody, block.Body().Attributes()) + if diskSizeGB != nil && (blockType == nElectableSpecs || + blockType == nReadOnlySpecs || blockType == nAnalyticsSpecs) { + blockBody.SetAttributeRaw(nDiskSizeGB, diskSizeGB) + } + regionConfigBody.SetAttributeRaw(blockType, hcl.TokensObject(blockBody)) + } - repSpecBody := buildRepSpecBody(dSpec, regionTokens) + configForEach := fmt.Sprintf("%s.%s", nSpec, nConfig) + regionForExpr := buildForExpr(nRegion, configForEach, false) + regionTokens := hcl.TokensFromExpr(regionForExpr) + regionTokens = append(regionTokens, hcl.TokensObject(regionConfigBody)...) - // Inline buildInnerForExpr - innerForExpr := buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr), false) - innerTokens := hcl.TokensFromExpr(innerForExpr) - innerTokens = append(innerTokens, hcl.TokensObject(repSpecBody)...) + // Inline buildRepSpecBody + repSpecFile := hclwrite.NewEmptyFile() + repSpecBody := repSpecFile.Body() + if zoneNameAttr := dSpec.content.Body().GetAttribute(nZoneName); zoneNameAttr != nil { + zoneNameExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(zoneNameAttr), nRepSpecs, nSpec) + repSpecBody.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(zoneNameExpr)) + } + repSpecBody.SetAttributeRaw(nConfig, hcl.EncloseBracketsNewLines(regionTokens)) - // Inline buildOuterForExpr - outerForExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach), true) - outerTokens := hcl.TokensFromExpr(outerForExpr) - outerTokens = append(outerTokens, hcl.EncloseBracketsNewLines(innerTokens)...) + // Inline buildInnerForExpr + innerForExpr := buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr), false) + innerTokens := hcl.TokensFromExpr(innerForExpr) + innerTokens = append(innerTokens, hcl.TokensObject(repSpecBody)...) - return hcl.TokensFuncFlatten(outerTokens), nil -} - -func buildRegionConfigBody(dConfig dynamicBlock, diskSizeGB hclwrite.Tokens) *hclwrite.Body { - regionConfigFile := hclwrite.NewEmptyFile() - regionConfigBody := regionConfigFile.Body() - copyAttributesSorted(regionConfigBody, dConfig.content.Body().Attributes()) - processRegionConfigBlocks(regionConfigBody, dConfig.content.Body().Blocks(), diskSizeGB) - return regionConfigBody -} + // Inline buildOuterForExpr + outerForExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach), true) + outerTokens := hcl.TokensFromExpr(outerForExpr) + outerTokens = append(outerTokens, hcl.EncloseBracketsNewLines(innerTokens)...) -func buildRepSpecBody(dSpec dynamicBlock, regionTokens hclwrite.Tokens) *hclwrite.Body { - repSpecFile := hclwrite.NewEmptyFile() - repSpecBody := repSpecFile.Body() - if zoneNameAttr := dSpec.content.Body().GetAttribute(nZoneName); zoneNameAttr != nil { - zoneNameExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(zoneNameAttr), nRepSpecs, nSpec) - repSpecBody.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(zoneNameExpr)) - } - repSpecBody.SetAttributeRaw(nConfig, hcl.EncloseBracketsNewLines(regionTokens)) - return repSpecBody -} - -func processRegionConfigBlocks(targetBody *hclwrite.Body, blocks []*hclwrite.Block, diskSizeGB hclwrite.Tokens) { - for _, block := range blocks { - blockType := block.Type() - blockFile := hclwrite.NewEmptyFile() - blockBody := blockFile.Body() - copyAttributesSorted(blockBody, block.Body().Attributes()) - if diskSizeGB != nil && (blockType == nElectableSpecs || - blockType == nReadOnlySpecs || blockType == nAnalyticsSpecs) { - blockBody.SetAttributeRaw(nDiskSizeGB, diskSizeGB) + tokens := hcl.TokensFuncFlatten(outerTokens) + resourceb.RemoveBlock(dSpec.block) + resourceb.SetAttributeRaw(nRepSpecs, tokens) + return nil + } + // Will inline convertDynamicRepSpecsWithoutNumShards + repSpecFile := hclwrite.NewEmptyFile() + repSpecb := repSpecFile.Body() + if zoneNameAttr := dSpec.content.Body().GetAttribute(nZoneName); zoneNameAttr != nil { + zoneNameExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(zoneNameAttr), nRepSpecs, nSpec) + repSpecb.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(zoneNameExpr)) + } + configFile := hclwrite.NewEmptyFile() + configb := configFile.Body() + addAttributesWithTransform(configb, dConfig.content.Body().Attributes(), configBlockName) + for _, block := range dConfig.content.Body().Blocks() { + newBlock := configb.AppendNewBlock(block.Type(), block.Labels()) + newBlockb := newBlock.Body() + addAttributesWithTransform(newBlockb, block.Body().Attributes(), configBlockName) + } + processAllSpecs(configb, diskSizeGB) + configForEach := fmt.Sprintf("%s.%s", nSpec, nConfig) + regionForExpr := buildForExpr(nRegion, configForEach, false) + regionTokens := hcl.TokensFromExpr(regionForExpr) + regionTokens = append(regionTokens, hcl.TokensObject(configb)...) + repSpecb.SetAttributeRaw(nConfig, hcl.EncloseBracketsNewLines(regionTokens)) + forExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach), true) + innerTokens := hcl.TokensFromExpr(forExpr) + innerTokens = append(innerTokens, hcl.TokensArraySingle(repSpecb)...) + tokens := hcl.TokensFuncFlatten(innerTokens) + resourceb.RemoveBlock(dSpec.block) + resourceb.SetAttributeRaw(nRepSpecs, tokens) + return nil } - targetBody.SetAttributeRaw(blockType, hcl.TokensObject(blockBody)) - } -} - -func convertDynamicRepSpecsWithDynamicConfig(resourceb *hclwrite.Body, dSpec, dConfig dynamicBlock, - diskSizeGB hclwrite.Tokens) error { - configBlockName := getResourceName(dConfig.block) - numShardsAttr := dSpec.content.Body().GetAttribute(nNumShards) - if numShardsAttr != nil { - tokens, err := buildDynamicRepSpecsWithNumShards(dSpec, dConfig, diskSizeGB, configBlockName, numShardsAttr) - if err != nil { + // Inline processDynamicRepSpecsWithoutConfig + numShardsAttr := dSpec.content.Body().GetAttribute(nNumShards) + dSpec.content.Body().RemoveAttribute(nNumShards) + if err := convertConfig(dSpec.content.Body(), diskSizeGB); err != nil { return err } + var tokens hclwrite.Tokens + if numShardsAttr != nil { + // Inline buildDynamicRepSpecsWithShards + numShardsExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec) + outerFor := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach), false) + innerFor := buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr), false) + forExpr := fmt.Sprintf("%s [\n %s ", outerFor, innerFor) + tokens = hcl.TokensFromExpr(forExpr) + tokens = append(tokens, hcl.TokensObject(dSpec.content.Body())...) + tokens = append(tokens, hcl.TokensFromExpr("\n ]\n]")...) + tokens = hcl.TokensFuncFlatten(tokens) + } else { + // Inline buildSimpleDynamicRepSpecs + forExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach), false) + tokens = hcl.TokensFromExpr(forExpr) + tokens = append(tokens, hcl.TokensObject(dSpec.content.Body())...) + tokens = hcl.EncloseBracketsNewLines(tokens) + } resourceb.RemoveBlock(dSpec.block) resourceb.SetAttributeRaw(nRepSpecs, tokens) return nil } - return convertDynamicRepSpecsWithoutNumShards(resourceb, dSpec, dConfig, diskSizeGB, configBlockName) + repSpecBlocks := collectBlocks(resourceb, nRepSpecs) + if len(repSpecBlocks) == 0 { + return fmt.Errorf("must have at least one replication_specs") + } + var tokens hclwrite.Tokens + if hasVariableNumShards(repSpecBlocks) { + // Inline processVariableNumShards + var concatParts []hclwrite.Tokens + for _, block := range repSpecBlocks { + blockb := block.Body() + numShardsAttr := blockb.GetAttribute(nNumShards) + blockb.RemoveAttribute(nNumShards) + if err := convertConfig(blockb, diskSizeGB); err != nil { + return err + } + concatParts = append(concatParts, processNumShards(numShardsAttr, blockb)) + } + tokens = hcl.TokensFuncConcat(concatParts...) + } else { + // Inline processStaticNumShards + var repSpecs []*hclwrite.Body + for _, block := range repSpecBlocks { + blockb := block.Body() + numShardsAttr := blockb.GetAttribute(nNumShards) + blockb.RemoveAttribute(nNumShards) + if err := convertConfig(blockb, diskSizeGB); err != nil { + return err + } + if numShardsAttr != nil { + numShardsVal, _ := hcl.GetAttrInt(numShardsAttr, errNumShards) + for range numShardsVal { + repSpecs = append(repSpecs, blockb) + } + } else { + repSpecs = append(repSpecs, blockb) + } + } + tokens = hcl.TokensArray(repSpecs) + } + resourceb.SetAttributeRaw(nRepSpecs, tokens) + return nil } func addAttributesWithTransform(targetBody *hclwrite.Body, sourceAttrs map[string]*hclwrite.Attribute, @@ -274,48 +258,11 @@ func addAttributesWithTransform(targetBody *hclwrite.Body, sourceAttrs map[strin transformAttributesSorted(targetBody, sourceAttrs, transform1, transform2) } -func convertDynamicRepSpecsWithoutNumShards(resourceb *hclwrite.Body, dSpec, dConfig dynamicBlock, - diskSizeGB hclwrite.Tokens, configBlockName string) error { - repSpecFile := hclwrite.NewEmptyFile() - repSpecb := repSpecFile.Body() - if zoneNameAttr := dSpec.content.Body().GetAttribute(nZoneName); zoneNameAttr != nil { - zoneNameExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(zoneNameAttr), nRepSpecs, nSpec) - repSpecb.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(zoneNameExpr)) - } - configFile := hclwrite.NewEmptyFile() - configb := configFile.Body() - addAttributesWithTransform(configb, dConfig.content.Body().Attributes(), configBlockName) - for _, block := range dConfig.content.Body().Blocks() { - newBlock := configb.AppendNewBlock(block.Type(), block.Labels()) - newBlockb := newBlock.Body() - addAttributesWithTransform(newBlockb, block.Body().Attributes(), configBlockName) - } - processAllSpecs(configb, diskSizeGB) - configForEach := fmt.Sprintf("%s.%s", nSpec, nConfig) - regionForExpr := buildForExpr(nRegion, configForEach, false) - regionTokens := hcl.TokensFromExpr(regionForExpr) - regionTokens = append(regionTokens, hcl.TokensObject(configb)...) - repSpecb.SetAttributeRaw(nConfig, hcl.EncloseBracketsNewLines(regionTokens)) - forExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach), true) - innerTokens := hcl.TokensFromExpr(forExpr) - innerTokens = append(innerTokens, hcl.TokensArraySingle(repSpecb)...) - tokens := hcl.TokensFuncFlatten(innerTokens) - resourceb.RemoveBlock(dSpec.block) - resourceb.SetAttributeRaw(nRepSpecs, tokens) - return nil -} - func convertConfig(repSpecs *hclwrite.Body, diskSizeGB hclwrite.Tokens) error { dConfig, err := getDynamicBlock(repSpecs, nConfig) if err != nil { return err } - if !dConfig.IsPresent() { - dConfig, err = getDynamicBlock(repSpecs, nConfigSrc) - if err != nil { - return err - } - } if dConfig.IsPresent() { return convertDynamicConfig(repSpecs, dConfig, diskSizeGB) } @@ -334,7 +281,15 @@ func convertConfig(repSpecs *hclwrite.Body, diskSizeGB hclwrite.Tokens) error { func convertDynamicConfig(repSpecs *hclwrite.Body, dConfig dynamicBlock, diskSizeGB hclwrite.Tokens) error { blockName := getResourceName(dConfig.block) - transformDynamicBlockReferencesRecursive(dConfig.content.Body(), blockName, nRegion) + // Inline transformDynamicBlockReferencesRecursive + transform := func(expr string) string { + return replaceDynamicBlockReferences(expr, blockName, nRegion) + } + transformAttributesSorted(dConfig.content.Body(), dConfig.content.Body().Attributes(), transform) + for _, block := range dConfig.content.Body().Blocks() { + // Recursive call inlined + transformAttributesSorted(block.Body(), block.Body().Attributes(), transform) + } processAllSpecs(dConfig.content.Body(), diskSizeGB) forExpr := buildForExpr(nRegion, hcl.GetAttrExpr(dConfig.forEach), false) tokens := hcl.TokensFromExpr(forExpr) @@ -357,3 +312,58 @@ func hasExpectedBlocksAsAttributes(resourceb *hclwrite.Body) bool { } return false } + +// copyAttributesSorted copies attributes from source to target in sorted order for deterministic output +func copyAttributesSorted(targetBody *hclwrite.Body, sourceAttrs map[string]*hclwrite.Attribute) { + var names []string + for name := range sourceAttrs { + names = append(names, name) + } + slices.Sort(names) + for _, name := range names { + attr := sourceAttrs[name] + targetBody.SetAttributeRaw(name, hcl.TokensFromExpr(hcl.GetAttrExpr(attr))) + } +} + +// transformAttributesSorted transforms and copies attributes in sorted order +func transformAttributesSorted(targetBody *hclwrite.Body, sourceAttrs map[string]*hclwrite.Attribute, + transforms ...func(string) string) { + var names []string + for name := range sourceAttrs { + names = append(names, name) + } + slices.Sort(names) + for _, name := range names { + attr := sourceAttrs[name] + expr := hcl.GetAttrExpr(attr) + // Apply all transformations + for _, transform := range transforms { + expr = transform(expr) + } + targetBody.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) + } +} + +// processAllSpecs processes all spec blocks (electable, read_only, analytics) and auto_scaling blocks +func processAllSpecs(body *hclwrite.Body, diskSizeGB hclwrite.Tokens) { + fillSpecOpt(body, nElectableSpecs, diskSizeGB) + fillSpecOpt(body, nReadOnlySpecs, diskSizeGB) + fillSpecOpt(body, nAnalyticsSpecs, diskSizeGB) + fillSpecOpt(body, nAutoScaling, nil) + fillSpecOpt(body, nAnalyticsAutoScaling, nil) +} + +// fillSpecOpt converts a spec block to an attribute with object value and optionally adds disk_size_gb +func fillSpecOpt(resourceb *hclwrite.Body, name string, diskSizeGBTokens hclwrite.Tokens) { + block := resourceb.FirstMatchingBlock(name, nil) + if block == nil { + return + } + if diskSizeGBTokens != nil { + blockb := block.Body() + blockb.RemoveAttribute(nDiskSizeGB) + blockb.SetAttributeRaw(nDiskSizeGB, diskSizeGBTokens) + } + fillBlockOpt(resourceb, name) +} diff --git a/internal/convert/shared.go b/internal/convert/shared.go index 1acffee..92dfd1d 100644 --- a/internal/convert/shared.go +++ b/internal/convert/shared.go @@ -109,18 +109,6 @@ func transformDynamicBlockReferences(configSrcb *hclwrite.Body, blockName, varNa } } -// transformDynamicBlockReferencesRecursive transforms attributes and nested blocks recursively -// replacing references from dynamic block format, e.g. regions_config.value.* to region.* -func transformDynamicBlockReferencesRecursive(body *hclwrite.Body, blockName, varName string) { - transform := func(expr string) string { - return replaceDynamicBlockReferences(expr, blockName, varName) - } - transformAttributesSorted(body, body.Attributes(), transform) - for _, block := range body.Blocks() { - transformDynamicBlockReferencesRecursive(block.Body(), blockName, varName) - } -} - // collectBlocks removes and returns all blocks of the given name from body in order of appearance. func collectBlocks(body *hclwrite.Body, name string) []*hclwrite.Block { var blocks []*hclwrite.Block @@ -160,61 +148,6 @@ func fillAdvConfigOpt(resourceb *hclwrite.Body) { fillBlockOpt(resourceb, nAdvConfig) } -// copyAttributesSorted copies attributes from source to target in sorted order for deterministic output -func copyAttributesSorted(targetBody *hclwrite.Body, sourceAttrs map[string]*hclwrite.Attribute) { - var names []string - for name := range sourceAttrs { - names = append(names, name) - } - slices.Sort(names) - for _, name := range names { - attr := sourceAttrs[name] - targetBody.SetAttributeRaw(name, hcl.TokensFromExpr(hcl.GetAttrExpr(attr))) - } -} - -// transformAttributesSorted transforms and copies attributes in sorted order -func transformAttributesSorted(targetBody *hclwrite.Body, sourceAttrs map[string]*hclwrite.Attribute, - transforms ...func(string) string) { - var names []string - for name := range sourceAttrs { - names = append(names, name) - } - slices.Sort(names) - for _, name := range names { - attr := sourceAttrs[name] - expr := hcl.GetAttrExpr(attr) - // Apply all transformations - for _, transform := range transforms { - expr = transform(expr) - } - targetBody.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) - } -} - -// processAllSpecs processes all spec blocks (electable, read_only, analytics) and auto_scaling blocks -func processAllSpecs(body *hclwrite.Body, diskSizeGB hclwrite.Tokens) { - fillSpecOpt(body, nElectableSpecs, diskSizeGB) - fillSpecOpt(body, nReadOnlySpecs, diskSizeGB) - fillSpecOpt(body, nAnalyticsSpecs, diskSizeGB) - fillSpecOpt(body, nAutoScaling, nil) - fillSpecOpt(body, nAnalyticsAutoScaling, nil) -} - -// fillSpecOpt converts a spec block to an attribute with object value and optionally adds disk_size_gb -func fillSpecOpt(resourceb *hclwrite.Body, name string, diskSizeGBTokens hclwrite.Tokens) { - block := resourceb.FirstMatchingBlock(name, nil) - if block == nil { - return - } - if diskSizeGBTokens != nil { - blockb := block.Body() - blockb.RemoveAttribute(nDiskSizeGB) - blockb.SetAttributeRaw(nDiskSizeGB, diskSizeGBTokens) - } - fillBlockOpt(resourceb, name) -} - // buildForExpr builds a for expression with the given variable and collection func buildForExpr(varName, collection string, trailingSpace bool) string { expr := fmt.Sprintf("for %s in %s :", varName, collection) From c8240d5c7fdc8123ac009aedd56438f27931da30 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Sun, 24 Aug 2025 12:48:38 +0200 Subject: [PATCH 32/43] convertRepSpecsWithDynamicBlock --- internal/convert/adv2v2.go | 149 ++++++++++++++++-------------------- internal/convert/clu2adv.go | 2 +- 2 files changed, 67 insertions(+), 84 deletions(-) diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index 678e047..25f75d6 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -60,22 +60,62 @@ func updateResource(resource *hclwrite.Block) (bool, error) { } func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error { - dSpec, err := getDynamicBlock(resourceb, nRepSpecs) + d, err := convertRepSpecsWithDynamicBlock(resourceb, diskSizeGB) if err != nil { return err } + if d.IsPresent() { + return nil + } + repSpecBlocks := collectBlocks(resourceb, nRepSpecs) + if len(repSpecBlocks) == 0 { + return fmt.Errorf("must have at least one replication_specs") + } + hasVariableShards := hasVariableNumShards(repSpecBlocks) + var resultTokens []hclwrite.Tokens + var resultBodies []*hclwrite.Body + for _, block := range repSpecBlocks { + blockb := block.Body() + shardsAttr := blockb.GetAttribute(nNumShards) + blockb.RemoveAttribute(nNumShards) + if err := convertConfig(blockb, diskSizeGB); err != nil { + return err + } + if hasVariableShards { + resultTokens = append(resultTokens, processNumShards(shardsAttr, blockb)) + continue + } + numShardsVal := 1 // Default to 1 if num_shards is not set + if shardsAttr != nil { + numShardsVal, _ = hcl.GetAttrInt(shardsAttr, errNumShards) + } + for range numShardsVal { + resultBodies = append(resultBodies, blockb) + } + } + if hasVariableShards { + resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensFuncConcat(resultTokens...)) + } else { + resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensArray(resultBodies)) + } + return nil +} + +func convertRepSpecsWithDynamicBlock(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) (dynamicBlock, error) { + dSpec, err := getDynamicBlock(resourceb, nRepSpecs) + if err != nil { + return dynamicBlock{}, err + } if dSpec.IsPresent() { - // Inline convertDynamicRepSpecs transformDynamicBlockReferences(dSpec.content.Body(), nRepSpecs, nSpec) - // Inline findDynamicConfigBlock dConfig, err := getDynamicBlock(dSpec.content.Body(), nConfig) if err != nil { - return err + return dynamicBlock{}, err } if !dConfig.IsPresent() { dConfig, err = getDynamicBlock(dSpec.content.Body(), nConfigSrc) if err != nil { - return err + return dynamicBlock{}, err } } if dConfig.IsPresent() { @@ -83,9 +123,7 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error configBlockName := getResourceName(dConfig.block) numShardsAttr := dSpec.content.Body().GetAttribute(nNumShards) if numShardsAttr != nil { - // Inline buildDynamicRepSpecsWithNumShards numShardsExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec) - // Inline transformDynamicBlockReferencesRecursive for dConfig transform1 := func(expr string) string { return replaceDynamicBlockReferences(expr, configBlockName, nRegion) } @@ -101,11 +139,9 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error for _, block := range dConfig.content.Body().Blocks() { transformAttributesSorted(block.Body(), block.Body().Attributes(), transform2) } - // Inline buildRegionConfigBody regionConfigFile := hclwrite.NewEmptyFile() regionConfigBody := regionConfigFile.Body() copyAttributesSorted(regionConfigBody, dConfig.content.Body().Attributes()) - // Inline processRegionConfigBlocks for _, block := range dConfig.content.Body().Blocks() { blockType := block.Type() blockFile := hclwrite.NewEmptyFile() @@ -123,7 +159,6 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error regionTokens := hcl.TokensFromExpr(regionForExpr) regionTokens = append(regionTokens, hcl.TokensObject(regionConfigBody)...) - // Inline buildRepSpecBody repSpecFile := hclwrite.NewEmptyFile() repSpecBody := repSpecFile.Body() if zoneNameAttr := dSpec.content.Body().GetAttribute(nZoneName); zoneNameAttr != nil { @@ -132,12 +167,10 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error } repSpecBody.SetAttributeRaw(nConfig, hcl.EncloseBracketsNewLines(regionTokens)) - // Inline buildInnerForExpr innerForExpr := buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr), false) innerTokens := hcl.TokensFromExpr(innerForExpr) innerTokens = append(innerTokens, hcl.TokensObject(repSpecBody)...) - // Inline buildOuterForExpr outerForExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach), true) outerTokens := hcl.TokensFromExpr(outerForExpr) outerTokens = append(outerTokens, hcl.EncloseBracketsNewLines(innerTokens)...) @@ -145,9 +178,8 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error tokens := hcl.TokensFuncFlatten(outerTokens) resourceb.RemoveBlock(dSpec.block) resourceb.SetAttributeRaw(nRepSpecs, tokens) - return nil + return dSpec, nil } - // Will inline convertDynamicRepSpecsWithoutNumShards repSpecFile := hclwrite.NewEmptyFile() repSpecb := repSpecFile.Body() if zoneNameAttr := dSpec.content.Body().GetAttribute(nZoneName); zoneNameAttr != nil { @@ -174,17 +206,15 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error tokens := hcl.TokensFuncFlatten(innerTokens) resourceb.RemoveBlock(dSpec.block) resourceb.SetAttributeRaw(nRepSpecs, tokens) - return nil + return dSpec, nil } - // Inline processDynamicRepSpecsWithoutConfig numShardsAttr := dSpec.content.Body().GetAttribute(nNumShards) dSpec.content.Body().RemoveAttribute(nNumShards) if err := convertConfig(dSpec.content.Body(), diskSizeGB); err != nil { - return err + return dynamicBlock{}, err } var tokens hclwrite.Tokens if numShardsAttr != nil { - // Inline buildDynamicRepSpecsWithShards numShardsExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec) outerFor := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach), false) innerFor := buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr), false) @@ -194,7 +224,6 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error tokens = append(tokens, hcl.TokensFromExpr("\n ]\n]")...) tokens = hcl.TokensFuncFlatten(tokens) } else { - // Inline buildSimpleDynamicRepSpecs forExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach), false) tokens = hcl.TokensFromExpr(forExpr) tokens = append(tokens, hcl.TokensObject(dSpec.content.Body())...) @@ -202,49 +231,9 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error } resourceb.RemoveBlock(dSpec.block) resourceb.SetAttributeRaw(nRepSpecs, tokens) - return nil + return dSpec, nil } - repSpecBlocks := collectBlocks(resourceb, nRepSpecs) - if len(repSpecBlocks) == 0 { - return fmt.Errorf("must have at least one replication_specs") - } - var tokens hclwrite.Tokens - if hasVariableNumShards(repSpecBlocks) { - // Inline processVariableNumShards - var concatParts []hclwrite.Tokens - for _, block := range repSpecBlocks { - blockb := block.Body() - numShardsAttr := blockb.GetAttribute(nNumShards) - blockb.RemoveAttribute(nNumShards) - if err := convertConfig(blockb, diskSizeGB); err != nil { - return err - } - concatParts = append(concatParts, processNumShards(numShardsAttr, blockb)) - } - tokens = hcl.TokensFuncConcat(concatParts...) - } else { - // Inline processStaticNumShards - var repSpecs []*hclwrite.Body - for _, block := range repSpecBlocks { - blockb := block.Body() - numShardsAttr := blockb.GetAttribute(nNumShards) - blockb.RemoveAttribute(nNumShards) - if err := convertConfig(blockb, diskSizeGB); err != nil { - return err - } - if numShardsAttr != nil { - numShardsVal, _ := hcl.GetAttrInt(numShardsAttr, errNumShards) - for range numShardsVal { - repSpecs = append(repSpecs, blockb) - } - } else { - repSpecs = append(repSpecs, blockb) - } - } - tokens = hcl.TokensArray(repSpecs) - } - resourceb.SetAttributeRaw(nRepSpecs, tokens) - return nil + return dynamicBlock{}, nil } func addAttributesWithTransform(targetBody *hclwrite.Body, sourceAttrs map[string]*hclwrite.Attribute, @@ -264,7 +253,22 @@ func convertConfig(repSpecs *hclwrite.Body, diskSizeGB hclwrite.Tokens) error { return err } if dConfig.IsPresent() { - return convertDynamicConfig(repSpecs, dConfig, diskSizeGB) + blockName := getResourceName(dConfig.block) + transform := func(expr string) string { + return replaceDynamicBlockReferences(expr, blockName, nRegion) + } + transformAttributesSorted(dConfig.content.Body(), dConfig.content.Body().Attributes(), transform) + for _, block := range dConfig.content.Body().Blocks() { + transformAttributesSorted(block.Body(), block.Body().Attributes(), transform) + } + processAllSpecs(dConfig.content.Body(), diskSizeGB) + forExpr := buildForExpr(nRegion, hcl.GetAttrExpr(dConfig.forEach), false) + tokens := hcl.TokensFromExpr(forExpr) + tokens = append(tokens, hcl.TokensObject(dConfig.content.Body())...) + tokens = hcl.EncloseBracketsNewLines(tokens) + repSpecs.RemoveBlock(dConfig.block) + repSpecs.SetAttributeRaw(nConfig, tokens) + return nil } var configs []*hclwrite.Body for _, block := range collectBlocks(repSpecs, nConfig) { @@ -279,27 +283,6 @@ func convertConfig(repSpecs *hclwrite.Body, diskSizeGB hclwrite.Tokens) error { return nil } -func convertDynamicConfig(repSpecs *hclwrite.Body, dConfig dynamicBlock, diskSizeGB hclwrite.Tokens) error { - blockName := getResourceName(dConfig.block) - // Inline transformDynamicBlockReferencesRecursive - transform := func(expr string) string { - return replaceDynamicBlockReferences(expr, blockName, nRegion) - } - transformAttributesSorted(dConfig.content.Body(), dConfig.content.Body().Attributes(), transform) - for _, block := range dConfig.content.Body().Blocks() { - // Recursive call inlined - transformAttributesSorted(block.Body(), block.Body().Attributes(), transform) - } - processAllSpecs(dConfig.content.Body(), diskSizeGB) - forExpr := buildForExpr(nRegion, hcl.GetAttrExpr(dConfig.forEach), false) - tokens := hcl.TokensFromExpr(forExpr) - tokens = append(tokens, hcl.TokensObject(dConfig.content.Body())...) - tokens = hcl.EncloseBracketsNewLines(tokens) - repSpecs.RemoveBlock(dConfig.block) - repSpecs.SetAttributeRaw(nConfig, tokens) - return nil -} - // hasExpectedBlocksAsAttributes checks if any of the expected block names // exist as attributes in the resource body. In that case conversion is not done // as advanced cluster is not in a valid SDKv2 configuration. diff --git a/internal/convert/clu2adv.go b/internal/convert/clu2adv.go index da41b64..f7d30db 100644 --- a/internal/convert/clu2adv.go +++ b/internal/convert/clu2adv.go @@ -176,7 +176,7 @@ func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error { } repSpecBlocks := collectBlocks(resourceb, nRepSpecs) if len(repSpecBlocks) == 0 { - return fmt.Errorf("%s: no replication_specs found", errRepSpecs) + return fmt.Errorf("must have at least one replication_specs") } dConfig, err := fillWithDynamicRegionConfigs(repSpecBlocks[0].Body(), root, false) if err != nil { From 3ae7484ab7da832f415abebc770636571ac054d8 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Sun, 24 Aug 2025 12:55:59 +0200 Subject: [PATCH 33/43] renames in clu2adv --- internal/convert/clu2adv.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/convert/clu2adv.go b/internal/convert/clu2adv.go index f7d30db..310891d 100644 --- a/internal/convert/clu2adv.go +++ b/internal/convert/clu2adv.go @@ -148,7 +148,7 @@ func fillCluster(resourceb *hclwrite.Body) error { resourceb.RemoveAttribute(nNumShards) // num_shards in root is not relevant, only in replication_specs // ok to fail as cloud_backup is optional _ = hcl.MoveAttr(resourceb, resourceb, nCloudBackup, nBackupEnabled, errRepSpecs) - if err := fillReplicationSpecs(resourceb, root); err != nil { + if err := fillRepSpecs(resourceb, root); err != nil { return err } if err := fillTagsLabelsOpt(resourceb, nTags); err != nil { @@ -164,8 +164,8 @@ func fillCluster(resourceb *hclwrite.Body) error { return nil } -func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error { - d, err := fillReplicationSpecsWithDynamicBlock(resourceb, root) +func fillRepSpecs(resourceb *hclwrite.Body, root attrVals) error { + d, err := fillRepSpecsWithDynamicBlock(resourceb, root) if err != nil { return err } @@ -178,7 +178,7 @@ func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error { if len(repSpecBlocks) == 0 { return fmt.Errorf("must have at least one replication_specs") } - dConfig, err := fillWithDynamicRegionConfigs(repSpecBlocks[0].Body(), root, false) + dConfig, err := fillConfigsWithDynamicRegion(repSpecBlocks[0].Body(), root, false) if err != nil { return err } @@ -221,14 +221,14 @@ func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error { return nil } -// fillReplicationSpecsWithDynamicBlock used for dynamic blocks in replication_specs -func fillReplicationSpecsWithDynamicBlock(resourceb *hclwrite.Body, root attrVals) (dynamicBlock, error) { +// fillRepSpecsWithDynamicBlock used for dynamic blocks in replication_specs +func fillRepSpecsWithDynamicBlock(resourceb *hclwrite.Body, root attrVals) (dynamicBlock, error) { dSpec, err := getDynamicBlock(resourceb, nRepSpecs) if err != nil || !dSpec.IsPresent() { return dynamicBlock{}, err } transformDynamicBlockReferences(dSpec.content.Body(), nRepSpecs, nSpec) - dConfig, err := fillWithDynamicRegionConfigs(dSpec.content.Body(), root, true) + dConfig, err := fillConfigsWithDynamicRegion(dSpec.content.Body(), root, true) if err != nil { return dynamicBlock{}, err } @@ -239,8 +239,8 @@ func fillReplicationSpecsWithDynamicBlock(resourceb *hclwrite.Body, root attrVal return dSpec, nil } -// fillWithDynamicRegionConfigs is used for dynamic blocks in region_configs -func fillWithDynamicRegionConfigs(specbSrc *hclwrite.Body, root attrVals, changeReferences bool) (dynamicBlock, error) { +// fillConfigsWithDynamicRegion is used for dynamic blocks in region_configs +func fillConfigsWithDynamicRegion(specbSrc *hclwrite.Body, root attrVals, changeReferences bool) (dynamicBlock, error) { d, err := getDynamicBlock(specbSrc, nConfigSrc) if err != nil || !d.IsPresent() { return dynamicBlock{}, err From 0ada06aa7bbefb2bbb0c92d2029dbec8d188343a Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Sun, 24 Aug 2025 13:59:19 +0200 Subject: [PATCH 34/43] reduce duplication in adv2v2 --- internal/convert/adv2v2.go | 223 ++++++++++++++++--------------------- 1 file changed, 97 insertions(+), 126 deletions(-) diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index 25f75d6..b58c233 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -65,6 +65,8 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error return err } if d.IsPresent() { + resourceb.RemoveBlock(d.block) + resourceb.SetAttributeRaw(nRepSpecs, d.tokens) return nil } repSpecBlocks := collectBlocks(resourceb, nRepSpecs) @@ -103,148 +105,117 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error func convertRepSpecsWithDynamicBlock(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) (dynamicBlock, error) { dSpec, err := getDynamicBlock(resourceb, nRepSpecs) + if err != nil || !dSpec.IsPresent() { + return dynamicBlock{}, err + } + transformDynamicBlockReferences(dSpec.content.Body(), nRepSpecs, nSpec) + dConfig, err := convertConfigsWithDynamicBlock(dSpec.content.Body(), diskSizeGB) + if err != nil { + return dynamicBlock{}, err + } + forSpec := hcl.TokensFromExpr(buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach), true)) + forSpec = append(forSpec, dConfig.tokens...) + tokens := hcl.TokensFuncFlatten(forSpec) + dSpec.tokens = tokens + return dSpec, nil +} + +func convertConfigsWithDynamicBlock(specbSrc *hclwrite.Body, diskSizeGB hclwrite.Tokens) (dynamicBlock, error) { + d, err := getDynamicBlock(specbSrc, nConfig) if err != nil { return dynamicBlock{}, err } - if dSpec.IsPresent() { - transformDynamicBlockReferences(dSpec.content.Body(), nRepSpecs, nSpec) - dConfig, err := getDynamicBlock(dSpec.content.Body(), nConfig) + if !d.IsPresent() { + d, err = getDynamicBlock(specbSrc, nConfigSrc) if err != nil { return dynamicBlock{}, err } - if !dConfig.IsPresent() { - dConfig, err = getDynamicBlock(dSpec.content.Body(), nConfigSrc) - if err != nil { - return dynamicBlock{}, err - } + } + if !d.IsPresent() { + // No dynamic config block, handle num_shards if present + numShardsAttr := specbSrc.GetAttribute(nNumShards) + if numShardsAttr == nil { + return dynamicBlock{}, fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) } - if dConfig.IsPresent() { - // Will inline convertDynamicRepSpecsWithDynamicConfig - configBlockName := getResourceName(dConfig.block) - numShardsAttr := dSpec.content.Body().GetAttribute(nNumShards) - if numShardsAttr != nil { - numShardsExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec) - transform1 := func(expr string) string { - return replaceDynamicBlockReferences(expr, configBlockName, nRegion) - } - transformAttributesSorted(dConfig.content.Body(), dConfig.content.Body().Attributes(), transform1) - for _, block := range dConfig.content.Body().Blocks() { - // Recursive call inlined - transformAttributesSorted(block.Body(), block.Body().Attributes(), transform1) - } - transform2 := func(expr string) string { - return replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) - } - transformAttributesSorted(dConfig.content.Body(), dConfig.content.Body().Attributes(), transform2) - for _, block := range dConfig.content.Body().Blocks() { - transformAttributesSorted(block.Body(), block.Body().Attributes(), transform2) - } - regionConfigFile := hclwrite.NewEmptyFile() - regionConfigBody := regionConfigFile.Body() - copyAttributesSorted(regionConfigBody, dConfig.content.Body().Attributes()) - for _, block := range dConfig.content.Body().Blocks() { - blockType := block.Type() - blockFile := hclwrite.NewEmptyFile() - blockBody := blockFile.Body() - copyAttributesSorted(blockBody, block.Body().Attributes()) - if diskSizeGB != nil && (blockType == nElectableSpecs || - blockType == nReadOnlySpecs || blockType == nAnalyticsSpecs) { - blockBody.SetAttributeRaw(nDiskSizeGB, diskSizeGB) - } - regionConfigBody.SetAttributeRaw(blockType, hcl.TokensObject(blockBody)) - } - - configForEach := fmt.Sprintf("%s.%s", nSpec, nConfig) - regionForExpr := buildForExpr(nRegion, configForEach, false) - regionTokens := hcl.TokensFromExpr(regionForExpr) - regionTokens = append(regionTokens, hcl.TokensObject(regionConfigBody)...) + specbSrc.RemoveAttribute(nNumShards) + if errConv := convertConfig(specbSrc, diskSizeGB); errConv != nil { + return dynamicBlock{}, errConv + } + numShardsExpr := hcl.GetAttrExpr(numShardsAttr) + numShardsExpr = replaceDynamicBlockReferences(numShardsExpr, nRepSpecs, nSpec) + tokens := hcl.TokensFromExpr(buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr), false)) + tokens = append(tokens, hcl.TokensObject(specbSrc)...) + tokens = hcl.EncloseBracketsNewLines(tokens) + return dynamicBlock{tokens: tokens}, nil + } - repSpecFile := hclwrite.NewEmptyFile() - repSpecBody := repSpecFile.Body() - if zoneNameAttr := dSpec.content.Body().GetAttribute(nZoneName); zoneNameAttr != nil { - zoneNameExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(zoneNameAttr), nRepSpecs, nSpec) - repSpecBody.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(zoneNameExpr)) - } - repSpecBody.SetAttributeRaw(nConfig, hcl.EncloseBracketsNewLines(regionTokens)) + // Dynamic config block found + repSpec := hclwrite.NewEmptyFile() + repSpecb := repSpec.Body() + if zoneNameAttr := specbSrc.GetAttribute(nZoneName); zoneNameAttr != nil { + zoneNameExpr := hcl.GetAttrExpr(zoneNameAttr) + zoneNameExpr = replaceDynamicBlockReferences(zoneNameExpr, nRepSpecs, nSpec) + repSpecb.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(zoneNameExpr)) + } - innerForExpr := buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr), false) - innerTokens := hcl.TokensFromExpr(innerForExpr) - innerTokens = append(innerTokens, hcl.TokensObject(repSpecBody)...) + configForEach := fmt.Sprintf("%s.%s", nSpec, nConfig) - outerForExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach), true) - outerTokens := hcl.TokensFromExpr(outerForExpr) - outerTokens = append(outerTokens, hcl.EncloseBracketsNewLines(innerTokens)...) + regionConfig, err := getDynamicRegionConfig(d, configForEach, diskSizeGB) + if err != nil { + return dynamicBlock{}, err + } + repSpecb.SetAttributeRaw(nConfig, regionConfig) - tokens := hcl.TokensFuncFlatten(outerTokens) - resourceb.RemoveBlock(dSpec.block) - resourceb.SetAttributeRaw(nRepSpecs, tokens) - return dSpec, nil - } - repSpecFile := hclwrite.NewEmptyFile() - repSpecb := repSpecFile.Body() - if zoneNameAttr := dSpec.content.Body().GetAttribute(nZoneName); zoneNameAttr != nil { - zoneNameExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(zoneNameAttr), nRepSpecs, nSpec) - repSpecb.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(zoneNameExpr)) - } - configFile := hclwrite.NewEmptyFile() - configb := configFile.Body() - addAttributesWithTransform(configb, dConfig.content.Body().Attributes(), configBlockName) - for _, block := range dConfig.content.Body().Blocks() { - newBlock := configb.AppendNewBlock(block.Type(), block.Labels()) - newBlockb := newBlock.Body() - addAttributesWithTransform(newBlockb, block.Body().Attributes(), configBlockName) - } - processAllSpecs(configb, diskSizeGB) - configForEach := fmt.Sprintf("%s.%s", nSpec, nConfig) - regionForExpr := buildForExpr(nRegion, configForEach, false) - regionTokens := hcl.TokensFromExpr(regionForExpr) - regionTokens = append(regionTokens, hcl.TokensObject(configb)...) - repSpecb.SetAttributeRaw(nConfig, hcl.EncloseBracketsNewLines(regionTokens)) - forExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach), true) - innerTokens := hcl.TokensFromExpr(forExpr) - innerTokens = append(innerTokens, hcl.TokensArraySingle(repSpecb)...) - tokens := hcl.TokensFuncFlatten(innerTokens) - resourceb.RemoveBlock(dSpec.block) - resourceb.SetAttributeRaw(nRepSpecs, tokens) - return dSpec, nil - } - numShardsAttr := dSpec.content.Body().GetAttribute(nNumShards) - dSpec.content.Body().RemoveAttribute(nNumShards) - if err := convertConfig(dSpec.content.Body(), diskSizeGB); err != nil { - return dynamicBlock{}, err - } - var tokens hclwrite.Tokens - if numShardsAttr != nil { - numShardsExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec) - outerFor := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach), false) - innerFor := buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr), false) - forExpr := fmt.Sprintf("%s [\n %s ", outerFor, innerFor) - tokens = hcl.TokensFromExpr(forExpr) - tokens = append(tokens, hcl.TokensObject(dSpec.content.Body())...) - tokens = append(tokens, hcl.TokensFromExpr("\n ]\n]")...) - tokens = hcl.TokensFuncFlatten(tokens) - } else { - forExpr := buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach), false) - tokens = hcl.TokensFromExpr(forExpr) - tokens = append(tokens, hcl.TokensObject(dSpec.content.Body())...) - tokens = hcl.EncloseBracketsNewLines(tokens) - } - resourceb.RemoveBlock(dSpec.block) - resourceb.SetAttributeRaw(nRepSpecs, tokens) - return dSpec, nil + // Handle num_shards + numShardsAttr := specbSrc.GetAttribute(nNumShards) + if numShardsAttr != nil { + numShardsExpr := hcl.GetAttrExpr(numShardsAttr) + numShardsExpr = replaceDynamicBlockReferences(numShardsExpr, nRepSpecs, nSpec) + tokens := hcl.TokensFromExpr(buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr), false)) + tokens = append(tokens, hcl.TokensObject(repSpecb)...) + return dynamicBlock{tokens: hcl.EncloseBracketsNewLines(tokens)}, nil } - return dynamicBlock{}, nil + return dynamicBlock{tokens: hcl.TokensArraySingle(repSpecb)}, nil } -func addAttributesWithTransform(targetBody *hclwrite.Body, sourceAttrs map[string]*hclwrite.Attribute, - configBlockName string) { - transform1 := func(expr string) string { - return replaceDynamicBlockReferences(expr, configBlockName, nRegion) +// getDynamicRegionConfig builds the region config array for a dynamic config block +func getDynamicRegionConfig(d dynamicBlock, configForEach string, diskSizeGB hclwrite.Tokens) (hclwrite.Tokens, error) { + configBlockName := getResourceName(d.block) + transformDynamicBlockReferences(d.content.Body(), configBlockName, nRegion) + for _, block := range d.content.Body().Blocks() { + transformDynamicBlockReferences(block.Body(), configBlockName, nRegion) } - transform2 := func(expr string) string { - return replaceDynamicBlockReferences(expr, nRepSpecs, nSpec) + // Additional transformation for nested spec references + for name, attr := range d.content.Body().Attributes() { + expr := replaceDynamicBlockReferences(hcl.GetAttrExpr(attr), nRepSpecs, nSpec) + d.content.Body().SetAttributeRaw(name, hcl.TokensFromExpr(expr)) } - transformAttributesSorted(targetBody, sourceAttrs, transform1, transform2) + for _, block := range d.content.Body().Blocks() { + for name, attr := range block.Body().Attributes() { + expr := replaceDynamicBlockReferences(hcl.GetAttrExpr(attr), nRepSpecs, nSpec) + block.Body().SetAttributeRaw(name, hcl.TokensFromExpr(expr)) + } + } + + regionConfigFile := hclwrite.NewEmptyFile() + regionConfigBody := regionConfigFile.Body() + copyAttributesSorted(regionConfigBody, d.content.Body().Attributes()) + for _, block := range d.content.Body().Blocks() { + blockType := block.Type() + blockFile := hclwrite.NewEmptyFile() + blockBody := blockFile.Body() + copyAttributesSorted(blockBody, block.Body().Attributes()) + if diskSizeGB != nil && (blockType == nElectableSpecs || + blockType == nReadOnlySpecs || blockType == nAnalyticsSpecs) { + blockBody.SetAttributeRaw(nDiskSizeGB, diskSizeGB) + } + regionConfigBody.SetAttributeRaw(blockType, hcl.TokensObject(blockBody)) + } + + regionForExpr := buildForExpr(nRegion, configForEach, false) + regionTokens := hcl.TokensFromExpr(regionForExpr) + regionTokens = append(regionTokens, hcl.TokensObject(regionConfigBody)...) + return hcl.EncloseBracketsNewLines(regionTokens), nil } func convertConfig(repSpecs *hclwrite.Body, diskSizeGB hclwrite.Tokens) error { From a177614022c2f1d26249da34428b8c0197ca2b1b Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Sun, 24 Aug 2025 14:09:33 +0200 Subject: [PATCH 35/43] remove unneeded code --- internal/convert/adv2v2.go | 105 +++++++------------------------------ 1 file changed, 20 insertions(+), 85 deletions(-) diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index b58c233..bbd797a 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -114,9 +114,7 @@ func convertRepSpecsWithDynamicBlock(resourceb *hclwrite.Body, diskSizeGB hclwri return dynamicBlock{}, err } forSpec := hcl.TokensFromExpr(buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach), true)) - forSpec = append(forSpec, dConfig.tokens...) - tokens := hcl.TokensFuncFlatten(forSpec) - dSpec.tokens = tokens + dSpec.tokens = hcl.TokensFuncFlatten(append(forSpec, dConfig.tokens...)) return dSpec, nil } @@ -125,67 +123,18 @@ func convertConfigsWithDynamicBlock(specbSrc *hclwrite.Body, diskSizeGB hclwrite if err != nil { return dynamicBlock{}, err } - if !d.IsPresent() { - d, err = getDynamicBlock(specbSrc, nConfigSrc) - if err != nil { - return dynamicBlock{}, err - } - } - if !d.IsPresent() { - // No dynamic config block, handle num_shards if present - numShardsAttr := specbSrc.GetAttribute(nNumShards) - if numShardsAttr == nil { - return dynamicBlock{}, fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) - } - specbSrc.RemoveAttribute(nNumShards) - if errConv := convertConfig(specbSrc, diskSizeGB); errConv != nil { - return dynamicBlock{}, errConv - } - numShardsExpr := hcl.GetAttrExpr(numShardsAttr) - numShardsExpr = replaceDynamicBlockReferences(numShardsExpr, nRepSpecs, nSpec) - tokens := hcl.TokensFromExpr(buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr), false)) - tokens = append(tokens, hcl.TokensObject(specbSrc)...) - tokens = hcl.EncloseBracketsNewLines(tokens) - return dynamicBlock{tokens: tokens}, nil - } - - // Dynamic config block found repSpec := hclwrite.NewEmptyFile() repSpecb := repSpec.Body() if zoneNameAttr := specbSrc.GetAttribute(nZoneName); zoneNameAttr != nil { - zoneNameExpr := hcl.GetAttrExpr(zoneNameAttr) - zoneNameExpr = replaceDynamicBlockReferences(zoneNameExpr, nRepSpecs, nSpec) - repSpecb.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(zoneNameExpr)) + expr := replaceDynamicBlockReferences(hcl.GetAttrExpr(zoneNameAttr), nRepSpecs, nSpec) + repSpecb.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(expr)) } - configForEach := fmt.Sprintf("%s.%s", nSpec, nConfig) - - regionConfig, err := getDynamicRegionConfig(d, configForEach, diskSizeGB) - if err != nil { - return dynamicBlock{}, err - } - repSpecb.SetAttributeRaw(nConfig, regionConfig) - - // Handle num_shards - numShardsAttr := specbSrc.GetAttribute(nNumShards) - if numShardsAttr != nil { - numShardsExpr := hcl.GetAttrExpr(numShardsAttr) - numShardsExpr = replaceDynamicBlockReferences(numShardsExpr, nRepSpecs, nSpec) - tokens := hcl.TokensFromExpr(buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr), false)) - tokens = append(tokens, hcl.TokensObject(repSpecb)...) - return dynamicBlock{tokens: hcl.EncloseBracketsNewLines(tokens)}, nil - } - return dynamicBlock{tokens: hcl.TokensArraySingle(repSpecb)}, nil -} - -// getDynamicRegionConfig builds the region config array for a dynamic config block -func getDynamicRegionConfig(d dynamicBlock, configForEach string, diskSizeGB hclwrite.Tokens) (hclwrite.Tokens, error) { configBlockName := getResourceName(d.block) transformDynamicBlockReferences(d.content.Body(), configBlockName, nRegion) for _, block := range d.content.Body().Blocks() { transformDynamicBlockReferences(block.Body(), configBlockName, nRegion) } - // Additional transformation for nested spec references for name, attr := range d.content.Body().Attributes() { expr := replaceDynamicBlockReferences(hcl.GetAttrExpr(attr), nRepSpecs, nSpec) d.content.Body().SetAttributeRaw(name, hcl.TokensFromExpr(expr)) @@ -196,7 +145,6 @@ func getDynamicRegionConfig(d dynamicBlock, configForEach string, diskSizeGB hcl block.Body().SetAttributeRaw(name, hcl.TokensFromExpr(expr)) } } - regionConfigFile := hclwrite.NewEmptyFile() regionConfigBody := regionConfigFile.Body() copyAttributesSorted(regionConfigBody, d.content.Body().Attributes()) @@ -211,11 +159,18 @@ func getDynamicRegionConfig(d dynamicBlock, configForEach string, diskSizeGB hcl } regionConfigBody.SetAttributeRaw(blockType, hcl.TokensObject(blockBody)) } - - regionForExpr := buildForExpr(nRegion, configForEach, false) - regionTokens := hcl.TokensFromExpr(regionForExpr) + regionTokens := hcl.TokensFromExpr(buildForExpr(nRegion, configForEach, false)) regionTokens = append(regionTokens, hcl.TokensObject(regionConfigBody)...) - return hcl.EncloseBracketsNewLines(regionTokens), nil + regionConfig := hcl.EncloseBracketsNewLines(regionTokens) + repSpecb.SetAttributeRaw(nConfig, regionConfig) + numShardsAttr := specbSrc.GetAttribute(nNumShards) + if numShardsAttr != nil { + numShardsExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec) + tokens := hcl.TokensFromExpr(buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr), false)) + tokens = append(tokens, hcl.TokensObject(repSpecb)...) + return dynamicBlock{tokens: hcl.EncloseBracketsNewLines(tokens)}, nil + } + return dynamicBlock{tokens: hcl.TokensArraySingle(repSpecb)}, nil } func convertConfig(repSpecs *hclwrite.Body, diskSizeGB hclwrite.Tokens) error { @@ -228,17 +183,15 @@ func convertConfig(repSpecs *hclwrite.Body, diskSizeGB hclwrite.Tokens) error { transform := func(expr string) string { return replaceDynamicBlockReferences(expr, blockName, nRegion) } - transformAttributesSorted(dConfig.content.Body(), dConfig.content.Body().Attributes(), transform) + copyAttributesSorted(dConfig.content.Body(), dConfig.content.Body().Attributes(), transform) for _, block := range dConfig.content.Body().Blocks() { - transformAttributesSorted(block.Body(), block.Body().Attributes(), transform) + copyAttributesSorted(block.Body(), block.Body().Attributes(), transform) } processAllSpecs(dConfig.content.Body(), diskSizeGB) - forExpr := buildForExpr(nRegion, hcl.GetAttrExpr(dConfig.forEach), false) - tokens := hcl.TokensFromExpr(forExpr) + tokens := hcl.TokensFromExpr(buildForExpr(nRegion, hcl.GetAttrExpr(dConfig.forEach), false)) tokens = append(tokens, hcl.TokensObject(dConfig.content.Body())...) - tokens = hcl.EncloseBracketsNewLines(tokens) repSpecs.RemoveBlock(dConfig.block) - repSpecs.SetAttributeRaw(nConfig, tokens) + repSpecs.SetAttributeRaw(nConfig, hcl.EncloseBracketsNewLines(tokens)) return nil } var configs []*hclwrite.Body @@ -267,21 +220,7 @@ func hasExpectedBlocksAsAttributes(resourceb *hclwrite.Body) bool { return false } -// copyAttributesSorted copies attributes from source to target in sorted order for deterministic output -func copyAttributesSorted(targetBody *hclwrite.Body, sourceAttrs map[string]*hclwrite.Attribute) { - var names []string - for name := range sourceAttrs { - names = append(names, name) - } - slices.Sort(names) - for _, name := range names { - attr := sourceAttrs[name] - targetBody.SetAttributeRaw(name, hcl.TokensFromExpr(hcl.GetAttrExpr(attr))) - } -} - -// transformAttributesSorted transforms and copies attributes in sorted order -func transformAttributesSorted(targetBody *hclwrite.Body, sourceAttrs map[string]*hclwrite.Attribute, +func copyAttributesSorted(targetBody *hclwrite.Body, sourceAttrs map[string]*hclwrite.Attribute, transforms ...func(string) string) { var names []string for name := range sourceAttrs { @@ -289,9 +228,7 @@ func transformAttributesSorted(targetBody *hclwrite.Body, sourceAttrs map[string } slices.Sort(names) for _, name := range names { - attr := sourceAttrs[name] - expr := hcl.GetAttrExpr(attr) - // Apply all transformations + expr := hcl.GetAttrExpr(sourceAttrs[name]) for _, transform := range transforms { expr = transform(expr) } @@ -299,7 +236,6 @@ func transformAttributesSorted(targetBody *hclwrite.Body, sourceAttrs map[string } } -// processAllSpecs processes all spec blocks (electable, read_only, analytics) and auto_scaling blocks func processAllSpecs(body *hclwrite.Body, diskSizeGB hclwrite.Tokens) { fillSpecOpt(body, nElectableSpecs, diskSizeGB) fillSpecOpt(body, nReadOnlySpecs, diskSizeGB) @@ -308,7 +244,6 @@ func processAllSpecs(body *hclwrite.Body, diskSizeGB hclwrite.Tokens) { fillSpecOpt(body, nAnalyticsAutoScaling, nil) } -// fillSpecOpt converts a spec block to an attribute with object value and optionally adds disk_size_gb func fillSpecOpt(resourceb *hclwrite.Body, name string, diskSizeGBTokens hclwrite.Tokens) { block := resourceb.FirstMatchingBlock(name, nil) if block == nil { From 49f81b8da1678fb84bc146e77dd2dcad05fc2a85 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Sun, 24 Aug 2025 15:34:58 +0200 Subject: [PATCH 36/43] rename dynamic block helper functions --- internal/convert/adv2v2.go | 29 +++++++++++++++-------------- internal/convert/clu2adv.go | 6 +++--- internal/convert/shared.go | 14 +++++++------- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index bbd797a..1a84ebf 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -108,7 +108,7 @@ func convertRepSpecsWithDynamicBlock(resourceb *hclwrite.Body, diskSizeGB hclwri if err != nil || !dSpec.IsPresent() { return dynamicBlock{}, err } - transformDynamicBlockReferences(dSpec.content.Body(), nRepSpecs, nSpec) + transformReferences(dSpec.content.Body(), nRepSpecs, nSpec) dConfig, err := convertConfigsWithDynamicBlock(dSpec.content.Body(), diskSizeGB) if err != nil { return dynamicBlock{}, err @@ -123,32 +123,33 @@ func convertConfigsWithDynamicBlock(specbSrc *hclwrite.Body, diskSizeGB hclwrite if err != nil { return dynamicBlock{}, err } + configBody := d.content.Body() repSpec := hclwrite.NewEmptyFile() repSpecb := repSpec.Body() if zoneNameAttr := specbSrc.GetAttribute(nZoneName); zoneNameAttr != nil { - expr := replaceDynamicBlockReferences(hcl.GetAttrExpr(zoneNameAttr), nRepSpecs, nSpec) + expr := transformReference(hcl.GetAttrExpr(zoneNameAttr), nRepSpecs, nSpec) repSpecb.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(expr)) } configForEach := fmt.Sprintf("%s.%s", nSpec, nConfig) configBlockName := getResourceName(d.block) - transformDynamicBlockReferences(d.content.Body(), configBlockName, nRegion) - for _, block := range d.content.Body().Blocks() { - transformDynamicBlockReferences(block.Body(), configBlockName, nRegion) + transformReferences(configBody, configBlockName, nRegion) + for _, block := range configBody.Blocks() { + transformReferences(block.Body(), configBlockName, nRegion) } - for name, attr := range d.content.Body().Attributes() { - expr := replaceDynamicBlockReferences(hcl.GetAttrExpr(attr), nRepSpecs, nSpec) - d.content.Body().SetAttributeRaw(name, hcl.TokensFromExpr(expr)) + for name, attr := range configBody.Attributes() { + expr := transformReference(hcl.GetAttrExpr(attr), nRepSpecs, nSpec) + configBody.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) } - for _, block := range d.content.Body().Blocks() { + for _, block := range configBody.Blocks() { for name, attr := range block.Body().Attributes() { - expr := replaceDynamicBlockReferences(hcl.GetAttrExpr(attr), nRepSpecs, nSpec) + expr := transformReference(hcl.GetAttrExpr(attr), nRepSpecs, nSpec) block.Body().SetAttributeRaw(name, hcl.TokensFromExpr(expr)) } } regionConfigFile := hclwrite.NewEmptyFile() regionConfigBody := regionConfigFile.Body() - copyAttributesSorted(regionConfigBody, d.content.Body().Attributes()) - for _, block := range d.content.Body().Blocks() { + copyAttributesSorted(regionConfigBody, configBody.Attributes()) + for _, block := range configBody.Blocks() { blockType := block.Type() blockFile := hclwrite.NewEmptyFile() blockBody := blockFile.Body() @@ -165,7 +166,7 @@ func convertConfigsWithDynamicBlock(specbSrc *hclwrite.Body, diskSizeGB hclwrite repSpecb.SetAttributeRaw(nConfig, regionConfig) numShardsAttr := specbSrc.GetAttribute(nNumShards) if numShardsAttr != nil { - numShardsExpr := replaceDynamicBlockReferences(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec) + numShardsExpr := transformReference(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec) tokens := hcl.TokensFromExpr(buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr), false)) tokens = append(tokens, hcl.TokensObject(repSpecb)...) return dynamicBlock{tokens: hcl.EncloseBracketsNewLines(tokens)}, nil @@ -181,7 +182,7 @@ func convertConfig(repSpecs *hclwrite.Body, diskSizeGB hclwrite.Tokens) error { if dConfig.IsPresent() { blockName := getResourceName(dConfig.block) transform := func(expr string) string { - return replaceDynamicBlockReferences(expr, blockName, nRegion) + return transformReference(expr, blockName, nRegion) } copyAttributesSorted(dConfig.content.Body(), dConfig.content.Body().Attributes(), transform) for _, block := range dConfig.content.Body().Blocks() { diff --git a/internal/convert/clu2adv.go b/internal/convert/clu2adv.go index 310891d..1523bae 100644 --- a/internal/convert/clu2adv.go +++ b/internal/convert/clu2adv.go @@ -227,7 +227,7 @@ func fillRepSpecsWithDynamicBlock(resourceb *hclwrite.Body, root attrVals) (dyna if err != nil || !dSpec.IsPresent() { return dynamicBlock{}, err } - transformDynamicBlockReferences(dSpec.content.Body(), nRepSpecs, nSpec) + transformReferences(dSpec.content.Body(), nRepSpecs, nSpec) dConfig, err := fillConfigsWithDynamicRegion(dSpec.content.Body(), root, true) if err != nil { return dynamicBlock{}, err @@ -252,7 +252,7 @@ func fillConfigsWithDynamicRegion(specbSrc *hclwrite.Body, root attrVals, change } forEach := hcl.GetAttrExpr(d.forEach) if changeReferences { - forEach = replaceDynamicBlockReferences(forEach, nRepSpecs, nSpec) + forEach = transformReference(forEach, nRepSpecs, nSpec) } regionFor, err := getDynamicBlockRegionArray(forEach, d.content, root) if err != nil { @@ -403,7 +403,7 @@ func replaceDynamicBlockExpr(attr *hclwrite.Attribute, blockName, attrName strin // getDynamicBlockRegionArray 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 getDynamicBlockRegionArray(forEach string, configSrc *hclwrite.Block, root attrVals) (hclwrite.Tokens, error) { - transformDynamicBlockReferences(configSrc.Body(), nConfigSrc, nRegion) + transformReferences(configSrc.Body(), nConfigSrc, nRegion) priorityStr := hcl.GetAttrExpr(configSrc.Body().GetAttribute(nPriority)) if priorityStr == "" { return nil, fmt.Errorf("%s: %s not found", errRepSpecs, nPriority) diff --git a/internal/convert/shared.go b/internal/convert/shared.go index 92dfd1d..fa4fb5b 100644 --- a/internal/convert/shared.go +++ b/internal/convert/shared.go @@ -93,19 +93,19 @@ func getResourceName(resource *hclwrite.Block) string { return labels[0] } -// replaceDynamicBlockReferences changes value references, +// transformReference changes value references, // e.g. regions_config.value.electable_nodes to region.electable_nodes -func replaceDynamicBlockReferences(expr, blockName, varName string) string { +func transformReference(expr, blockName, varName string) string { return strings.ReplaceAll(expr, fmt.Sprintf("%s.%s.", blockName, nValue), fmt.Sprintf("%s.", varName)) } -// transformDynamicBlockReferences transforms all attribute references in a body from dynamic block format -func transformDynamicBlockReferences(configSrcb *hclwrite.Body, blockName, varName string) { - for name, attr := range configSrcb.Attributes() { - expr := replaceDynamicBlockReferences(hcl.GetAttrExpr(attr), blockName, varName) - configSrcb.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) +// transformReferences transforms all attribute references in a body from dynamic block format +func transformReferences(body *hclwrite.Body, blockName, varName string) { + for name, attr := range body.Attributes() { + expr := transformReference(hcl.GetAttrExpr(attr), blockName, varName) + body.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) } } From 4f0cca7946db8e1ab9a3aabb7144a5dc6bc5c75b Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Mon, 25 Aug 2025 10:52:05 +0200 Subject: [PATCH 37/43] reduce use of transformReference --- internal/convert/adv2v2.go | 25 +++++-------------------- internal/convert/shared.go | 3 +++ 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index 1a84ebf..601b652 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -133,19 +133,6 @@ func convertConfigsWithDynamicBlock(specbSrc *hclwrite.Body, diskSizeGB hclwrite configForEach := fmt.Sprintf("%s.%s", nSpec, nConfig) configBlockName := getResourceName(d.block) transformReferences(configBody, configBlockName, nRegion) - for _, block := range configBody.Blocks() { - transformReferences(block.Body(), configBlockName, nRegion) - } - for name, attr := range configBody.Attributes() { - expr := transformReference(hcl.GetAttrExpr(attr), nRepSpecs, nSpec) - configBody.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) - } - for _, block := range configBody.Blocks() { - for name, attr := range block.Body().Attributes() { - expr := transformReference(hcl.GetAttrExpr(attr), nRepSpecs, nSpec) - block.Body().SetAttributeRaw(name, hcl.TokensFromExpr(expr)) - } - } regionConfigFile := hclwrite.NewEmptyFile() regionConfigBody := regionConfigFile.Body() copyAttributesSorted(regionConfigBody, configBody.Attributes()) @@ -181,13 +168,8 @@ func convertConfig(repSpecs *hclwrite.Body, diskSizeGB hclwrite.Tokens) error { } if dConfig.IsPresent() { blockName := getResourceName(dConfig.block) - transform := func(expr string) string { - return transformReference(expr, blockName, nRegion) - } - copyAttributesSorted(dConfig.content.Body(), dConfig.content.Body().Attributes(), transform) - for _, block := range dConfig.content.Body().Blocks() { - copyAttributesSorted(block.Body(), block.Body().Attributes(), transform) - } + transformReferences(dConfig.content.Body(), blockName, nRegion) + copyAttributesSorted(dConfig.content.Body(), dConfig.content.Body().Attributes()) processAllSpecs(dConfig.content.Body(), diskSizeGB) tokens := hcl.TokensFromExpr(buildForExpr(nRegion, hcl.GetAttrExpr(dConfig.forEach), false)) tokens = append(tokens, hcl.TokensObject(dConfig.content.Body())...) @@ -235,6 +217,9 @@ func copyAttributesSorted(targetBody *hclwrite.Body, sourceAttrs map[string]*hcl } targetBody.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) } + for _, block := range targetBody.Blocks() { + copyAttributesSorted(block.Body(), block.Body().Attributes(), transforms...) + } } func processAllSpecs(body *hclwrite.Body, diskSizeGB hclwrite.Tokens) { diff --git a/internal/convert/shared.go b/internal/convert/shared.go index fa4fb5b..68f6d7f 100644 --- a/internal/convert/shared.go +++ b/internal/convert/shared.go @@ -107,6 +107,9 @@ func transformReferences(body *hclwrite.Body, blockName, varName string) { expr := transformReference(hcl.GetAttrExpr(attr), blockName, varName) body.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) } + for _, block := range body.Blocks() { + transformReferences(block.Body(), blockName, varName) + } } // collectBlocks removes and returns all blocks of the given name from body in order of appearance. From f7921d5786b0e575e60c5eed7f90a106c14c5ec8 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Mon, 25 Aug 2025 11:14:41 +0200 Subject: [PATCH 38/43] inline back convertConfig --- internal/convert/adv2v2.go | 59 +++++++++++++++++--------------------- internal/convert/shared.go | 15 ++++------ 2 files changed, 31 insertions(+), 43 deletions(-) diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index 601b652..6781920 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -80,9 +80,34 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error blockb := block.Body() shardsAttr := blockb.GetAttribute(nNumShards) blockb.RemoveAttribute(nNumShards) - if err := convertConfig(blockb, diskSizeGB); err != nil { + + // Process config - inlined from convertConfig + dConfig, err := getDynamicBlock(blockb, nConfig) + if err != nil { return err } + + if dConfig.IsPresent() { + blockName := getResourceName(dConfig.block) + transformReferences(dConfig.content.Body(), blockName, nRegion) + copyAttributesSorted(dConfig.content.Body(), dConfig.content.Body().Attributes()) + processAllSpecs(dConfig.content.Body(), diskSizeGB) + tokens := hcl.TokensFromExpr(buildForExpr(nRegion, hcl.GetAttrExpr(dConfig.forEach), false)) + tokens = append(tokens, hcl.TokensObject(dConfig.content.Body())...) + blockb.RemoveBlock(dConfig.block) + blockb.SetAttributeRaw(nConfig, hcl.EncloseBracketsNewLines(tokens)) + } else { + var configs []*hclwrite.Body + for _, configBlock := range collectBlocks(blockb, nConfig) { + configBlockb := configBlock.Body() + processAllSpecs(configBlockb, diskSizeGB) + configs = append(configs, configBlockb) + } + if len(configs) == 0 { + return fmt.Errorf("replication_specs must have at least one region_configs") + } + blockb.SetAttributeRaw(nConfig, hcl.TokensArray(configs)) + } if hasVariableShards { resultTokens = append(resultTokens, processNumShards(shardsAttr, blockb)) continue @@ -161,35 +186,6 @@ func convertConfigsWithDynamicBlock(specbSrc *hclwrite.Body, diskSizeGB hclwrite return dynamicBlock{tokens: hcl.TokensArraySingle(repSpecb)}, nil } -func convertConfig(repSpecs *hclwrite.Body, diskSizeGB hclwrite.Tokens) error { - dConfig, err := getDynamicBlock(repSpecs, nConfig) - if err != nil { - return err - } - if dConfig.IsPresent() { - blockName := getResourceName(dConfig.block) - transformReferences(dConfig.content.Body(), blockName, nRegion) - copyAttributesSorted(dConfig.content.Body(), dConfig.content.Body().Attributes()) - processAllSpecs(dConfig.content.Body(), diskSizeGB) - tokens := hcl.TokensFromExpr(buildForExpr(nRegion, hcl.GetAttrExpr(dConfig.forEach), false)) - tokens = append(tokens, hcl.TokensObject(dConfig.content.Body())...) - repSpecs.RemoveBlock(dConfig.block) - repSpecs.SetAttributeRaw(nConfig, hcl.EncloseBracketsNewLines(tokens)) - return nil - } - var configs []*hclwrite.Body - for _, block := range collectBlocks(repSpecs, nConfig) { - blockb := block.Body() - processAllSpecs(blockb, diskSizeGB) - configs = append(configs, blockb) - } - if len(configs) == 0 { - return fmt.Errorf("replication_specs must have at least one region_configs") - } - repSpecs.SetAttributeRaw(nConfig, hcl.TokensArray(configs)) - return nil -} - // hasExpectedBlocksAsAttributes checks if any of the expected block names // exist as attributes in the resource body. In that case conversion is not done // as advanced cluster is not in a valid SDKv2 configuration. @@ -217,9 +213,6 @@ func copyAttributesSorted(targetBody *hclwrite.Body, sourceAttrs map[string]*hcl } targetBody.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) } - for _, block := range targetBody.Blocks() { - copyAttributesSorted(block.Body(), block.Body().Attributes(), transforms...) - } } func processAllSpecs(body *hclwrite.Body, diskSizeGB hclwrite.Tokens) { diff --git a/internal/convert/shared.go b/internal/convert/shared.go index 68f6d7f..6791913 100644 --- a/internal/convert/shared.go +++ b/internal/convert/shared.go @@ -208,13 +208,12 @@ func extractTagsLabelsIndividual(resourceb *hclwrite.Body, name string) (hclwrit var ( file = hclwrite.NewEmptyFile() fileb = file.Body() - found = false ) - for { - block := resourceb.FirstMatchingBlock(name, nil) - if block == nil { - break - } + blocks := collectBlocks(resourceb, name) + if len(blocks) == 0 { + return nil, nil + } + for _, block := range blocks { key := block.Body().GetAttribute(nKey) value := block.Body().GetAttribute(nValue) if key == nil || value == nil { @@ -222,10 +221,6 @@ func extractTagsLabelsIndividual(resourceb *hclwrite.Body, name string) (hclwrit } setKeyValue(fileb, key, value) resourceb.RemoveBlock(block) - found = true - } - if !found { - return nil, nil } return hcl.TokensObject(fileb), nil } From 7b6afc3641a51db10d8a281e4a7965be69742fdb Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Mon, 25 Aug 2025 11:28:56 +0200 Subject: [PATCH 39/43] refactor new block creation --- internal/convert/adv2v2.go | 12 ++++-------- internal/convert/clu2adv.go | 38 ++++++++++++++++--------------------- internal/convert/shared.go | 3 +-- 3 files changed, 21 insertions(+), 32 deletions(-) diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index 6781920..46a4d2d 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -149,8 +149,7 @@ func convertConfigsWithDynamicBlock(specbSrc *hclwrite.Body, diskSizeGB hclwrite return dynamicBlock{}, err } configBody := d.content.Body() - repSpec := hclwrite.NewEmptyFile() - repSpecb := repSpec.Body() + repSpecb := hclwrite.NewEmptyFile().Body() if zoneNameAttr := specbSrc.GetAttribute(nZoneName); zoneNameAttr != nil { expr := transformReference(hcl.GetAttrExpr(zoneNameAttr), nRepSpecs, nSpec) repSpecb.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(expr)) @@ -158,13 +157,11 @@ func convertConfigsWithDynamicBlock(specbSrc *hclwrite.Body, diskSizeGB hclwrite configForEach := fmt.Sprintf("%s.%s", nSpec, nConfig) configBlockName := getResourceName(d.block) transformReferences(configBody, configBlockName, nRegion) - regionConfigFile := hclwrite.NewEmptyFile() - regionConfigBody := regionConfigFile.Body() + regionConfigBody := hclwrite.NewEmptyFile().Body() copyAttributesSorted(regionConfigBody, configBody.Attributes()) for _, block := range configBody.Blocks() { blockType := block.Type() - blockFile := hclwrite.NewEmptyFile() - blockBody := blockFile.Body() + blockBody := hclwrite.NewEmptyFile().Body() copyAttributesSorted(blockBody, block.Body().Attributes()) if diskSizeGB != nil && (blockType == nElectableSpecs || blockType == nReadOnlySpecs || blockType == nAnalyticsSpecs) { @@ -176,8 +173,7 @@ func convertConfigsWithDynamicBlock(specbSrc *hclwrite.Body, diskSizeGB hclwrite regionTokens = append(regionTokens, hcl.TokensObject(regionConfigBody)...) regionConfig := hcl.EncloseBracketsNewLines(regionTokens) repSpecb.SetAttributeRaw(nConfig, regionConfig) - numShardsAttr := specbSrc.GetAttribute(nNumShards) - if numShardsAttr != nil { + if numShardsAttr := specbSrc.GetAttribute(nNumShards); numShardsAttr != nil { numShardsExpr := transformReference(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec) tokens := hcl.TokensFromExpr(buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr), false)) tokens = append(tokens, hcl.TokensObject(repSpecb)...) diff --git a/internal/convert/clu2adv.go b/internal/convert/clu2adv.go index 1523bae..d1a3c73 100644 --- a/internal/convert/clu2adv.go +++ b/internal/convert/clu2adv.go @@ -116,8 +116,7 @@ func fillMovedBlocks(body *hclwrite.Body, moveLabels []string) { // fillFreeTierCluster is the entry point to convert clusters in free tier func fillFreeTierCluster(resourceb *hclwrite.Body) error { resourceb.SetAttributeValue(nClusterType, cty.StringVal(valClusterType)) - config := hclwrite.NewEmptyFile() - configb := config.Body() + configb := hclwrite.NewEmptyFile().Body() hcl.SetAttrInt(configb, nPriority, valMaxPriority) if err := hcl.MoveAttr(resourceb, configb, nRegionNameSrc, nRegionName, errFreeCluster); err != nil { return err @@ -128,14 +127,14 @@ func fillFreeTierCluster(resourceb *hclwrite.Body) error { if err := hcl.MoveAttr(resourceb, configb, nBackingProviderName, nBackingProviderName, errFreeCluster); err != nil { return err } - electableSpec := hclwrite.NewEmptyFile() - if err := hcl.MoveAttr(resourceb, electableSpec.Body(), nInstanceSizeSrc, nInstanceSize, errFreeCluster); err != nil { + electableSpecb := hclwrite.NewEmptyFile().Body() + if err := hcl.MoveAttr(resourceb, electableSpecb, nInstanceSizeSrc, nInstanceSize, errFreeCluster); err != nil { return err } - configb.SetAttributeRaw(nElectableSpecs, hcl.TokensObject(electableSpec.Body())) - repSpecs := hclwrite.NewEmptyFile() - repSpecs.Body().SetAttributeRaw(nConfig, hcl.TokensArraySingle(configb)) - resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensArraySingle(repSpecs.Body())) + configb.SetAttributeRaw(nElectableSpecs, hcl.TokensObject(electableSpecb)) + repSpecsb := hclwrite.NewEmptyFile().Body() + repSpecsb.SetAttributeRaw(nConfig, hcl.TokensArraySingle(configb)) + resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensArraySingle(repSpecsb)) return nil } @@ -190,8 +189,7 @@ func fillRepSpecs(resourceb *hclwrite.Body, root attrVals) error { var resultTokens []hclwrite.Tokens var resultBodies []*hclwrite.Body for _, block := range repSpecBlocks { - spec := hclwrite.NewEmptyFile() - specb := spec.Body() + specb := hclwrite.NewEmptyFile().Body() specbSrc := block.Body() _ = hcl.MoveAttr(specbSrc, specb, nZoneName, nZoneName, errRepSpecs) shardsAttr := specbSrc.GetAttribute(nNumShards) @@ -245,8 +243,7 @@ func fillConfigsWithDynamicRegion(specbSrc *hclwrite.Body, root attrVals, change if err != nil || !d.IsPresent() { return dynamicBlock{}, err } - repSpec := hclwrite.NewEmptyFile() - repSpecb := repSpec.Body() + repSpecb := hclwrite.NewEmptyFile().Body() if zoneName := hcl.GetAttrExpr(specbSrc.GetAttribute(nZoneName)); zoneName != "" { repSpecb.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(zoneName)) } @@ -268,7 +265,7 @@ func fillConfigsWithDynamicRegion(specbSrc *hclwrite.Body, root attrVals, change return dynamicBlock{}, fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) } tokens := hcl.TokensFromExpr(buildForExpr("i", fmt.Sprintf("range(%s)", hcl.GetAttrExpr(shards)), false)) - tokens = append(tokens, hcl.EncloseBraces(repSpec.BuildTokens(nil), true)...) + tokens = append(tokens, hcl.EncloseBraces(repSpecb.BuildTokens(nil), true)...) d.tokens = hcl.EncloseBracketsNewLines(tokens) return d, nil } @@ -284,7 +281,7 @@ func fillRegionConfigs(specb, specbSrc *hclwrite.Body, root attrVals) error { if err != nil { return err } - configs = append(configs, config.Body()) + configs = append(configs, config) specbSrc.RemoveBlock(configSrc) } if len(configs) == 0 { @@ -295,9 +292,8 @@ func fillRegionConfigs(specb, specbSrc *hclwrite.Body, root attrVals) error { return nil } -func getRegionConfig(configSrc *hclwrite.Block, root attrVals, isDynamicBlock bool) (*hclwrite.File, error) { - file := hclwrite.NewEmptyFile() - fileb := file.Body() +func getRegionConfig(configSrc *hclwrite.Block, root attrVals, isDynamicBlock bool) (*hclwrite.Body, error) { + fileb := hclwrite.NewEmptyFile().Body() fileb.SetAttributeRaw(nProviderName, root.req[nProviderName]) if err := hcl.MoveAttr(configSrc.Body(), fileb, nRegionName, nRegionName, errRepSpecs); err != nil { return nil, err @@ -317,13 +313,12 @@ func getRegionConfig(configSrc *hclwrite.Block, root attrVals, isDynamicBlock bo if autoScaling := getAutoScalingOpt(root.opt); autoScaling != nil { fileb.SetAttributeRaw(nAutoScaling, autoScaling) } - return file, nil + return fileb, nil } func getSpec(configSrc *hclwrite.Block, countName string, root attrVals, isDynamicBlock bool) (hclwrite.Tokens, error) { var ( - file = hclwrite.NewEmptyFile() - fileb = file.Body() + fileb = hclwrite.NewEmptyFile().Body() count = configSrc.Body().GetAttribute(countName) ) if count == nil { @@ -359,8 +354,7 @@ func getAutoScalingOpt(opt map[string]hclwrite.Tokens) hclwrite.Tokens { {nComputeMaxInstanceSizeSrc, nComputeMaxInstanceSize}, {nComputeScaleDownEnabledSrc, nComputeScaleDownEnabled}, } - file = hclwrite.NewEmptyFile() - fileb = file.Body() + fileb = hclwrite.NewEmptyFile().Body() found = false ) for _, tuple := range names { diff --git a/internal/convert/shared.go b/internal/convert/shared.go index 6791913..af8d6e0 100644 --- a/internal/convert/shared.go +++ b/internal/convert/shared.go @@ -206,8 +206,7 @@ func extractTagsLabelsDynamicBlock(resourceb *hclwrite.Body, name string) (hclwr func extractTagsLabelsIndividual(resourceb *hclwrite.Body, name string) (hclwrite.Tokens, error) { var ( - file = hclwrite.NewEmptyFile() - fileb = file.Body() + fileb = hclwrite.NewEmptyFile().Body() ) blocks := collectBlocks(resourceb, name) if len(blocks) == 0 { From 07a68c422e1dbab41fa7ef671071ae01ad81c5f7 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Mon, 25 Aug 2025 12:06:33 +0200 Subject: [PATCH 40/43] simplify convertRepSpecs --- internal/convert/adv2v2.go | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index 46a4d2d..7b6d697 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -80,22 +80,18 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error blockb := block.Body() shardsAttr := blockb.GetAttribute(nNumShards) blockb.RemoveAttribute(nNumShards) - - // Process config - inlined from convertConfig dConfig, err := getDynamicBlock(blockb, nConfig) if err != nil { return err } - if dConfig.IsPresent() { - blockName := getResourceName(dConfig.block) - transformReferences(dConfig.content.Body(), blockName, nRegion) + transformReferences(dConfig.content.Body(), getResourceName(dConfig.block), nRegion) copyAttributesSorted(dConfig.content.Body(), dConfig.content.Body().Attributes()) processAllSpecs(dConfig.content.Body(), diskSizeGB) tokens := hcl.TokensFromExpr(buildForExpr(nRegion, hcl.GetAttrExpr(dConfig.forEach), false)) tokens = append(tokens, hcl.TokensObject(dConfig.content.Body())...) - blockb.RemoveBlock(dConfig.block) blockb.SetAttributeRaw(nConfig, hcl.EncloseBracketsNewLines(tokens)) + blockb.RemoveBlock(dConfig.block) } else { var configs []*hclwrite.Body for _, configBlock := range collectBlocks(blockb, nConfig) { @@ -149,33 +145,30 @@ func convertConfigsWithDynamicBlock(specbSrc *hclwrite.Body, diskSizeGB hclwrite return dynamicBlock{}, err } configBody := d.content.Body() - repSpecb := hclwrite.NewEmptyFile().Body() - if zoneNameAttr := specbSrc.GetAttribute(nZoneName); zoneNameAttr != nil { - expr := transformReference(hcl.GetAttrExpr(zoneNameAttr), nRepSpecs, nSpec) - repSpecb.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(expr)) - } - configForEach := fmt.Sprintf("%s.%s", nSpec, nConfig) - configBlockName := getResourceName(d.block) - transformReferences(configBody, configBlockName, nRegion) + transformReferences(configBody, getResourceName(d.block), nRegion) regionConfigBody := hclwrite.NewEmptyFile().Body() copyAttributesSorted(regionConfigBody, configBody.Attributes()) for _, block := range configBody.Blocks() { blockType := block.Type() blockBody := hclwrite.NewEmptyFile().Body() copyAttributesSorted(blockBody, block.Body().Attributes()) - if diskSizeGB != nil && (blockType == nElectableSpecs || - blockType == nReadOnlySpecs || blockType == nAnalyticsSpecs) { + if diskSizeGB != nil && + (blockType == nElectableSpecs || blockType == nReadOnlySpecs || blockType == nAnalyticsSpecs) { blockBody.SetAttributeRaw(nDiskSizeGB, diskSizeGB) } regionConfigBody.SetAttributeRaw(blockType, hcl.TokensObject(blockBody)) } - regionTokens := hcl.TokensFromExpr(buildForExpr(nRegion, configForEach, false)) + repSpecb := hclwrite.NewEmptyFile().Body() + if zoneNameAttr := specbSrc.GetAttribute(nZoneName); zoneNameAttr != nil { + repSpecb.SetAttributeRaw(nZoneName, hcl.TokensFromExpr( + transformReference(hcl.GetAttrExpr(zoneNameAttr), nRepSpecs, nSpec))) + } + regionTokens := hcl.TokensFromExpr(buildForExpr(nRegion, fmt.Sprintf("%s.%s", nSpec, nConfig), false)) regionTokens = append(regionTokens, hcl.TokensObject(regionConfigBody)...) - regionConfig := hcl.EncloseBracketsNewLines(regionTokens) - repSpecb.SetAttributeRaw(nConfig, regionConfig) + repSpecb.SetAttributeRaw(nConfig, hcl.EncloseBracketsNewLines(regionTokens)) if numShardsAttr := specbSrc.GetAttribute(nNumShards); numShardsAttr != nil { - numShardsExpr := transformReference(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec) - tokens := hcl.TokensFromExpr(buildForExpr("i", fmt.Sprintf("range(%s)", numShardsExpr), false)) + tokens := hcl.TokensFromExpr(buildForExpr("i", + fmt.Sprintf("range(%s)", transformReference(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec)), false)) tokens = append(tokens, hcl.TokensObject(repSpecb)...) return dynamicBlock{tokens: hcl.EncloseBracketsNewLines(tokens)}, nil } From 65fa29eae6f6566d3dde350878ac89951cb1467e Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Mon, 25 Aug 2025 12:10:52 +0200 Subject: [PATCH 41/43] simplify copyAttributesSorted --- internal/convert/adv2v2.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index 7b6d697..67a9f3d 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -188,8 +188,7 @@ func hasExpectedBlocksAsAttributes(resourceb *hclwrite.Body) bool { return false } -func copyAttributesSorted(targetBody *hclwrite.Body, sourceAttrs map[string]*hclwrite.Attribute, - transforms ...func(string) string) { +func copyAttributesSorted(targetBody *hclwrite.Body, sourceAttrs map[string]*hclwrite.Attribute) { var names []string for name := range sourceAttrs { names = append(names, name) @@ -197,9 +196,6 @@ func copyAttributesSorted(targetBody *hclwrite.Body, sourceAttrs map[string]*hcl slices.Sort(names) for _, name := range names { expr := hcl.GetAttrExpr(sourceAttrs[name]) - for _, transform := range transforms { - expr = transform(expr) - } targetBody.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) } } From 9027f4d51e277871cb17446912b2b199c44d2c31 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Mon, 25 Aug 2025 12:18:11 +0200 Subject: [PATCH 42/43] Improve collectBlocks Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- internal/convert/shared.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/convert/shared.go b/internal/convert/shared.go index af8d6e0..ddb381e 100644 --- a/internal/convert/shared.go +++ b/internal/convert/shared.go @@ -115,13 +115,13 @@ func transformReferences(body *hclwrite.Body, blockName, varName string) { // collectBlocks removes and returns all blocks of the given name from body in order of appearance. func collectBlocks(body *hclwrite.Body, name string) []*hclwrite.Block { var blocks []*hclwrite.Block - for { - block := body.FirstMatchingBlock(name, nil) - if block == nil { - break + for _, block := range body.Blocks() { + if block.Type() == name { + blocks = append(blocks, block) } + } + for _, block := range blocks { body.RemoveBlock(block) - blocks = append(blocks, block) } return blocks } From 308d00b575461872e2c879cf4ceecd35be88bb01 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Mon, 25 Aug 2025 13:41:44 +0200 Subject: [PATCH 43/43] make name more explicit with processNumShardsWhenSomeIsVariable --- internal/convert/adv2v2.go | 2 +- internal/convert/clu2adv.go | 2 +- internal/convert/shared.go | 9 +++------ 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/internal/convert/adv2v2.go b/internal/convert/adv2v2.go index 67a9f3d..2e3e3c3 100644 --- a/internal/convert/adv2v2.go +++ b/internal/convert/adv2v2.go @@ -105,7 +105,7 @@ func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error blockb.SetAttributeRaw(nConfig, hcl.TokensArray(configs)) } if hasVariableShards { - resultTokens = append(resultTokens, processNumShards(shardsAttr, blockb)) + resultTokens = append(resultTokens, processNumShardsWhenSomeIsVariable(shardsAttr, blockb)) continue } numShardsVal := 1 // Default to 1 if num_shards is not set diff --git a/internal/convert/clu2adv.go b/internal/convert/clu2adv.go index d1a3c73..7fae2a3 100644 --- a/internal/convert/clu2adv.go +++ b/internal/convert/clu2adv.go @@ -200,7 +200,7 @@ func fillRepSpecs(resourceb *hclwrite.Body, root attrVals) error { return errConfig } if hasVariableShards { - resultTokens = append(resultTokens, processNumShards(shardsAttr, specb)) + resultTokens = append(resultTokens, processNumShardsWhenSomeIsVariable(shardsAttr, specb)) continue } shardsVal, err := hcl.GetAttrInt(shardsAttr, errNumShards) diff --git a/internal/convert/shared.go b/internal/convert/shared.go index ddb381e..9d11e77 100644 --- a/internal/convert/shared.go +++ b/internal/convert/shared.go @@ -21,9 +21,8 @@ func hasVariableNumShards(blocks []*hclwrite.Block) bool { return false } -// processNumShards handles num_shards for a block, returning tokens for the expanded specs. -// processedBody is the body with num_shards removed and other processing done. -func processNumShards(shardsAttr *hclwrite.Attribute, processedBody *hclwrite.Body) hclwrite.Tokens { +// processNumShardsWhenSomeIsVariable handles num_shards when some replication_specs have variable num_shards +func processNumShardsWhenSomeIsVariable(shardsAttr *hclwrite.Attribute, processedBody *hclwrite.Body) hclwrite.Tokens { if shardsAttr == nil { return hcl.TokensArraySingle(processedBody) // Default 1 if no num_shards specified } @@ -118,11 +117,9 @@ func collectBlocks(body *hclwrite.Body, name string) []*hclwrite.Block { for _, block := range body.Blocks() { if block.Type() == name { blocks = append(blocks, block) + body.RemoveBlock(block) } } - for _, block := range blocks { - body.RemoveBlock(block) - } return blocks }