From c7857b02384f9d6be114de7a5fb33886793ad651 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Mon, 17 Mar 2025 14:57:31 +0100 Subject: [PATCH 01/36] doc assumption for dynamic blocks in tags and labels --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 71e0378..be8e5d5 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,8 @@ Given the different ways of using dynamic blocks, we recommend reviewing the out #### Dynamic blocks in tags and labels -You can use `dynamic` blocks for `tags` and `labels`. You can also combine the use of dynamic blocks in `tags` and `labels` with individual blocks in the same cluster definition, e.g.: +You can use `dynamic` blocks for `tags` and `labels`. The plugin assumes that `for_each` has an expression which is evaluated to a map of strings. +You can also combine the use of dynamic blocks in `tags` and `labels` with individual blocks in the same cluster definition, e.g.: ```hcl tags { key = "environment" From 72a2fbc8b0f2ee82ebcfd569b18ced5d2925eaab Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Mon, 17 Mar 2025 17:49:58 +0100 Subject: [PATCH 02/36] dynamic_regions_config example --- .../clu2adv/dynamic_regions_config.in.tf | 49 +++++++++++++++ .../clu2adv/dynamic_regions_config.out.tf | 59 +++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 internal/convert/testdata/clu2adv/dynamic_regions_config.in.tf create mode 100644 internal/convert/testdata/clu2adv/dynamic_regions_config.out.tf diff --git a/internal/convert/testdata/clu2adv/dynamic_regions_config.in.tf b/internal/convert/testdata/clu2adv/dynamic_regions_config.in.tf new file mode 100644 index 0000000..ae7567b --- /dev/null +++ b/internal/convert/testdata/clu2adv/dynamic_regions_config.in.tf @@ -0,0 +1,49 @@ +resource "mongodbatlas_cluster" "dynamic_regions_config" { + project_id = var.project_id + name = "cluster" + cluster_type = "SHARDED" + provider_name = "AWS" + provider_instance_size_name = "M10" + replication_specs { + num_shards = var.replication_specs.num_shards + dynamic "regions_config" { + for_each = var.replication_specs.regions_config + content { + region_name = regions_config.value.region_name + electable_nodes = regions_config.value.electable_nodes + priority = regions_config.value.priority + read_only_nodes = regions_config.value.read_only_nodes + } + } + } +} + +# example of variable for demostration purposes, not used in the conversion +variable "replication_specs" { + type = object({ + num_shards = number + regions_config = set(object({ + region_name = string + electable_nodes = number + priority = number + read_only_nodes = number + })) + }) + default = { + num_shards = 3 + regions_config = [ + { + region_name = "US_EAST_1" + electable_nodes = 3 + priority = 7 + read_only_nodes = 0 + }, + { + region_name = "US_WEST_2" + electable_nodes = 2 + priority = 6 + read_only_nodes = 1 + } + ] + } +} diff --git a/internal/convert/testdata/clu2adv/dynamic_regions_config.out.tf b/internal/convert/testdata/clu2adv/dynamic_regions_config.out.tf new file mode 100644 index 0000000..d276314 --- /dev/null +++ b/internal/convert/testdata/clu2adv/dynamic_regions_config.out.tf @@ -0,0 +1,59 @@ +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) : { + region_configs = flatten([ + for priority in [7, 6, 5, 4, 3, 2, 1, 0] : [ + for region in var.replication_specs.regions_config : { + provider_name = "AWS" + region_name = region.region_name + priority = region.priority + electable_specs = region.electable_nodes > 0 ? { + node_count = region.electable_nodes + instance_size = "M10" + } : null + read_only_specs = region.read_only_nodes > 0 ? { + node_count = region.read_only_nodes + instance_size = "M10" + } : null + } if region.priority == priority + ] + ]) + } + ] + + # Generated by atlas-cli-plugin-terraform. + # Please confirm that all references to this resource are updated. +} + +# example of variable for demostration purposes, not used in the conversion +variable "replication_specs" { + type = object({ + num_shards = number + regions_config = set(object({ + region_name = string + electable_nodes = number + priority = number + read_only_nodes = number + })) + }) + default = { + num_shards = 3 + regions_config = [ + { + region_name = "US_EAST_1" + electable_nodes = 3 + priority = 7 + read_only_nodes = 0 + }, + { + region_name = "US_WEST_2" + electable_nodes = 2 + priority = 6 + read_only_nodes = 1 + } + ] + } +} From 26d21833ab18f59e313835218ad284393b4b1d91 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Mon, 17 Mar 2025 18:14:51 +0100 Subject: [PATCH 03/36] range for priorities --- internal/convert/testdata/clu2adv/dynamic_regions_config.out.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/convert/testdata/clu2adv/dynamic_regions_config.out.tf b/internal/convert/testdata/clu2adv/dynamic_regions_config.out.tf index d276314..bc1d70d 100644 --- a/internal/convert/testdata/clu2adv/dynamic_regions_config.out.tf +++ b/internal/convert/testdata/clu2adv/dynamic_regions_config.out.tf @@ -5,7 +5,7 @@ resource "mongodbatlas_advanced_cluster" "dynamic_regions_config" { replication_specs = [ for i in range(var.replication_specs.num_shards) : { region_configs = flatten([ - for priority in [7, 6, 5, 4, 3, 2, 1, 0] : [ + for priority in range(7, 0, -1) : [ for region in var.replication_specs.regions_config : { provider_name = "AWS" region_name = region.region_name From cf5e3f7803b38622a03b8551ce382f2ef54cbb51 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Tue, 18 Mar 2025 08:49:01 +0100 Subject: [PATCH 04/36] allow dynamic block in regions_config --- internal/convert/convert.go | 2 +- internal/convert/testdata/clu2adv/errors.json | 1 - .../regions_config_unsupported_dynamic.in.tf | 38 ------------------- 3 files changed, 1 insertion(+), 40 deletions(-) delete mode 100644 internal/convert/testdata/clu2adv/regions_config_unsupported_dynamic.in.tf diff --git a/internal/convert/convert.go b/internal/convert/convert.go index 74ff298..abb3ef7 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -37,7 +37,7 @@ const ( ) var ( - dynamicBlockAllowList = []string{nTags, nLabels} + dynamicBlockAllowList = []string{nTags, nLabels, nConfigSrc} ) type attrVals struct { diff --git a/internal/convert/testdata/clu2adv/errors.json b/internal/convert/testdata/clu2adv/errors.json index 7a04706..4147bcc 100644 --- a/internal/convert/testdata/clu2adv/errors.json +++ b/internal/convert/testdata/clu2adv/errors.json @@ -8,7 +8,6 @@ "regions_config_out_of_range_priority": "setting priority: priority is 0 but must be between 1 and 7", "regions_config_non_literal_priority": "setting priority: failed to evaluate number", "replication_specs_unsupported_dynamic": "dynamic blocks are not supported", - "regions_config_unsupported_dynamic": "dynamic blocks are not supported", "replication_specs_non_literal_num_shards": "setting num_shards: failed to evaluate number", "replication_specs_missing_num_shards": "num_shards not found" } diff --git a/internal/convert/testdata/clu2adv/regions_config_unsupported_dynamic.in.tf b/internal/convert/testdata/clu2adv/regions_config_unsupported_dynamic.in.tf deleted file mode 100644 index 7934936..0000000 --- a/internal/convert/testdata/clu2adv/regions_config_unsupported_dynamic.in.tf +++ /dev/null @@ -1,38 +0,0 @@ -resource "mongodbatlas_cluster" "dynamic_region" { - project_id = var.project_id - name = "dynamic" - num_shards = 1 - cluster_type = "REPLICASET" - provider_name = "AWS" - provider_instance_size_name = "M10" - - replication_specs { - num_shards = 1 - dynamic "regions_config" { - for_each = { - US_WEST_2 = { - electable_nodes = 3 - priority = 6 - read_only_nodes = 0 - } - US_WEST_1 = { - electable_nodes = 1 - priority = 5 - read_only_nodes = 0 - } - US_EAST_1 = { - electable_nodes = 3 - priority = 7 - read_only_nodes = 0 - } - } - content { - region_name = regions_config.key - electable_nodes = regions_config.value.electable_nodes - priority = regions_config.value.priority - read_only_nodes = regions_config.value.read_only_nodes - } - - } - } -} From 771845c8d238725937fc0a0239a017e2014935f7 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Tue, 18 Mar 2025 08:51:07 +0100 Subject: [PATCH 05/36] doc --- CHANGELOG.md | 2 +- README.md | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65a778f..6d6920c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ENHANCEMENTS: -* Supports `dynamic` block for `tags` and `labels` +* Supports `dynamic` block for `tags`, `labels` and `regions_config` ## 1.0.0 (Mar 6, 2025) diff --git a/README.md b/README.md index be8e5d5..4879f01 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Given the different ways of using dynamic blocks, we recommend reviewing the out #### Dynamic blocks in tags and labels -You can use `dynamic` blocks for `tags` and `labels`. The plugin assumes that `for_each` has an expression which is evaluated to a map of strings. +You can use `dynamic` blocks for `tags` and `labels`. The plugin assumes that `for_each` has an expression which is evaluated to a `map` of strings. You can also combine the use of dynamic blocks in `tags` and `labels` with individual blocks in the same cluster definition, e.g.: ```hcl tags { @@ -73,12 +73,17 @@ dynamic "tags" { } ``` +#### Dynamic blocks in regions_config + +You can use `dynamic` blocks for `regions_config`. The plugin assumes that `for_each` has an expression which is evaluated to a `list` or `set` of objects. +Dynamic block and individual blocks for `regions_config` are not supported at the same time in a `replication_specs`. + ### Limitations - The plugin doesn't support `regions_config` without `electable_nodes` as there can be some issues with `priority` when they only have `analytics_nodes` and/or `electable_nodes`. - [`priority`](https://registry.terraform.io/providers/mongodb/mongodbatlas/latest/docs/resources/cluster#priority-1) is required in `regions_config` and must be a numeric [literal expression](https://developer.hashicorp.com/nomad/docs/job-specification/hcl2/expressions#literal-expressions) between 7 and 1, e.g. `var.priority` is not supported. This is to allow reordering them by descending priority as this is expected in `mongodbatlas_advanced_cluster`. - [`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`. -- `dynamic` blocks are currently supported only for `tags` and `labels`. **Coming soon**: support for `replication_specs` and `regions_config`. +- `dynamic` blocks are currently supported only for `tags`, `labels` and `regions_config`. **Coming soon**: support for `replication_specs`. ## Contributing From 5dfbedefa14704dcf6735e362a311fcc6f4a6f62 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Tue, 18 Mar 2025 09:11:08 +0100 Subject: [PATCH 06/36] update comment --- internal/convert/convert.go | 2 +- .../clu2adv/adv_config_bi_connector_pinned_fcv.out.tf | 2 +- .../clu2adv/analytics_read_only_all_params.out.tf | 2 +- .../clu2adv/analytics_read_only_min_params.out.tf | 2 +- internal/convert/testdata/clu2adv/autoscaling.out.tf | 2 +- internal/convert/testdata/clu2adv/data_sources.out.tf | 4 ++-- .../testdata/clu2adv/dynamic_regions_config.out.tf | 2 +- .../testdata/clu2adv/dynamic_tags_labels.out.tf | 10 +++++----- .../testdata/clu2adv/free_cluster_with_count.out.tf | 2 +- .../testdata/clu2adv/includeMoved_multiple.out.tf | 8 ++++---- .../testdata/clu2adv/includeMoved_single.out.tf | 2 +- internal/convert/testdata/clu2adv/multi_region.out.tf | 2 +- .../testdata/clu2adv/multi_replicaction_specs.out.tf | 4 ++-- .../testdata/clu2adv/tags_labels_timeouts.out.tf | 6 +++--- test/e2e/testdata/clu2adv.expected.tf | 2 +- test/e2e/testdata/clu2adv.expected_moved.tf | 2 +- 16 files changed, 27 insertions(+), 27 deletions(-) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index abb3ef7..5ef133b 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -31,7 +31,7 @@ const ( errNumShards = "setting " + nNumShards commentGeneratedBy = "Generated by atlas-cli-plugin-terraform." - commentConfirmReferences = "Please confirm that all references to this resource are updated." + commentConfirmReferences = "Please review the changes and confirm that references to this resource are updated." commentMovedBlock = "Moved blocks" commentRemovedOld = "Note: Remember to remove or comment out the old cluster definitions." ) diff --git a/internal/convert/testdata/clu2adv/adv_config_bi_connector_pinned_fcv.out.tf b/internal/convert/testdata/clu2adv/adv_config_bi_connector_pinned_fcv.out.tf index 603b42f..f1f7239 100644 --- a/internal/convert/testdata/clu2adv/adv_config_bi_connector_pinned_fcv.out.tf +++ b/internal/convert/testdata/clu2adv/adv_config_bi_connector_pinned_fcv.out.tf @@ -32,5 +32,5 @@ resource "mongodbatlas_advanced_cluster" "this" { } # Generated by atlas-cli-plugin-terraform. - # Please confirm that all references to this resource are updated. + # Please review the changes and confirm that references to this resource are updated. } diff --git a/internal/convert/testdata/clu2adv/analytics_read_only_all_params.out.tf b/internal/convert/testdata/clu2adv/analytics_read_only_all_params.out.tf index b8f0db7..85407ea 100644 --- a/internal/convert/testdata/clu2adv/analytics_read_only_all_params.out.tf +++ b/internal/convert/testdata/clu2adv/analytics_read_only_all_params.out.tf @@ -36,5 +36,5 @@ resource "mongodbatlas_advanced_cluster" "ar" { ] # Generated by atlas-cli-plugin-terraform. - # Please confirm that all references to this resource are updated. + # Please review the changes and confirm that references to this resource are updated. } diff --git a/internal/convert/testdata/clu2adv/analytics_read_only_min_params.out.tf b/internal/convert/testdata/clu2adv/analytics_read_only_min_params.out.tf index cb8c9ea..c4681a9 100644 --- a/internal/convert/testdata/clu2adv/analytics_read_only_min_params.out.tf +++ b/internal/convert/testdata/clu2adv/analytics_read_only_min_params.out.tf @@ -27,5 +27,5 @@ resource "mongodbatlas_advanced_cluster" "ar" { ] # Generated by atlas-cli-plugin-terraform. - # Please confirm that all references to this resource are updated. + # Please review the changes and confirm that references to this resource are updated. } diff --git a/internal/convert/testdata/clu2adv/autoscaling.out.tf b/internal/convert/testdata/clu2adv/autoscaling.out.tf index c1bbddb..97ab2e6 100644 --- a/internal/convert/testdata/clu2adv/autoscaling.out.tf +++ b/internal/convert/testdata/clu2adv/autoscaling.out.tf @@ -36,5 +36,5 @@ resource "mongodbatlas_advanced_cluster" "autoscaling" { ] # Generated by atlas-cli-plugin-terraform. - # Please confirm that all references to this resource are updated. + # Please review the changes and confirm that references to this resource are updated. } diff --git a/internal/convert/testdata/clu2adv/data_sources.out.tf b/internal/convert/testdata/clu2adv/data_sources.out.tf index fb8561d..2ba949f 100644 --- a/internal/convert/testdata/clu2adv/data_sources.out.tf +++ b/internal/convert/testdata/clu2adv/data_sources.out.tf @@ -6,7 +6,7 @@ data "mongodbatlas_advanced_cluster" "singular" { use_replication_spec_per_shard = true # Generated by atlas-cli-plugin-terraform. - # Please confirm that all references to this resource are updated. + # Please review the changes and confirm that references to this resource are updated. } data "mongodbatlas_advanced_clusters" "plural" { @@ -15,7 +15,7 @@ data "mongodbatlas_advanced_clusters" "plural" { use_replication_spec_per_shard = true # Generated by atlas-cli-plugin-terraform. - # Please confirm that all references to this resource are updated. + # Please review the changes and confirm that references to this resource are updated. } data "mongodbatlas_advanced_cluster" "adv_singular" { diff --git a/internal/convert/testdata/clu2adv/dynamic_regions_config.out.tf b/internal/convert/testdata/clu2adv/dynamic_regions_config.out.tf index bc1d70d..ab48c3c 100644 --- a/internal/convert/testdata/clu2adv/dynamic_regions_config.out.tf +++ b/internal/convert/testdata/clu2adv/dynamic_regions_config.out.tf @@ -25,7 +25,7 @@ resource "mongodbatlas_advanced_cluster" "dynamic_regions_config" { ] # Generated by atlas-cli-plugin-terraform. - # Please confirm that all references to this resource are updated. + # Please review the changes and confirm that references to this resource are updated. } # example of variable for demostration purposes, not used in the conversion diff --git a/internal/convert/testdata/clu2adv/dynamic_tags_labels.out.tf b/internal/convert/testdata/clu2adv/dynamic_tags_labels.out.tf index 3e78a8f..471ccf7 100644 --- a/internal/convert/testdata/clu2adv/dynamic_tags_labels.out.tf +++ b/internal/convert/testdata/clu2adv/dynamic_tags_labels.out.tf @@ -20,7 +20,7 @@ resource "mongodbatlas_advanced_cluster" "simplified" { tags = var.tags # Generated by atlas-cli-plugin-terraform. - # Please confirm that all references to this resource are updated. + # Please review the changes and confirm that references to this resource are updated. } resource "mongodbatlas_advanced_cluster" "expression" { @@ -47,7 +47,7 @@ resource "mongodbatlas_advanced_cluster" "expression" { } # Generated by atlas-cli-plugin-terraform. - # Please confirm that all references to this resource are updated. + # Please review the changes and confirm that references to this resource are updated. } resource "mongodbatlas_advanced_cluster" "simplified_individual" { @@ -78,7 +78,7 @@ resource "mongodbatlas_advanced_cluster" "simplified_individual" { ) # Generated by atlas-cli-plugin-terraform. - # Please confirm that all references to this resource are updated. + # Please review the changes and confirm that references to this resource are updated. } resource "mongodbatlas_advanced_cluster" "expression_individual" { @@ -111,7 +111,7 @@ resource "mongodbatlas_advanced_cluster" "expression_individual" { ) # Generated by atlas-cli-plugin-terraform. - # Please confirm that all references to this resource are updated. + # Please review the changes and confirm that references to this resource are updated. } resource "mongodbatlas_advanced_cluster" "full_example" { @@ -157,5 +157,5 @@ resource "mongodbatlas_advanced_cluster" "full_example" { ) # Generated by atlas-cli-plugin-terraform. - # Please confirm that all references to this resource are updated. + # Please review the changes and confirm that references to this resource are updated. } diff --git a/internal/convert/testdata/clu2adv/free_cluster_with_count.out.tf b/internal/convert/testdata/clu2adv/free_cluster_with_count.out.tf index c24b56f..c7dbdbf 100644 --- a/internal/convert/testdata/clu2adv/free_cluster_with_count.out.tf +++ b/internal/convert/testdata/clu2adv/free_cluster_with_count.out.tf @@ -26,5 +26,5 @@ resource "mongodbatlas_advanced_cluster" "free_cluster" { # comment in the resou ] # Generated by atlas-cli-plugin-terraform. - # Please confirm that all references to this resource are updated. + # Please review the changes and confirm that references to this resource are updated. } diff --git a/internal/convert/testdata/clu2adv/includeMoved_multiple.out.tf b/internal/convert/testdata/clu2adv/includeMoved_multiple.out.tf index 8a11ff4..436ca65 100644 --- a/internal/convert/testdata/clu2adv/includeMoved_multiple.out.tf +++ b/internal/convert/testdata/clu2adv/includeMoved_multiple.out.tf @@ -19,7 +19,7 @@ resource "mongodbatlas_advanced_cluster" "cluster1" { ] # Generated by atlas-cli-plugin-terraform. - # Please confirm that all references to this resource are updated. + # Please review the changes and confirm that references to this resource are updated. } resource "mongodbatlas_advanced_cluster" "cluster2" { @@ -43,7 +43,7 @@ resource "mongodbatlas_advanced_cluster" "cluster2" { ] # Generated by atlas-cli-plugin-terraform. - # Please confirm that all references to this resource are updated. + # Please review the changes and confirm that references to this resource are updated. } resource "mongodbatlas_advanced_cluster" "count" { @@ -69,7 +69,7 @@ resource "mongodbatlas_advanced_cluster" "count" { ] # Generated by atlas-cli-plugin-terraform. - # Please confirm that all references to this resource are updated. + # Please review the changes and confirm that references to this resource are updated. } resource "mongodbatlas_advanced_cluster" "forEach" { @@ -95,7 +95,7 @@ resource "mongodbatlas_advanced_cluster" "forEach" { ] # Generated by atlas-cli-plugin-terraform. - # Please confirm that all references to this resource are updated. + # Please review the changes and confirm that references to this resource are updated. } resource "another_resource" "another" { diff --git a/internal/convert/testdata/clu2adv/includeMoved_single.out.tf b/internal/convert/testdata/clu2adv/includeMoved_single.out.tf index 5a49d34..fc8c497 100644 --- a/internal/convert/testdata/clu2adv/includeMoved_single.out.tf +++ b/internal/convert/testdata/clu2adv/includeMoved_single.out.tf @@ -19,7 +19,7 @@ resource "mongodbatlas_advanced_cluster" "cluster" { ] # Generated by atlas-cli-plugin-terraform. - # Please confirm that all references to this resource are updated. + # Please review the changes and confirm that references to this resource are updated. } # Moved blocks diff --git a/internal/convert/testdata/clu2adv/multi_region.out.tf b/internal/convert/testdata/clu2adv/multi_region.out.tf index facda3c..b7bbd54 100644 --- a/internal/convert/testdata/clu2adv/multi_region.out.tf +++ b/internal/convert/testdata/clu2adv/multi_region.out.tf @@ -43,5 +43,5 @@ resource "mongodbatlas_advanced_cluster" "multi_region" { ] # Generated by atlas-cli-plugin-terraform. - # Please confirm that all references to this resource are updated. + # Please review the changes and confirm that references to this resource are updated. } diff --git a/internal/convert/testdata/clu2adv/multi_replicaction_specs.out.tf b/internal/convert/testdata/clu2adv/multi_replicaction_specs.out.tf index 290011d..cbb3526 100644 --- a/internal/convert/testdata/clu2adv/multi_replicaction_specs.out.tf +++ b/internal/convert/testdata/clu2adv/multi_replicaction_specs.out.tf @@ -37,7 +37,7 @@ resource "mongodbatlas_advanced_cluster" "multirep" { ] # Generated by atlas-cli-plugin-terraform. - # Please confirm that all references to this resource are updated. + # Please review the changes and confirm that references to this resource are updated. } resource "mongodbatlas_advanced_cluster" "geo" { @@ -124,5 +124,5 @@ resource "mongodbatlas_advanced_cluster" "geo" { ] # Generated by atlas-cli-plugin-terraform. - # Please confirm that all references to this resource are updated. + # Please review the changes and confirm that references to this resource are updated. } diff --git a/internal/convert/testdata/clu2adv/tags_labels_timeouts.out.tf b/internal/convert/testdata/clu2adv/tags_labels_timeouts.out.tf index 94bf061..9127502 100644 --- a/internal/convert/testdata/clu2adv/tags_labels_timeouts.out.tf +++ b/internal/convert/testdata/clu2adv/tags_labels_timeouts.out.tf @@ -22,7 +22,7 @@ resource "mongodbatlas_advanced_cluster" "basictags" { } # Generated by atlas-cli-plugin-terraform. - # Please confirm that all references to this resource are updated. + # Please review the changes and confirm that references to this resource are updated. } resource "mongodbatlas_advanced_cluster" "basictimeouts" { @@ -49,7 +49,7 @@ resource "mongodbatlas_advanced_cluster" "basictimeouts" { } # Generated by atlas-cli-plugin-terraform. - # Please confirm that all references to this resource are updated. + # Please review the changes and confirm that references to this resource are updated. } resource "mongodbatlas_advanced_cluster" "all" { @@ -88,5 +88,5 @@ resource "mongodbatlas_advanced_cluster" "all" { } # Generated by atlas-cli-plugin-terraform. - # Please confirm that all references to this resource are updated. + # Please review the changes and confirm that references to this resource are updated. } diff --git a/test/e2e/testdata/clu2adv.expected.tf b/test/e2e/testdata/clu2adv.expected.tf index 684dafe..0544aee 100644 --- a/test/e2e/testdata/clu2adv.expected.tf +++ b/test/e2e/testdata/clu2adv.expected.tf @@ -22,5 +22,5 @@ resource "mongodbatlas_advanced_cluster" "cluster" { } # Generated by atlas-cli-plugin-terraform. - # Please confirm that all references to this resource are updated. + # Please review the changes and confirm that references to this resource are updated. } diff --git a/test/e2e/testdata/clu2adv.expected_moved.tf b/test/e2e/testdata/clu2adv.expected_moved.tf index 6ef84c3..5fbfb91 100644 --- a/test/e2e/testdata/clu2adv.expected_moved.tf +++ b/test/e2e/testdata/clu2adv.expected_moved.tf @@ -22,7 +22,7 @@ resource "mongodbatlas_advanced_cluster" "cluster" { } # Generated by atlas-cli-plugin-terraform. - # Please confirm that all references to this resource are updated. + # Please review the changes and confirm that references to this resource are updated. } # Moved blocks From 17a6444cc71f4e490f706d3403ee0dd7d0857c33 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Tue, 18 Mar 2025 09:45:44 +0100 Subject: [PATCH 07/36] minimum implementation to have test failing because difference in golden file --- internal/convert/convert.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index 5ef133b..3125946 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -202,11 +202,11 @@ func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error { break } specbSrc := specSrc.Body() - if err := checkDynamicBlock(specbSrc); err != nil { - return err - } // ok to fail as zone_name is optional _ = hcl.MoveAttr(specbSrc, specb, nZoneName, nZoneName, errRepSpecs) + if hasDynamicBlock, err := fillRegionConfigsDynamicBlock(specb, specbSrc, root); err != nil || hasDynamicBlock { + return err + } shards := specbSrc.GetAttribute(nNumShards) if shards == nil { return fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) @@ -306,6 +306,14 @@ func fillBlockOpt(resourceb *hclwrite.Body, name string) { resourceb.SetAttributeRaw(name, hcl.TokensObject(block.Body())) } +func fillRegionConfigsDynamicBlock(specb, specbSrc *hclwrite.Body, root attrVals) (bool, error) { + d, err := getDynamicBlock(specbSrc, nConfigSrc) + if err != nil || d.forEach == nil { + return false, err + } + return true, nil +} + func fillRegionConfigs(specb, specbSrc *hclwrite.Body, root attrVals) error { var configs []*hclwrite.Body for { From 0f10bb9baef11c27c0eb8e38728121053e7b08a5 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Tue, 18 Mar 2025 12:29:03 +0100 Subject: [PATCH 08/36] export enclose funcs --- internal/hcl/hcl.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/hcl/hcl.go b/internal/hcl/hcl.go index 46f661c..179ff47 100644 --- a/internal/hcl/hcl.go +++ b/internal/hcl/hcl.go @@ -92,7 +92,7 @@ func TokensArray(bodies []*hclwrite.Body) hclwrite.Tokens { } ret = append(ret, joinTokens(tokens...)...) ret = append(ret, tokenNewLine) - return encloseBrackets(ret) + return EncloseBrackets(ret) } // TokensArraySingle creates an array of one object. @@ -104,7 +104,7 @@ func TokensArraySingle(body *hclwrite.Body) hclwrite.Tokens { func TokensObject(body *hclwrite.Body) hclwrite.Tokens { tokens := hclwrite.Tokens{tokenNewLine} tokens = append(tokens, RemoveLeadingNewline(body.BuildTokens(nil))...) - return encloseBraces(tokens) + return EncloseBraces(tokens) } // TokensFromExpr creates the tokens for an expression provided as a string. @@ -117,7 +117,7 @@ func TokensObjectFromExpr(expr string) hclwrite.Tokens { tokens := hclwrite.Tokens{tokenNewLine} tokens = append(tokens, TokensFromExpr(expr)...) tokens = append(tokens, tokenNewLine) - return encloseBraces(tokens) + return EncloseBraces(tokens) } // TokensFuncMerge creates the tokens for the HCL merge function. @@ -126,7 +126,7 @@ func TokensFuncMerge(tokens ...hclwrite.Tokens) hclwrite.Tokens { params = append(params, joinTokens(tokens...)...) params = append(params, tokenNewLine) ret := hclwrite.Tokens{{Type: hclsyntax.TokenIdent, Bytes: []byte("merge")}} - return append(ret, encloseParens(params)...) + return append(ret, EncloseParens(params)...) } // RemoveLeadingNewline removes the first newline if it exists to make the output prettier. @@ -170,22 +170,22 @@ func joinTokens(tokens ...hclwrite.Tokens) hclwrite.Tokens { return ret } -// encloseParens encloses tokens with parentheses, ( ). -func encloseParens(tokens hclwrite.Tokens) hclwrite.Tokens { +// EncloseParens encloses tokens with parentheses, ( ). +func EncloseParens(tokens hclwrite.Tokens) hclwrite.Tokens { ret := hclwrite.Tokens{{Type: hclsyntax.TokenOParen, Bytes: []byte("(")}} ret = append(ret, tokens...) return append(ret, &hclwrite.Token{Type: hclsyntax.TokenCParen, Bytes: []byte(")")}) } -// encloseBraces encloses tokens with curly braces, { }. -func encloseBraces(tokens hclwrite.Tokens) hclwrite.Tokens { +// EncloseBraces encloses tokens with curly braces, { }. +func EncloseBraces(tokens hclwrite.Tokens) hclwrite.Tokens { ret := hclwrite.Tokens{{Type: hclsyntax.TokenOBrace, Bytes: []byte("{")}} ret = append(ret, tokens...) return append(ret, &hclwrite.Token{Type: hclsyntax.TokenCBrace, Bytes: []byte("}")}) } -// encloseBrackets encloses tokens with square brackets, [ ]. -func encloseBrackets(tokens hclwrite.Tokens) hclwrite.Tokens { +// EncloseBrackets encloses tokens with square brackets, [ ]. +func EncloseBrackets(tokens hclwrite.Tokens) hclwrite.Tokens { ret := hclwrite.Tokens{{Type: hclsyntax.TokenOBrack, Bytes: []byte("[")}} ret = append(ret, tokens...) return append(ret, &hclwrite.Token{Type: hclsyntax.TokenCBrack, Bytes: []byte("]")}) From 44a4bc72b76256d1f815c90b58b9fd2bbc3c1129 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Tue, 18 Mar 2025 13:03:18 +0100 Subject: [PATCH 09/36] create EncloseNewLines and remove SetAttrExpr --- internal/convert/convert.go | 4 ++-- internal/hcl/hcl.go | 29 +++++++++++------------------ 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index 3125946..153b569 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -129,8 +129,8 @@ func fillMovedBlocks(body *hclwrite.Body, moveLabels []string) { for i, moveLabel := range moveLabels { block := body.AppendNewBlock(nMoved, nil) blockb := block.Body() - hcl.SetAttrExpr(blockb, nFrom, fmt.Sprintf("%s.%s", cluster, moveLabel)) - hcl.SetAttrExpr(blockb, nTo, fmt.Sprintf("%s.%s", advCluster, moveLabel)) + blockb.SetAttributeRaw(nFrom, hcl.TokensFromExpr(fmt.Sprintf("%s.%s", cluster, moveLabel))) + blockb.SetAttributeRaw(nTo, hcl.TokensFromExpr(fmt.Sprintf("%s.%s", advCluster, moveLabel))) if i < len(moveLabels)-1 { body.AppendNewline() } diff --git a/internal/hcl/hcl.go b/internal/hcl/hcl.go index 179ff47..c8e7da9 100644 --- a/internal/hcl/hcl.go +++ b/internal/hcl/hcl.go @@ -31,12 +31,6 @@ func PopAttr(body *hclwrite.Body, attrName, errPrefix string) (hclwrite.Tokens, return tokens, nil } -// SetAttrExpr sets an attribute to an expression (possibly with interpolations) without quotes. -func SetAttrExpr(body *hclwrite.Body, attrName, expresion string) { - tokens := hclwrite.Tokens{{Type: hclsyntax.TokenIdent, Bytes: []byte(expresion)}} - body.SetAttributeRaw(attrName, tokens) -} - // GetAttrExpr returns the expression of an attribute as a string. func GetAttrExpr(attr *hclwrite.Attribute) string { return strings.TrimSpace(string(attr.Expr().BuildTokens(nil).Bytes())) @@ -85,14 +79,11 @@ func GetAttrString(attr *hclwrite.Attribute, errPrefix string) (string, error) { // TokensArray creates an array of objects. func TokensArray(bodies []*hclwrite.Body) hclwrite.Tokens { - ret := hclwrite.Tokens{tokenNewLine} tokens := make([]hclwrite.Tokens, 0) for i := range bodies { tokens = append(tokens, TokensObject(bodies[i])) } - ret = append(ret, joinTokens(tokens...)...) - ret = append(ret, tokenNewLine) - return EncloseBrackets(ret) + return EncloseBrackets(EncloseNewLines(joinTokens(tokens...))) } // TokensArraySingle creates an array of one object. @@ -114,18 +105,13 @@ func TokensFromExpr(expr string) hclwrite.Tokens { // TokensObjectFromExpr creates an object with an expression. func TokensObjectFromExpr(expr string) hclwrite.Tokens { - tokens := hclwrite.Tokens{tokenNewLine} - tokens = append(tokens, TokensFromExpr(expr)...) - tokens = append(tokens, tokenNewLine) - return EncloseBraces(tokens) + return EncloseBraces(EncloseNewLines(TokensFromExpr(expr))) } // TokensFuncMerge creates the tokens for the HCL merge function. func TokensFuncMerge(tokens ...hclwrite.Tokens) hclwrite.Tokens { - params := hclwrite.Tokens{tokenNewLine} - params = append(params, joinTokens(tokens...)...) - params = append(params, tokenNewLine) - ret := hclwrite.Tokens{{Type: hclsyntax.TokenIdent, Bytes: []byte("merge")}} + params := EncloseNewLines(joinTokens(tokens...)) + ret := TokensFromExpr("merge") return append(ret, EncloseParens(params)...) } @@ -190,3 +176,10 @@ func EncloseBrackets(tokens hclwrite.Tokens) hclwrite.Tokens { ret = append(ret, tokens...) return append(ret, &hclwrite.Token{Type: hclsyntax.TokenCBrack, Bytes: []byte("]")}) } + +// EncloseNewLines encloses tokens with newlines at the beginning and end. +func EncloseNewLines(tokens hclwrite.Tokens) hclwrite.Tokens { + ret := hclwrite.Tokens{tokenNewLine} + ret = append(ret, tokens...) + return append(ret, tokenNewLine) +} From 130009b7188bb3e5d41678ae6ae19278bc81f62a Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Tue, 18 Mar 2025 13:10:03 +0100 Subject: [PATCH 10/36] root replication_specs --- internal/convert/convert.go | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index 153b569..68c2625 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -204,9 +204,15 @@ func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error { specbSrc := specSrc.Body() // ok to fail as zone_name is optional _ = hcl.MoveAttr(specbSrc, specb, nZoneName, nZoneName, errRepSpecs) - if hasDynamicBlock, err := fillRegionConfigsDynamicBlock(specb, specbSrc, root); err != nil || hasDynamicBlock { + d, err := fillRegionConfigsDynamicBlock(specbSrc, root) + if err != nil { return err } + if d.IsPresent() { + resourceb.RemoveBlock(specSrc) + resourceb.SetAttributeRaw(nRepSpecs, d.tokens) + return nil + } shards := specbSrc.GetAttribute(nNumShards) if shards == nil { return fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) @@ -251,7 +257,7 @@ func fillTagsLabelsOpt(resourceb *hclwrite.Body, name string) error { func extractTagsLabelsDynamicBlock(resourceb *hclwrite.Body, name string) (hclwrite.Tokens, error) { d, err := getDynamicBlock(resourceb, name) - if err != nil || d.forEach == nil { + if err != nil || !d.IsPresent() { return nil, err } key := d.content.Body().GetAttribute(nKey) @@ -306,12 +312,13 @@ func fillBlockOpt(resourceb *hclwrite.Body, name string) { resourceb.SetAttributeRaw(name, hcl.TokensObject(block.Body())) } -func fillRegionConfigsDynamicBlock(specb, specbSrc *hclwrite.Body, root attrVals) (bool, error) { +func fillRegionConfigsDynamicBlock(specbSrc *hclwrite.Body, root attrVals) (dynamicBlock, error) { d, err := getDynamicBlock(specbSrc, nConfigSrc) - if err != nil || d.forEach == nil { - return false, err + if err != nil || !d.IsPresent() { + return dynamicBlock{}, err } - return true, nil + d.tokens = hcl.EncloseBrackets(hcl.EncloseNewLines(hcl.TokensFromExpr("for statement"))) + return d, nil } func fillRegionConfigs(specb, specbSrc *hclwrite.Body, root attrVals) error { @@ -463,6 +470,11 @@ 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 getDynamicBlock(body *hclwrite.Body, name string) (dynamicBlock, error) { From b1dc050870f1602215cf82bf1458221f5203c991 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:41:14 +0100 Subject: [PATCH 11/36] remove priority checks about numerical literal --- internal/convert/convert.go | 22 +++---------------- internal/convert/testdata/clu2adv/errors.json | 4 +--- .../regions_config_non_literal_priority.in.tf | 16 -------------- ...regions_config_out_of_range_priority.in.tf | 16 -------------- 4 files changed, 4 insertions(+), 54 deletions(-) delete mode 100644 internal/convert/testdata/clu2adv/regions_config_non_literal_priority.in.tf delete mode 100644 internal/convert/testdata/clu2adv/regions_config_out_of_range_priority.in.tf diff --git a/internal/convert/convert.go b/internal/convert/convert.go index 68c2625..0a0e935 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -22,7 +22,6 @@ const ( advClusterPlural = "mongodbatlas_advanced_clusters" valClusterType = "REPLICASET" valMaxPriority = 7 - valMinPriority = 1 errFreeCluster = "free cluster (because no " + nRepSpecs + ")" errRepSpecs = "setting " + nRepSpecs @@ -354,14 +353,14 @@ func getRegionConfig(configSrc *hclwrite.Block, root attrVals) (*hclwrite.File, if err := hcl.MoveAttr(configSrc.Body(), fileb, nRegionName, nRegionName, errRepSpecs); err != nil { return nil, err } - if err := setPriority(fileb, configSrc.Body().GetAttribute(nPriority)); err != nil { + if err := hcl.MoveAttr(configSrc.Body(), fileb, nPriority, nPriority, errRepSpecs); err != nil { return nil, err } - electableSpecs, errElec := getSpecs(configSrc, nElectableNodes, root) + electable, errElec := getSpecs(configSrc, nElectableNodes, root) if errElec != nil { return nil, errElec } - fileb.SetAttributeRaw(nElectableSpecs, electableSpecs) + fileb.SetAttributeRaw(nElectableSpecs, electable) if readOnly, _ := getSpecs(configSrc, nReadOnlyNodes, root); readOnly != nil { fileb.SetAttributeRaw(nReadOnlySpecs, readOnly) } @@ -514,21 +513,6 @@ func setKeyValue(body *hclwrite.Body, key, value *hclwrite.Attribute) { body.SetAttributeRaw(keyStr, value.Expr().BuildTokens(nil)) } -func setPriority(body *hclwrite.Body, priority *hclwrite.Attribute) error { - if priority == nil { - return fmt.Errorf("%s: %s not found", errRepSpecs, nPriority) - } - valPriority, err := hcl.GetAttrInt(priority, errPriority) - if err != nil { - return err - } - if valPriority < valMinPriority || valPriority > valMaxPriority { - return fmt.Errorf("%s: %s is %d but must be between %d and %d", errPriority, nPriority, valPriority, valMinPriority, valMaxPriority) - } - hcl.SetAttrInt(body, nPriority, valPriority) - return nil -} - // popRootAttrs deletes the attributes common to all replication_specs/regions_config and returns them. func popRootAttrs(body *hclwrite.Body) (attrVals, error) { var ( diff --git a/internal/convert/testdata/clu2adv/errors.json b/internal/convert/testdata/clu2adv/errors.json index 4147bcc..2b9a8ce 100644 --- a/internal/convert/testdata/clu2adv/errors.json +++ b/internal/convert/testdata/clu2adv/errors.json @@ -4,9 +4,7 @@ "autoscaling_missing_attribute": "setting replication_specs: attribute provider_instance_size_name not found", "replication_specs_missing_regions_config": "setting replication_specs: regions_config not found", "regions_config_missing_electable_nodes": "setting replication_specs: attribute electable_nodes not found", - "regions_config_missing_priority": "setting replication_specs: priority not found", - "regions_config_out_of_range_priority": "setting priority: priority is 0 but must be between 1 and 7", - "regions_config_non_literal_priority": "setting priority: failed to evaluate number", + "regions_config_missing_priority": "setting replication_specs: attribute priority not found", "replication_specs_unsupported_dynamic": "dynamic blocks are not supported", "replication_specs_non_literal_num_shards": "setting num_shards: failed to evaluate number", "replication_specs_missing_num_shards": "num_shards not found" diff --git a/internal/convert/testdata/clu2adv/regions_config_non_literal_priority.in.tf b/internal/convert/testdata/clu2adv/regions_config_non_literal_priority.in.tf deleted file mode 100644 index 590febe..0000000 --- a/internal/convert/testdata/clu2adv/regions_config_non_literal_priority.in.tf +++ /dev/null @@ -1,16 +0,0 @@ -resource "mongodbatlas_cluster" "clu" { - project_id = "1234" - name = "clu" - cluster_type = "REPLICASET" - provider_name = "AWS" - provider_instance_size_name = "M10" - - replication_specs { - num_shards = 1 - regions_config { - region_name = "US_WEST_2" - priority = var.priority # unresolved - electable_nodes = 2 - } - } -} diff --git a/internal/convert/testdata/clu2adv/regions_config_out_of_range_priority.in.tf b/internal/convert/testdata/clu2adv/regions_config_out_of_range_priority.in.tf deleted file mode 100644 index e1fe04b..0000000 --- a/internal/convert/testdata/clu2adv/regions_config_out_of_range_priority.in.tf +++ /dev/null @@ -1,16 +0,0 @@ -resource "mongodbatlas_cluster" "clu" { - project_id = "1234" - name = "clu" - cluster_type = "REPLICASET" - provider_name = "AWS" - provider_instance_size_name = "M10" - - replication_specs { - num_shards = 1 - regions_config { - region_name = "US_WEST_2" - priority = 0 # range 1-7 - electable_nodes = 2 - } - } -} From c5e6d5f7f88f50ab8e96732158e21399680a8956 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:42:27 +0100 Subject: [PATCH 12/36] reuse getRegionConfig from dynamic block logic --- internal/convert/convert.go | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index 0a0e935..9d5333c 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -264,8 +264,8 @@ func extractTagsLabelsDynamicBlock(resourceb *hclwrite.Body, name string) (hclwr 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) + keyExpr := replaceDynamicBlockExpr(key, nKey, name, "") + valueExpr := replaceDynamicBlockExpr(value, nValue, name, "") collectionExpr := hcl.GetAttrExpr(d.forEach) forExpr := fmt.Sprintf("for key, value in %s : %s => %s", collectionExpr, keyExpr, valueExpr) tokens := hcl.TokensObjectFromExpr(forExpr) @@ -316,7 +316,23 @@ func fillRegionConfigsDynamicBlock(specbSrc *hclwrite.Body, root attrVals) (dyna if err != nil || !d.IsPresent() { return dynamicBlock{}, err } - d.tokens = hcl.EncloseBrackets(hcl.EncloseNewLines(hcl.TokensFromExpr("for statement"))) + const nRegion = "region" + configSrc := d.content + configSrcb := configSrc.Body() + + oldPrefix := fmt.Sprintf("%s.%s", nConfigSrc, nValue) + // Change value references in all attributes, e.g. regions_config.value.electable_nodes to region.electable_nodes + for name, attr := range configSrcb.Attributes() { + expr := replaceDynamicBlockExpr(attr, name, oldPrefix, nRegion) + configSrcb.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) + } + + region, err := getRegionConfig(configSrc, root) + if err != nil { + return dynamicBlock{}, err + } + + d.tokens = hcl.EncloseBrackets(hcl.EncloseNewLines(hcl.TokensObject(region.Body()))) return d, nil } @@ -495,9 +511,13 @@ func getDynamicBlock(body *hclwrite.Body, name string) (dynamicBlock, error) { return dynamicBlock{}, nil } -func replaceDynamicBlockExpr(attr *hclwrite.Attribute, blockName, attrName string) string { +func replaceDynamicBlockExpr(attr *hclwrite.Attribute, attrName, oldPrefix, newPrefix string) string { expr := hcl.GetAttrExpr(attr) - return strings.ReplaceAll(expr, fmt.Sprintf("%s.%s", blockName, attrName), attrName) + newStr := attrName + if newPrefix != "" { + newStr = fmt.Sprintf("%s.%s", newPrefix, attrName) + } + return strings.ReplaceAll(expr, fmt.Sprintf("%s.%s", oldPrefix, attrName), newStr) } func setKeyValue(body *hclwrite.Body, key, value *hclwrite.Attribute) { From ae8471bd368eac44b71ed5e4701de10f780af529 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Tue, 18 Mar 2025 16:35:30 +0100 Subject: [PATCH 13/36] only sort by priority if all priorities are numerical literals --- internal/convert/convert.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index 9d5333c..501519b 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -353,11 +353,7 @@ func fillRegionConfigs(specb, specbSrc *hclwrite.Body, root attrVals) error { if len(configs) == 0 { return fmt.Errorf("%s: %s not found", errRepSpecs, nConfigSrc) } - sort.Slice(configs, func(i, j int) bool { - pi, _ := hcl.GetAttrInt(configs[i].GetAttribute(nPriority), errPriority) - pj, _ := hcl.GetAttrInt(configs[j].GetAttribute(nPriority), errPriority) - return pi > pj - }) + configs = sortConfigsByPriority(configs) specb.SetAttributeRaw(nConfig, hcl.TokensArray(configs)) return nil } @@ -520,6 +516,20 @@ func replaceDynamicBlockExpr(attr *hclwrite.Attribute, attrName, oldPrefix, newP return strings.ReplaceAll(expr, fmt.Sprintf("%s.%s", oldPrefix, attrName), newStr) } +func sortConfigsByPriority(configs []*hclwrite.Body) []*hclwrite.Body { + for _, config := range configs { + if _, err := hcl.GetAttrInt(config.GetAttribute(nPriority), errPriority); err != nil { + return configs // don't sort priorities if any is not a numerical literal + } + } + sort.Slice(configs, func(i, j int) bool { + pi, _ := hcl.GetAttrInt(configs[i].GetAttribute(nPriority), errPriority) + pj, _ := hcl.GetAttrInt(configs[j].GetAttribute(nPriority), errPriority) + return pi > pj + }) + return configs +} + func setKeyValue(body *hclwrite.Body, key, value *hclwrite.Attribute) { keyStr, err := hcl.GetAttrString(key, "") if err == nil { From 3f93627d9d188977dbca1a8d81f2408134d1d8b5 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Tue, 18 Mar 2025 16:53:59 +0100 Subject: [PATCH 14/36] remove limitations for priority and electable_nodes --- README.md | 2 -- internal/convert/convert.go | 6 ++-- .../analytics_read_only_all_params.in.tf | 21 ++++++++++++ .../analytics_read_only_all_params.out.tf | 34 +++++++++++++++++++ .../analytics_read_only_min_params.in.tf | 17 ++++++++++ .../analytics_read_only_min_params.out.tf | 28 +++++++++++++++ internal/convert/testdata/clu2adv/errors.json | 1 - ...gions_config_missing_electable_nodes.in.tf | 17 ---------- 8 files changed, 102 insertions(+), 24 deletions(-) delete mode 100644 internal/convert/testdata/clu2adv/regions_config_missing_electable_nodes.in.tf diff --git a/README.md b/README.md index 4879f01..2a1cdfc 100644 --- a/README.md +++ b/README.md @@ -80,8 +80,6 @@ Dynamic block and individual blocks for `regions_config` are not supported at th ### Limitations -- The plugin doesn't support `regions_config` without `electable_nodes` as there can be some issues with `priority` when they only have `analytics_nodes` and/or `electable_nodes`. -- [`priority`](https://registry.terraform.io/providers/mongodb/mongodbatlas/latest/docs/resources/cluster#priority-1) is required in `regions_config` and must be a numeric [literal expression](https://developer.hashicorp.com/nomad/docs/job-specification/hcl2/expressions#literal-expressions) between 7 and 1, e.g. `var.priority` is not supported. This is to allow reordering them by descending priority as this is expected in `mongodbatlas_advanced_cluster`. - [`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`. - `dynamic` blocks are currently supported only for `tags`, `labels` and `regions_config`. **Coming soon**: support for `replication_specs`. diff --git a/internal/convert/convert.go b/internal/convert/convert.go index 501519b..fc414aa 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -368,11 +368,9 @@ func getRegionConfig(configSrc *hclwrite.Block, root attrVals) (*hclwrite.File, if err := hcl.MoveAttr(configSrc.Body(), fileb, nPriority, nPriority, errRepSpecs); err != nil { return nil, err } - electable, errElec := getSpecs(configSrc, nElectableNodes, root) - if errElec != nil { - return nil, errElec + if electable, _ := getSpecs(configSrc, nElectableNodes, root); electable != nil { + fileb.SetAttributeRaw(nElectableSpecs, electable) } - fileb.SetAttributeRaw(nElectableSpecs, electable) if readOnly, _ := getSpecs(configSrc, nReadOnlyNodes, root); readOnly != nil { fileb.SetAttributeRaw(nReadOnlySpecs, readOnly) } diff --git a/internal/convert/testdata/clu2adv/analytics_read_only_all_params.in.tf b/internal/convert/testdata/clu2adv/analytics_read_only_all_params.in.tf index 101c90e..8d4a84b 100644 --- a/internal/convert/testdata/clu2adv/analytics_read_only_all_params.in.tf +++ b/internal/convert/testdata/clu2adv/analytics_read_only_all_params.in.tf @@ -18,3 +18,24 @@ resource "mongodbatlas_cluster" "ar" { } } } + +resource "mongodbatlas_cluster" "ar_not_electable" { + project_id = var.project_id + name = "ar" + cluster_type = "REPLICASET" + provider_name = "AWS" + provider_instance_size_name = "M10" + disk_size_gb = 90 + provider_volume_type = "PROVISIONED" + provider_disk_iops = 100 + replication_specs { + num_shards = 1 + regions_config { + region_name = "US_EAST_1" + priority = 7 + electable_nodes = 0 + analytics_nodes = 2 + read_only_nodes = 1 + } + } +} diff --git a/internal/convert/testdata/clu2adv/analytics_read_only_all_params.out.tf b/internal/convert/testdata/clu2adv/analytics_read_only_all_params.out.tf index 85407ea..85b8eef 100644 --- a/internal/convert/testdata/clu2adv/analytics_read_only_all_params.out.tf +++ b/internal/convert/testdata/clu2adv/analytics_read_only_all_params.out.tf @@ -38,3 +38,37 @@ resource "mongodbatlas_advanced_cluster" "ar" { # Generated by atlas-cli-plugin-terraform. # Please review the changes and confirm that references to this resource are updated. } + +resource "mongodbatlas_advanced_cluster" "ar_not_electable" { + project_id = var.project_id + name = "ar" + cluster_type = "REPLICASET" + replication_specs = [ + { + region_configs = [ + { + provider_name = "AWS" + region_name = "US_EAST_1" + priority = 7 + read_only_specs = { + node_count = 1 + instance_size = "M10" + disk_size_gb = 90 + ebs_volume_type = "PROVISIONED" + disk_iops = 100 + } + analytics_specs = { + node_count = 2 + instance_size = "M10" + disk_size_gb = 90 + ebs_volume_type = "PROVISIONED" + disk_iops = 100 + } + } + ] + } + ] + + # 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/analytics_read_only_min_params.in.tf b/internal/convert/testdata/clu2adv/analytics_read_only_min_params.in.tf index 8aae1e1..d3b4cab 100644 --- a/internal/convert/testdata/clu2adv/analytics_read_only_min_params.in.tf +++ b/internal/convert/testdata/clu2adv/analytics_read_only_min_params.in.tf @@ -15,3 +15,20 @@ resource "mongodbatlas_cluster" "ar" { } } } + +resource "mongodbatlas_cluster" "ar_not_electable" { + project_id = var.project_id + name = "ar" + cluster_type = "REPLICASET" + provider_name = "AWS" + provider_instance_size_name = "M10" + replication_specs { + num_shards = 1 + regions_config { + region_name = "US_EAST_1" + priority = 7 + analytics_nodes = 2 + read_only_nodes = 1 + } + } +} diff --git a/internal/convert/testdata/clu2adv/analytics_read_only_min_params.out.tf b/internal/convert/testdata/clu2adv/analytics_read_only_min_params.out.tf index c4681a9..2699ea9 100644 --- a/internal/convert/testdata/clu2adv/analytics_read_only_min_params.out.tf +++ b/internal/convert/testdata/clu2adv/analytics_read_only_min_params.out.tf @@ -29,3 +29,31 @@ resource "mongodbatlas_advanced_cluster" "ar" { # Generated by atlas-cli-plugin-terraform. # Please review the changes and confirm that references to this resource are updated. } + +resource "mongodbatlas_advanced_cluster" "ar_not_electable" { + project_id = var.project_id + name = "ar" + cluster_type = "REPLICASET" + replication_specs = [ + { + region_configs = [ + { + provider_name = "AWS" + region_name = "US_EAST_1" + priority = 7 + read_only_specs = { + node_count = 1 + instance_size = "M10" + } + analytics_specs = { + node_count = 2 + instance_size = "M10" + } + } + ] + } + ] + + # 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/errors.json b/internal/convert/testdata/clu2adv/errors.json index 2b9a8ce..fc81442 100644 --- a/internal/convert/testdata/clu2adv/errors.json +++ b/internal/convert/testdata/clu2adv/errors.json @@ -3,7 +3,6 @@ "free_cluster_missing_attribute": "free cluster (because no replication_specs): attribute backing_provider_name not found", "autoscaling_missing_attribute": "setting replication_specs: attribute provider_instance_size_name not found", "replication_specs_missing_regions_config": "setting replication_specs: regions_config not found", - "regions_config_missing_electable_nodes": "setting replication_specs: attribute electable_nodes not found", "regions_config_missing_priority": "setting replication_specs: attribute priority not found", "replication_specs_unsupported_dynamic": "dynamic blocks are not supported", "replication_specs_non_literal_num_shards": "setting num_shards: failed to evaluate number", diff --git a/internal/convert/testdata/clu2adv/regions_config_missing_electable_nodes.in.tf b/internal/convert/testdata/clu2adv/regions_config_missing_electable_nodes.in.tf deleted file mode 100644 index f35c870..0000000 --- a/internal/convert/testdata/clu2adv/regions_config_missing_electable_nodes.in.tf +++ /dev/null @@ -1,17 +0,0 @@ -resource "mongodbatlas_cluster" "clu" { - project_id = "1234" - name = "clu" - cluster_type = "REPLICASET" - provider_name = "AWS" - provider_instance_size_name = "M10" - - replication_specs { - num_shards = 1 - regions_config { - # missing electable_nodes - region_name = "US_WEST_2" - priority = 7 - read_only_nodes = 2 - } - } -} From ff3fc227fe767431d56f12d76c35278926863fed Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Tue, 18 Mar 2025 20:54:22 +0100 Subject: [PATCH 15/36] use config in dynamic blocks from individual --- internal/convert/convert.go | 34 ++++++++++++++++--- .../clu2adv/dynamic_regions_config.in.tf | 1 + .../clu2adv/dynamic_regions_config.out.tf | 1 + internal/hcl/hcl.go | 32 ++++++++++++----- 4 files changed, 54 insertions(+), 14 deletions(-) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index fc414aa..4269cf9 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -201,8 +201,6 @@ func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error { break } specbSrc := specSrc.Body() - // ok to fail as zone_name is optional - _ = hcl.MoveAttr(specbSrc, specb, nZoneName, nZoneName, errRepSpecs) d, err := fillRegionConfigsDynamicBlock(specbSrc, root) if err != nil { return err @@ -212,6 +210,8 @@ func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error { 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) @@ -316,6 +316,11 @@ func fillRegionConfigsDynamicBlock(specbSrc *hclwrite.Body, root attrVals) (dyna if err != nil || !d.IsPresent() { return dynamicBlock{}, err } + shards := specbSrc.GetAttribute(nNumShards) + if shards == nil { + return dynamicBlock{}, fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) + } + zoneName := hcl.GetAttrExpr(specbSrc.GetAttribute(nZoneName)) const nRegion = "region" configSrc := d.content configSrcb := configSrc.Body() @@ -326,13 +331,32 @@ func fillRegionConfigsDynamicBlock(specbSrc *hclwrite.Body, root attrVals) (dyna expr := replaceDynamicBlockExpr(attr, name, oldPrefix, nRegion) configSrcb.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) } - region, err := getRegionConfig(configSrc, root) if err != nil { return dynamicBlock{}, err } - - d.tokens = hcl.EncloseBrackets(hcl.EncloseNewLines(hcl.TokensObject(region.Body()))) + forOuter := fmt.Sprintf("for i in range(%s) :", hcl.GetAttrExpr(shards)) + repSpec := hclwrite.NewEmptyFile() + repSpecb := repSpec.Body() + if zoneName != "" { + repSpecb.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(zoneName)) + } + config := region.BuildTokens(nil) + config = append(config, hcl.TokenNewLine()) + config = hcl.EncloseBraces(config, true) + config = append(config, hcl.TokensFromExpr("if region.priority == priority")...) + forRegion := hcl.TokensFromExpr(fmt.Sprintf("for region in %s :", hcl.GetAttrExpr(d.forEach))) + forRegion = append(forRegion, config...) + + priorityContent := hcl.EncloseBrackets(hcl.EncloseNewLines(forRegion)) + priorityBlock := hcl.TokensFromExpr("for priority in range(7, 0, -1) : ") + priorityBlock = append(priorityBlock, priorityContent...) + repSpecb.SetAttributeRaw(nConfig, hcl.TokensFuncFlatten(priorityBlock)) + + tokens := hcl.TokensFromExpr(forOuter) + tokens = append(tokens, hcl.EncloseBraces(repSpec.BuildTokens(nil), true)...) + tokens = hcl.EncloseBrackets(hcl.EncloseNewLines(tokens)) + d.tokens = tokens return d, nil } diff --git a/internal/convert/testdata/clu2adv/dynamic_regions_config.in.tf b/internal/convert/testdata/clu2adv/dynamic_regions_config.in.tf index ae7567b..3a27757 100644 --- a/internal/convert/testdata/clu2adv/dynamic_regions_config.in.tf +++ b/internal/convert/testdata/clu2adv/dynamic_regions_config.in.tf @@ -6,6 +6,7 @@ resource "mongodbatlas_cluster" "dynamic_regions_config" { provider_instance_size_name = "M10" replication_specs { num_shards = var.replication_specs.num_shards + zone_name = "Zone 1" dynamic "regions_config" { for_each = var.replication_specs.regions_config content { diff --git a/internal/convert/testdata/clu2adv/dynamic_regions_config.out.tf b/internal/convert/testdata/clu2adv/dynamic_regions_config.out.tf index ab48c3c..e2d0c52 100644 --- a/internal/convert/testdata/clu2adv/dynamic_regions_config.out.tf +++ b/internal/convert/testdata/clu2adv/dynamic_regions_config.out.tf @@ -4,6 +4,7 @@ resource "mongodbatlas_advanced_cluster" "dynamic_regions_config" { cluster_type = "SHARDED" replication_specs = [ for i in range(var.replication_specs.num_shards) : { + zone_name = "Zone 1" region_configs = flatten([ for priority in range(7, 0, -1) : [ for region in var.replication_specs.regions_config : { diff --git a/internal/hcl/hcl.go b/internal/hcl/hcl.go index c8e7da9..d7ceb96 100644 --- a/internal/hcl/hcl.go +++ b/internal/hcl/hcl.go @@ -33,6 +33,9 @@ func PopAttr(body *hclwrite.Body, attrName, errPrefix string) (hclwrite.Tokens, // GetAttrExpr returns the expression of an attribute as a string. func GetAttrExpr(attr *hclwrite.Attribute) string { + if attr == nil { + return "" + } return strings.TrimSpace(string(attr.Expr().BuildTokens(nil).Bytes())) } @@ -77,6 +80,10 @@ func GetAttrString(attr *hclwrite.Attribute, errPrefix string) (string, error) { return val.AsString(), nil } +func TokenNewLine() *hclwrite.Token { + return &hclwrite.Token{Type: hclsyntax.TokenNewline, Bytes: []byte("\n")} +} + // TokensArray creates an array of objects. func TokensArray(bodies []*hclwrite.Body) hclwrite.Tokens { tokens := make([]hclwrite.Tokens, 0) @@ -93,9 +100,9 @@ func TokensArraySingle(body *hclwrite.Body) hclwrite.Tokens { // TokensObject creates an object. func TokensObject(body *hclwrite.Body) hclwrite.Tokens { - tokens := hclwrite.Tokens{tokenNewLine} + tokens := hclwrite.Tokens{TokenNewLine()} tokens = append(tokens, RemoveLeadingNewline(body.BuildTokens(nil))...) - return EncloseBraces(tokens) + return EncloseBraces(tokens, false) } // TokensFromExpr creates the tokens for an expression provided as a string. @@ -105,7 +112,7 @@ func TokensFromExpr(expr string) hclwrite.Tokens { // TokensObjectFromExpr creates an object with an expression. func TokensObjectFromExpr(expr string) hclwrite.Tokens { - return EncloseBraces(EncloseNewLines(TokensFromExpr(expr))) + return EncloseBraces(EncloseNewLines(TokensFromExpr(expr)), false) } // TokensFuncMerge creates the tokens for the HCL merge function. @@ -115,6 +122,12 @@ func TokensFuncMerge(tokens ...hclwrite.Tokens) hclwrite.Tokens { return append(ret, EncloseParens(params)...) } +// TokensFuncFlatten creates the tokens for the HCL flatten function. +func TokensFuncFlatten(tokens hclwrite.Tokens) hclwrite.Tokens { + ret := TokensFromExpr("flatten") + return append(ret, EncloseParens(EncloseBrackets(EncloseNewLines(tokens)))...) +} + // RemoveLeadingNewline removes the first newline if it exists to make the output prettier. func RemoveLeadingNewline(tokens hclwrite.Tokens) hclwrite.Tokens { if len(tokens) > 0 && tokens[0].Type == hclsyntax.TokenNewline { @@ -140,8 +153,6 @@ func GetParser(config []byte) (*hclwrite.File, error) { return parser, nil } -var tokenNewLine = &hclwrite.Token{Type: hclsyntax.TokenNewline, Bytes: []byte("\n")} - // joinTokens joins multiple tokens with commas and newlines. func joinTokens(tokens ...hclwrite.Tokens) hclwrite.Tokens { ret := hclwrite.Tokens{} @@ -150,7 +161,7 @@ func joinTokens(tokens ...hclwrite.Tokens) hclwrite.Tokens { if i < len(tokens)-1 { ret = append(ret, &hclwrite.Token{Type: hclsyntax.TokenComma, Bytes: []byte(",")}, - tokenNewLine) + TokenNewLine()) } } return ret @@ -164,8 +175,11 @@ func EncloseParens(tokens hclwrite.Tokens) hclwrite.Tokens { } // EncloseBraces encloses tokens with curly braces, { }. -func EncloseBraces(tokens hclwrite.Tokens) hclwrite.Tokens { +func EncloseBraces(tokens hclwrite.Tokens, initialNewLine bool) hclwrite.Tokens { ret := hclwrite.Tokens{{Type: hclsyntax.TokenOBrace, Bytes: []byte("{")}} + if initialNewLine { + ret = append(ret, TokenNewLine()) + } ret = append(ret, tokens...) return append(ret, &hclwrite.Token{Type: hclsyntax.TokenCBrace, Bytes: []byte("}")}) } @@ -179,7 +193,7 @@ func EncloseBrackets(tokens hclwrite.Tokens) hclwrite.Tokens { // EncloseNewLines encloses tokens with newlines at the beginning and end. func EncloseNewLines(tokens hclwrite.Tokens) hclwrite.Tokens { - ret := hclwrite.Tokens{tokenNewLine} + ret := hclwrite.Tokens{TokenNewLine()} ret = append(ret, tokens...) - return append(ret, tokenNewLine) + return append(ret, TokenNewLine()) } From 87bf0fc2a00481a8187aee35411303c84899c318 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Wed, 19 Mar 2025 06:05:56 +0100 Subject: [PATCH 16/36] passing test --- internal/convert/convert.go | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index 4269cf9..b8fcc77 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -331,7 +331,7 @@ func fillRegionConfigsDynamicBlock(specbSrc *hclwrite.Body, root attrVals) (dyna expr := replaceDynamicBlockExpr(attr, name, oldPrefix, nRegion) configSrcb.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) } - region, err := getRegionConfig(configSrc, root) + region, err := getRegionConfig(configSrc, root, true) if err != nil { return dynamicBlock{}, err } @@ -342,7 +342,6 @@ func fillRegionConfigsDynamicBlock(specbSrc *hclwrite.Body, root attrVals) (dyna repSpecb.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(zoneName)) } config := region.BuildTokens(nil) - config = append(config, hcl.TokenNewLine()) config = hcl.EncloseBraces(config, true) config = append(config, hcl.TokensFromExpr("if region.priority == priority")...) forRegion := hcl.TokensFromExpr(fmt.Sprintf("for region in %s :", hcl.GetAttrExpr(d.forEach))) @@ -367,7 +366,7 @@ func fillRegionConfigs(specb, specbSrc *hclwrite.Body, root attrVals) error { if configSrc == nil { break } - config, err := getRegionConfig(configSrc, root) + config, err := getRegionConfig(configSrc, root, false) if err != nil { return err } @@ -382,7 +381,7 @@ func fillRegionConfigs(specb, specbSrc *hclwrite.Body, root attrVals) error { return nil } -func getRegionConfig(configSrc *hclwrite.Block, root attrVals) (*hclwrite.File, error) { +func getRegionConfig(configSrc *hclwrite.Block, root attrVals, isDynamicBlock bool) (*hclwrite.File, error) { file := hclwrite.NewEmptyFile() fileb := file.Body() fileb.SetAttributeRaw(nProviderName, root.req[nProviderName]) @@ -393,12 +392,30 @@ func getRegionConfig(configSrc *hclwrite.Block, root attrVals) (*hclwrite.File, return nil, err } if electable, _ := getSpecs(configSrc, nElectableNodes, root); electable != nil { + if isDynamicBlock { + tokens := hcl.TokensFromExpr(fmt.Sprintf("region.%s > 0 ?", nElectableNodes)) + tokens = append(tokens, electable...) + tokens = append(tokens, hcl.TokensFromExpr(": null")...) + electable = tokens + } fileb.SetAttributeRaw(nElectableSpecs, electable) } if readOnly, _ := getSpecs(configSrc, nReadOnlyNodes, root); readOnly != nil { + if isDynamicBlock { + tokens := hcl.TokensFromExpr(fmt.Sprintf("region.%s > 0 ?", nReadOnlyNodes)) + tokens = append(tokens, readOnly...) + tokens = append(tokens, hcl.TokensFromExpr(": null")...) + readOnly = tokens + } fileb.SetAttributeRaw(nReadOnlySpecs, readOnly) } if analytics, _ := getSpecs(configSrc, nAnalyticsNodes, root); analytics != nil { + if isDynamicBlock { + tokens := hcl.TokensFromExpr(fmt.Sprintf("region.%s > 0 ?", nAnalyticsNodes)) + tokens = append(tokens, analytics...) + tokens = append(tokens, hcl.TokensFromExpr(": null")...) + analytics = tokens + } fileb.SetAttributeRaw(nAnalyticsSpecs, analytics) } if autoScaling := getAutoScalingOpt(root.opt); autoScaling != nil { From b4db307b4dcede9ff632735b1303df2dfb29a3d0 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Wed, 19 Mar 2025 07:24:35 +0100 Subject: [PATCH 17/36] add auto_scaling example --- .../dynamic_regions_config_auto_scaling.in.tf | 66 +++++++++++++++++ ...dynamic_regions_config_auto_scaling.out.tf | 72 +++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 internal/convert/testdata/clu2adv/dynamic_regions_config_auto_scaling.in.tf create mode 100644 internal/convert/testdata/clu2adv/dynamic_regions_config_auto_scaling.out.tf diff --git a/internal/convert/testdata/clu2adv/dynamic_regions_config_auto_scaling.in.tf b/internal/convert/testdata/clu2adv/dynamic_regions_config_auto_scaling.in.tf new file mode 100644 index 0000000..9f0aaa9 --- /dev/null +++ b/internal/convert/testdata/clu2adv/dynamic_regions_config_auto_scaling.in.tf @@ -0,0 +1,66 @@ +resource "mongodbatlas_cluster" "cluster" { + project_id = var.project_id + name = var.cluster_name + cloud_backup = var.backup_enabled + pit_enabled = var.pit_enabled + retain_backups_enabled = var.retain_backups_enabled + auto_scaling_disk_gb_enabled = var.auto_scaling_disk_gb_enabled + mongo_db_major_version = var.mongodb_version + cluster_type = var.cluster_type + termination_protection_enabled = var.termination_protection_enabled + num_shards = var.replication_specs.num_shards + paused = var.paused + disk_size_gb = var.disk_size_gb + provider_volume_type = var.provider_volume_type + provider_disk_iops = var.provider_disk_iops + redact_client_log_data = true + provider_name = var.provider_name + provider_instance_size_name = var.provider_instance_size_name + encryption_at_rest_provider = var.encryption_at_rest_provider + + replication_specs { + num_shards = var.replication_specs.num_shards + dynamic "regions_config" { + for_each = var.replication_specs.regions_config + content { + region_name = regions_config.value.region_name + electable_nodes = regions_config.value.electable_nodes + priority = regions_config.value.priority + read_only_nodes = regions_config.value.read_only_nodes + } + } + } + + advanced_configuration { + oplog_size_mb = var.oplog_size_mb + transaction_lifetime_limit_seconds = var.transaction_lifetime_limit_seconds + minimum_enabled_tls_protocol = "TLS1_2" + javascript_enabled = false + tls_cipher_config_mode = "CUSTOM" + custom_openssl_cipher_config_tls12 = ["TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"] + } + + labels { + key = "label1" + value = var.label1val + } + dynamic "labels" { + for_each = local.labels + content { + key = labels.key + value = labels.value + } + } + + tags { + key = "tag1" + value = var.tag1val + } + dynamic "tags" { + for_each = local.tags + content { + key = tags.key + value = replace(tags.value, "/", "_") + } + } +} diff --git a/internal/convert/testdata/clu2adv/dynamic_regions_config_auto_scaling.out.tf b/internal/convert/testdata/clu2adv/dynamic_regions_config_auto_scaling.out.tf new file mode 100644 index 0000000..adef0e2 --- /dev/null +++ b/internal/convert/testdata/clu2adv/dynamic_regions_config_auto_scaling.out.tf @@ -0,0 +1,72 @@ +resource "mongodbatlas_advanced_cluster" "cluster" { + project_id = var.project_id + name = var.cluster_name + pit_enabled = var.pit_enabled + retain_backups_enabled = var.retain_backups_enabled + mongo_db_major_version = var.mongodb_version + cluster_type = var.cluster_type + termination_protection_enabled = var.termination_protection_enabled + paused = var.paused + redact_client_log_data = true + encryption_at_rest_provider = var.encryption_at_rest_provider + + + + + backup_enabled = var.backup_enabled + replication_specs = [ + for i in range(var.replication_specs.num_shards) : { + region_configs = flatten([ + for priority in range(7, 0, -1) : [ + for region in var.replication_specs.regions_config : { + provider_name = var.provider_name + region_name = region.region_name + priority = region.priority + electable_specs = region.electable_nodes > 0 ? { + node_count = region.electable_nodes + instance_size = var.provider_instance_size_name + disk_size_gb = var.disk_size_gb + ebs_volume_type = var.provider_volume_type + disk_iops = var.provider_disk_iops + } : null + read_only_specs = region.read_only_nodes > 0 ? { + node_count = region.read_only_nodes + instance_size = var.provider_instance_size_name + disk_size_gb = var.disk_size_gb + ebs_volume_type = var.provider_volume_type + disk_iops = var.provider_disk_iops + } : null + auto_scaling = { + disk_gb_enabled = var.auto_scaling_disk_gb_enabled + } + } if region.priority == priority + ] + ]) + } + ] + tags = merge( + { + for key, value in local.tags : key => replace(value, "/", "_") + }, + { + tag1 = var.tag1val + } + ) + labels = merge( + local.labels, + { + label1 = var.label1val + } + ) + advanced_configuration = { + oplog_size_mb = var.oplog_size_mb + transaction_lifetime_limit_seconds = var.transaction_lifetime_limit_seconds + minimum_enabled_tls_protocol = "TLS1_2" + javascript_enabled = false + tls_cipher_config_mode = "CUSTOM" + custom_openssl_cipher_config_tls12 = ["TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"] + } + + # Generated by atlas-cli-plugin-terraform. + # Please review the changes and confirm that references to this resource are updated. +} From 969da96939862756532f802936f529026140d919 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Wed, 19 Mar 2025 07:42:44 +0100 Subject: [PATCH 18/36] fix region_configs name replacement --- internal/convert/const_names.go | 1 + internal/convert/convert.go | 28 +++++++++++-------- ...dynamic_regions_config_auto_scaling.out.tf | 2 +- ....tf => dynamic_regions_config_basic.in.tf} | 10 +++---- ...tf => dynamic_regions_config_basic.out.tf} | 12 ++++---- 5 files changed, 29 insertions(+), 24 deletions(-) rename internal/convert/testdata/clu2adv/{dynamic_regions_config.in.tf => dynamic_regions_config_basic.in.tf} (87%) rename internal/convert/testdata/clu2adv/{dynamic_regions_config.out.tf => dynamic_regions_config_basic.out.tf} (88%) diff --git a/internal/convert/const_names.go b/internal/convert/const_names.go index 7352d18..c2adc20 100644 --- a/internal/convert/const_names.go +++ b/internal/convert/const_names.go @@ -54,4 +54,5 @@ const ( nDynamic = "dynamic" nForEach = "for_each" nContent = "content" + nRegion = "region" ) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index b8fcc77..eb10ce8 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -264,8 +264,8 @@ func extractTagsLabelsDynamicBlock(resourceb *hclwrite.Body, name string) (hclwr if key == nil || value == nil { return nil, fmt.Errorf("dynamic block %s: %s or %s not found", name, nKey, nValue) } - keyExpr := replaceDynamicBlockExpr(key, nKey, name, "") - valueExpr := replaceDynamicBlockExpr(value, nValue, name, "") + 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.TokensObjectFromExpr(forExpr) @@ -321,16 +321,24 @@ func fillRegionConfigsDynamicBlock(specbSrc *hclwrite.Body, root attrVals) (dyna return dynamicBlock{}, fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) } zoneName := hcl.GetAttrExpr(specbSrc.GetAttribute(nZoneName)) - const nRegion = "region" configSrc := d.content configSrcb := configSrc.Body() + var priorityStr string - oldPrefix := fmt.Sprintf("%s.%s", nConfigSrc, nValue) // Change value references in all attributes, e.g. regions_config.value.electable_nodes to region.electable_nodes for name, attr := range configSrcb.Attributes() { - expr := replaceDynamicBlockExpr(attr, name, oldPrefix, nRegion) + expr := hcl.GetAttrExpr(attr) + expr = strings.ReplaceAll(expr, + fmt.Sprintf("%s.%s.", nConfigSrc, nValue), + fmt.Sprintf("%s.", nRegion)) + if name == nPriority { + priorityStr = expr + } configSrcb.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) } + if priorityStr == "" { + return dynamicBlock{}, fmt.Errorf("%s: %s not found", errRepSpecs, nPriority) + } region, err := getRegionConfig(configSrc, root, true) if err != nil { return dynamicBlock{}, err @@ -343,7 +351,7 @@ func fillRegionConfigsDynamicBlock(specbSrc *hclwrite.Body, root attrVals) (dyna } config := region.BuildTokens(nil) config = hcl.EncloseBraces(config, true) - config = append(config, hcl.TokensFromExpr("if region.priority == priority")...) + config = append(config, hcl.TokensFromExpr(fmt.Sprintf("if priority == %s", priorityStr))...) forRegion := hcl.TokensFromExpr(fmt.Sprintf("for region in %s :", hcl.GetAttrExpr(d.forEach))) forRegion = append(forRegion, config...) @@ -546,13 +554,9 @@ func getDynamicBlock(body *hclwrite.Body, name string) (dynamicBlock, error) { return dynamicBlock{}, nil } -func replaceDynamicBlockExpr(attr *hclwrite.Attribute, attrName, oldPrefix, newPrefix string) string { +func replaceDynamicBlockExpr(attr *hclwrite.Attribute, blockName, attrName string) string { expr := hcl.GetAttrExpr(attr) - newStr := attrName - if newPrefix != "" { - newStr = fmt.Sprintf("%s.%s", newPrefix, attrName) - } - return strings.ReplaceAll(expr, fmt.Sprintf("%s.%s", oldPrefix, attrName), newStr) + return strings.ReplaceAll(expr, fmt.Sprintf("%s.%s", blockName, attrName), attrName) } func sortConfigsByPriority(configs []*hclwrite.Body) []*hclwrite.Body { diff --git a/internal/convert/testdata/clu2adv/dynamic_regions_config_auto_scaling.out.tf b/internal/convert/testdata/clu2adv/dynamic_regions_config_auto_scaling.out.tf index adef0e2..94ea6d1 100644 --- a/internal/convert/testdata/clu2adv/dynamic_regions_config_auto_scaling.out.tf +++ b/internal/convert/testdata/clu2adv/dynamic_regions_config_auto_scaling.out.tf @@ -39,7 +39,7 @@ resource "mongodbatlas_advanced_cluster" "cluster" { auto_scaling = { disk_gb_enabled = var.auto_scaling_disk_gb_enabled } - } if region.priority == priority + } if priority == region.priority ] ]) } diff --git a/internal/convert/testdata/clu2adv/dynamic_regions_config.in.tf b/internal/convert/testdata/clu2adv/dynamic_regions_config_basic.in.tf similarity index 87% rename from internal/convert/testdata/clu2adv/dynamic_regions_config.in.tf rename to internal/convert/testdata/clu2adv/dynamic_regions_config_basic.in.tf index 3a27757..5c5265b 100644 --- a/internal/convert/testdata/clu2adv/dynamic_regions_config.in.tf +++ b/internal/convert/testdata/clu2adv/dynamic_regions_config_basic.in.tf @@ -6,13 +6,13 @@ resource "mongodbatlas_cluster" "dynamic_regions_config" { provider_instance_size_name = "M10" replication_specs { num_shards = var.replication_specs.num_shards - zone_name = "Zone 1" + zone_name = var.zone_name dynamic "regions_config" { for_each = var.replication_specs.regions_config content { region_name = regions_config.value.region_name electable_nodes = regions_config.value.electable_nodes - priority = regions_config.value.priority + priority = regions_config.value.prio read_only_nodes = regions_config.value.read_only_nodes } } @@ -26,7 +26,7 @@ variable "replication_specs" { regions_config = set(object({ region_name = string electable_nodes = number - priority = number + prio = number read_only_nodes = number })) }) @@ -36,13 +36,13 @@ variable "replication_specs" { { region_name = "US_EAST_1" electable_nodes = 3 - priority = 7 + prio = 7 read_only_nodes = 0 }, { region_name = "US_WEST_2" electable_nodes = 2 - priority = 6 + prio = 6 read_only_nodes = 1 } ] diff --git a/internal/convert/testdata/clu2adv/dynamic_regions_config.out.tf b/internal/convert/testdata/clu2adv/dynamic_regions_config_basic.out.tf similarity index 88% rename from internal/convert/testdata/clu2adv/dynamic_regions_config.out.tf rename to internal/convert/testdata/clu2adv/dynamic_regions_config_basic.out.tf index e2d0c52..be50ca3 100644 --- a/internal/convert/testdata/clu2adv/dynamic_regions_config.out.tf +++ b/internal/convert/testdata/clu2adv/dynamic_regions_config_basic.out.tf @@ -4,13 +4,13 @@ resource "mongodbatlas_advanced_cluster" "dynamic_regions_config" { cluster_type = "SHARDED" replication_specs = [ for i in range(var.replication_specs.num_shards) : { - zone_name = "Zone 1" + zone_name = var.zone_name region_configs = flatten([ for priority in range(7, 0, -1) : [ for region in var.replication_specs.regions_config : { provider_name = "AWS" region_name = region.region_name - priority = region.priority + priority = region.prio electable_specs = region.electable_nodes > 0 ? { node_count = region.electable_nodes instance_size = "M10" @@ -19,7 +19,7 @@ resource "mongodbatlas_advanced_cluster" "dynamic_regions_config" { node_count = region.read_only_nodes instance_size = "M10" } : null - } if region.priority == priority + } if priority == region.prio ] ]) } @@ -36,7 +36,7 @@ variable "replication_specs" { regions_config = set(object({ region_name = string electable_nodes = number - priority = number + prio = number read_only_nodes = number })) }) @@ -46,13 +46,13 @@ variable "replication_specs" { { region_name = "US_EAST_1" electable_nodes = 3 - priority = 7 + prio = 7 read_only_nodes = 0 }, { region_name = "US_WEST_2" electable_nodes = 2 - priority = 6 + prio = 6 read_only_nodes = 1 } ] From 55b93fda9f20315cabe7096548ac7f1efb961cae Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Wed, 19 Mar 2025 07:57:30 +0100 Subject: [PATCH 19/36] refactor isDynamicBlock --- internal/convert/convert.go | 60 ++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 34 deletions(-) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index eb10ce8..5d7f7aa 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -399,31 +399,13 @@ func getRegionConfig(configSrc *hclwrite.Block, root attrVals, isDynamicBlock bo if err := hcl.MoveAttr(configSrc.Body(), fileb, nPriority, nPriority, errRepSpecs); err != nil { return nil, err } - if electable, _ := getSpecs(configSrc, nElectableNodes, root); electable != nil { - if isDynamicBlock { - tokens := hcl.TokensFromExpr(fmt.Sprintf("region.%s > 0 ?", nElectableNodes)) - tokens = append(tokens, electable...) - tokens = append(tokens, hcl.TokensFromExpr(": null")...) - electable = tokens - } + if electable, _ := getSpecs(configSrc, nElectableNodes, root, isDynamicBlock); electable != nil { fileb.SetAttributeRaw(nElectableSpecs, electable) } - if readOnly, _ := getSpecs(configSrc, nReadOnlyNodes, root); readOnly != nil { - if isDynamicBlock { - tokens := hcl.TokensFromExpr(fmt.Sprintf("region.%s > 0 ?", nReadOnlyNodes)) - tokens = append(tokens, readOnly...) - tokens = append(tokens, hcl.TokensFromExpr(": null")...) - readOnly = tokens - } + if readOnly, _ := getSpecs(configSrc, nReadOnlyNodes, root, isDynamicBlock); readOnly != nil { fileb.SetAttributeRaw(nReadOnlySpecs, readOnly) } - if analytics, _ := getSpecs(configSrc, nAnalyticsNodes, root); analytics != nil { - if isDynamicBlock { - tokens := hcl.TokensFromExpr(fmt.Sprintf("region.%s > 0 ?", nAnalyticsNodes)) - tokens = append(tokens, analytics...) - tokens = append(tokens, hcl.TokensFromExpr(": null")...) - analytics = tokens - } + if analytics, _ := getSpecs(configSrc, nAnalyticsNodes, root, isDynamicBlock); analytics != nil { fileb.SetAttributeRaw(nAnalyticsSpecs, analytics) } if autoScaling := getAutoScalingOpt(root.opt); autoScaling != nil { @@ -432,7 +414,7 @@ func getRegionConfig(configSrc *hclwrite.Block, root attrVals, isDynamicBlock bo return file, nil } -func getSpecs(configSrc *hclwrite.Block, countName string, root attrVals) (hclwrite.Tokens, error) { +func getSpecs(configSrc *hclwrite.Block, countName string, root attrVals, isDynamicBlock bool) (hclwrite.Tokens, error) { var ( file = hclwrite.NewEmptyFile() fileb = file.Body() @@ -455,7 +437,11 @@ func getSpecs(configSrc *hclwrite.Block, countName string, root attrVals) (hclwr if root.opt[nDiskIOPSSrc] != nil { fileb.SetAttributeRaw(nDiskIOPS, root.opt[nDiskIOPSSrc]) } - return hcl.TokensObject(fileb), nil + tokens := hcl.TokensObject(fileb) + if isDynamicBlock { + tokens = encloseDynamicBlockRegionSpec(tokens, countName) + } + return tokens, nil } func getAutoScalingOpt(opt map[string]hclwrite.Tokens) hclwrite.Tokens { @@ -513,17 +499,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 -} - type dynamicBlock struct { block *hclwrite.Block forEach *hclwrite.Attribute @@ -535,6 +510,17 @@ func (d dynamicBlock) IsPresent() bool { return d.block != nil } +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 getDynamicBlock(body *hclwrite.Body, name string) (dynamicBlock, error) { for _, block := range body.Blocks() { if block.Type() != nDynamic || name != getResourceName(block) { @@ -559,6 +545,12 @@ func replaceDynamicBlockExpr(attr *hclwrite.Attribute, blockName, attrName strin return strings.ReplaceAll(expr, fmt.Sprintf("%s.%s", blockName, attrName), attrName) } +func encloseDynamicBlockRegionSpec(specTokens hclwrite.Tokens, countName string) hclwrite.Tokens { + tokens := hcl.TokensFromExpr(fmt.Sprintf("%s.%s > 0 ?", nRegion, countName)) + tokens = append(tokens, specTokens...) + return append(tokens, hcl.TokensFromExpr(": null")...) +} + func sortConfigsByPriority(configs []*hclwrite.Body) []*hclwrite.Body { for _, config := range configs { if _, err := hcl.GetAttrInt(config.GetAttribute(nPriority), errPriority); err != nil { From 6186dcd1d11a69427d3a1b998ea8e494523f8c26 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Wed, 19 Mar 2025 08:03:48 +0100 Subject: [PATCH 20/36] go back to unexported tokenNewLine --- internal/hcl/hcl.go | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/internal/hcl/hcl.go b/internal/hcl/hcl.go index d7ceb96..43c1089 100644 --- a/internal/hcl/hcl.go +++ b/internal/hcl/hcl.go @@ -80,10 +80,6 @@ func GetAttrString(attr *hclwrite.Attribute, errPrefix string) (string, error) { return val.AsString(), nil } -func TokenNewLine() *hclwrite.Token { - return &hclwrite.Token{Type: hclsyntax.TokenNewline, Bytes: []byte("\n")} -} - // TokensArray creates an array of objects. func TokensArray(bodies []*hclwrite.Body) hclwrite.Tokens { tokens := make([]hclwrite.Tokens, 0) @@ -100,7 +96,7 @@ func TokensArraySingle(body *hclwrite.Body) hclwrite.Tokens { // TokensObject creates an object. func TokensObject(body *hclwrite.Body) hclwrite.Tokens { - tokens := hclwrite.Tokens{TokenNewLine()} + tokens := hclwrite.Tokens{tokenNewLine} tokens = append(tokens, RemoveLeadingNewline(body.BuildTokens(nil))...) return EncloseBraces(tokens, false) } @@ -159,9 +155,7 @@ func joinTokens(tokens ...hclwrite.Tokens) hclwrite.Tokens { for i := range tokens { ret = append(ret, tokens[i]...) if i < len(tokens)-1 { - ret = append(ret, - &hclwrite.Token{Type: hclsyntax.TokenComma, Bytes: []byte(",")}, - TokenNewLine()) + ret = append(ret, &hclwrite.Token{Type: hclsyntax.TokenComma, Bytes: []byte(",")}, tokenNewLine) } } return ret @@ -178,7 +172,7 @@ func EncloseParens(tokens hclwrite.Tokens) hclwrite.Tokens { func EncloseBraces(tokens hclwrite.Tokens, initialNewLine bool) hclwrite.Tokens { ret := hclwrite.Tokens{{Type: hclsyntax.TokenOBrace, Bytes: []byte("{")}} if initialNewLine { - ret = append(ret, TokenNewLine()) + ret = append(ret, tokenNewLine) } ret = append(ret, tokens...) return append(ret, &hclwrite.Token{Type: hclsyntax.TokenCBrace, Bytes: []byte("}")}) @@ -193,7 +187,9 @@ func EncloseBrackets(tokens hclwrite.Tokens) hclwrite.Tokens { // EncloseNewLines encloses tokens with newlines at the beginning and end. func EncloseNewLines(tokens hclwrite.Tokens) hclwrite.Tokens { - ret := hclwrite.Tokens{TokenNewLine()} + ret := hclwrite.Tokens{tokenNewLine} ret = append(ret, tokens...) - return append(ret, TokenNewLine()) + return append(ret, tokenNewLine) } + +var tokenNewLine = &hclwrite.Token{Type: hclsyntax.TokenNewline, Bytes: []byte("\n")} From 1883ff0d589bee5f1d6ae968719f5654d4a0c7ba Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Wed, 19 Mar 2025 10:43:57 +0100 Subject: [PATCH 21/36] add analytics specs --- .../clu2adv/dynamic_regions_config_auto_scaling.in.tf | 1 + .../clu2adv/dynamic_regions_config_auto_scaling.out.tf | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/internal/convert/testdata/clu2adv/dynamic_regions_config_auto_scaling.in.tf b/internal/convert/testdata/clu2adv/dynamic_regions_config_auto_scaling.in.tf index 9f0aaa9..bbc64bc 100644 --- a/internal/convert/testdata/clu2adv/dynamic_regions_config_auto_scaling.in.tf +++ b/internal/convert/testdata/clu2adv/dynamic_regions_config_auto_scaling.in.tf @@ -25,6 +25,7 @@ resource "mongodbatlas_cluster" "cluster" { content { region_name = regions_config.value.region_name electable_nodes = regions_config.value.electable_nodes + analytics_nodes = regions_config.value.analytics_nodes priority = regions_config.value.priority read_only_nodes = regions_config.value.read_only_nodes } diff --git a/internal/convert/testdata/clu2adv/dynamic_regions_config_auto_scaling.out.tf b/internal/convert/testdata/clu2adv/dynamic_regions_config_auto_scaling.out.tf index 94ea6d1..9dbadf6 100644 --- a/internal/convert/testdata/clu2adv/dynamic_regions_config_auto_scaling.out.tf +++ b/internal/convert/testdata/clu2adv/dynamic_regions_config_auto_scaling.out.tf @@ -36,6 +36,13 @@ resource "mongodbatlas_advanced_cluster" "cluster" { ebs_volume_type = var.provider_volume_type disk_iops = var.provider_disk_iops } : null + analytics_specs = region.analytics_nodes > 0 ? { + node_count = region.analytics_nodes + instance_size = var.provider_instance_size_name + disk_size_gb = var.disk_size_gb + ebs_volume_type = var.provider_volume_type + disk_iops = var.provider_disk_iops + } : null auto_scaling = { disk_gb_enabled = var.auto_scaling_disk_gb_enabled } From 4684c7c8f5f67f43fadea775134b6fd881a8b162 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Wed, 19 Mar 2025 11:53:26 +0100 Subject: [PATCH 22/36] example in readme --- README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2a1cdfc..3adec25 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,22 @@ dynamic "tags" { #### Dynamic blocks in regions_config You can use `dynamic` blocks for `regions_config`. The plugin assumes that `for_each` has an expression which is evaluated to a `list` or `set` of objects. -Dynamic block and individual blocks for `regions_config` are not supported at the same time in a `replication_specs`. +Dynamic block and individual blocks for `regions_config` are not supported at the same time in a `replication_specs`. This is an example of how to use dynamic blocks in `regions_config`: +```hcl + replication_specs { + num_shards = var.replication_specs.num_shards + zone_name = var.replication_specs.zone_name # only needed if you're using zones + dynamic "regions_config" { + for_each = var.replication_specs.regions_config + content { + priority = regions_config.value.priority + region_name = regions_config.value.region_name + electable_nodes = regions_config.value.electable_nodes + read_only_nodes = regions_config.value.read_only_nodes + } + } + } +``` ### Limitations From 861a6c4e9839989c3a70ebc835e4a38e6406ab80 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Wed, 19 Mar 2025 11:55:29 +0100 Subject: [PATCH 23/36] clarify num_shards limitation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3adec25..aa15639 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ Dynamic block and individual blocks for `regions_config` are not supported at th ### 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`. +- [`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 currently supported only for `tags`, `labels` and `regions_config`. **Coming soon**: support for `replication_specs`. ## Contributing From 165256e5ae85d7f6d56ee1abf039972b828a4f74 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Wed, 19 Mar 2025 12:10:15 +0100 Subject: [PATCH 24/36] feedback section --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index aa15639..36848cb 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,10 @@ Dynamic block and individual blocks for `regions_config` are not supported at th - [`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 currently supported only for `tags`, `labels` and `regions_config`. **Coming soon**: support for `replication_specs`. +## Feedback + +If you find any issues or have any suggestions, please open an [issue](https://github.com/mongodb-labs/atlas-cli-plugin-terraform/issues) in this repository. + ## Contributing See our [CONTRIBUTING.md](CONTRIBUTING.md) guide. From 11314b50eb14597bd4a204d9659b004307a21767 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Wed, 19 Mar 2025 13:05:02 +0100 Subject: [PATCH 25/36] getDynamicBlockRegionConfigsRegionArray --- internal/convert/convert.go | 67 +++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index 5d7f7aa..5bb365e 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -321,46 +321,20 @@ func fillRegionConfigsDynamicBlock(specbSrc *hclwrite.Body, root attrVals) (dyna return dynamicBlock{}, fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) } zoneName := hcl.GetAttrExpr(specbSrc.GetAttribute(nZoneName)) - configSrc := d.content - configSrcb := configSrc.Body() - var priorityStr string - - // Change value references in all attributes, e.g. regions_config.value.electable_nodes to region.electable_nodes - for name, attr := range configSrcb.Attributes() { - expr := hcl.GetAttrExpr(attr) - expr = strings.ReplaceAll(expr, - fmt.Sprintf("%s.%s.", nConfigSrc, nValue), - fmt.Sprintf("%s.", nRegion)) - if name == nPriority { - priorityStr = expr - } - configSrcb.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) - } - if priorityStr == "" { - return dynamicBlock{}, fmt.Errorf("%s: %s not found", errRepSpecs, nPriority) - } - region, err := getRegionConfig(configSrc, root, true) - if err != nil { - return dynamicBlock{}, err - } - forOuter := fmt.Sprintf("for i in range(%s) :", hcl.GetAttrExpr(shards)) repSpec := hclwrite.NewEmptyFile() repSpecb := repSpec.Body() if zoneName != "" { repSpecb.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(zoneName)) } - config := region.BuildTokens(nil) - config = hcl.EncloseBraces(config, true) - config = append(config, hcl.TokensFromExpr(fmt.Sprintf("if priority == %s", priorityStr))...) - forRegion := hcl.TokensFromExpr(fmt.Sprintf("for region in %s :", hcl.GetAttrExpr(d.forEach))) - forRegion = append(forRegion, config...) - - priorityContent := hcl.EncloseBrackets(hcl.EncloseNewLines(forRegion)) + regionArray, err := getDynamicBlockRegionConfigsRegionArray(d, root) + if err != nil { + return dynamicBlock{}, err + } priorityBlock := hcl.TokensFromExpr("for priority in range(7, 0, -1) : ") - priorityBlock = append(priorityBlock, priorityContent...) + priorityBlock = append(priorityBlock, regionArray...) repSpecb.SetAttributeRaw(nConfig, hcl.TokensFuncFlatten(priorityBlock)) - tokens := hcl.TokensFromExpr(forOuter) + tokens := hcl.TokensFromExpr(fmt.Sprintf("for i in range(%s) :", hcl.GetAttrExpr(shards))) tokens = append(tokens, hcl.EncloseBraces(repSpec.BuildTokens(nil), true)...) tokens = hcl.EncloseBrackets(hcl.EncloseNewLines(tokens)) d.tokens = tokens @@ -551,6 +525,35 @@ func encloseDynamicBlockRegionSpec(specTokens hclwrite.Tokens, countName string) return append(tokens, hcl.TokensFromExpr(": null")...) } +// getDynamicBlockRegionConfigsRegionArray returns the region array for a dynamic block in replication_specs. +// e.g. [ for region in var.replication_specs.regions_config : { ... } if priority == region.priority ] +func getDynamicBlockRegionConfigsRegionArray(d dynamicBlock, root attrVals) (hclwrite.Tokens, error) { + transformDynamicBlockReferences(d.content.Body()) + priorityStr := hcl.GetAttrExpr(d.content.Body().GetAttribute(nPriority)) + if priorityStr == "" { + return nil, fmt.Errorf("%s: %s not found", errRepSpecs, nPriority) + } + region, err := getRegionConfig(d.content, root, true) + if err != nil { + return nil, err + } + tokens := hcl.TokensFromExpr(fmt.Sprintf("for %s in %s :", nRegion, hcl.GetAttrExpr(d.forEach))) + tokens = append(tokens, hcl.EncloseBraces(region.BuildTokens(nil), true)...) + tokens = append(tokens, hcl.TokensFromExpr(fmt.Sprintf("if %s == %s", nPriority, priorityStr))...) + return hcl.EncloseBrackets(hcl.EncloseNewLines(tokens)), nil +} + +// transformDynamicBlockReferences changes value references in all attributes, e.g. regions_config.value.electable_nodes to region.electable_nodes +func transformDynamicBlockReferences(configSrcb *hclwrite.Body) { + for name, attr := range configSrcb.Attributes() { + expr := hcl.GetAttrExpr(attr) + expr = strings.ReplaceAll(expr, + fmt.Sprintf("%s.%s.", nConfigSrc, nValue), + fmt.Sprintf("%s.", nRegion)) + configSrcb.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) + } +} + func sortConfigsByPriority(configs []*hclwrite.Body) []*hclwrite.Body { for _, config := range configs { if _, err := hcl.GetAttrInt(config.GetAttribute(nPriority), errPriority); err != nil { From b2c316159b5df1e0e5445f161b3987d0a0f90bfb Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Wed, 19 Mar 2025 13:15:41 +0100 Subject: [PATCH 26/36] refactor fillRegionConfigsDynamicBlock --- internal/convert/convert.go | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index 5bb365e..7f8e675 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -22,12 +22,12 @@ const ( advClusterPlural = "mongodbatlas_advanced_clusters" valClusterType = "REPLICASET" valMaxPriority = 7 - - errFreeCluster = "free cluster (because no " + nRepSpecs + ")" - errRepSpecs = "setting " + nRepSpecs - errConfigs = "setting " + nConfig - errPriority = "setting " + nPriority - errNumShards = "setting " + nNumShards + valMinPriority = 0 + errFreeCluster = "free cluster (because no " + nRepSpecs + ")" + errRepSpecs = "setting " + nRepSpecs + errConfigs = "setting " + nConfig + errPriority = "setting " + nPriority + errNumShards = "setting " + nNumShards commentGeneratedBy = "Generated by atlas-cli-plugin-terraform." commentConfirmReferences = "Please review the changes and confirm that references to this resource are updated." @@ -320,24 +320,22 @@ func fillRegionConfigsDynamicBlock(specbSrc *hclwrite.Body, root attrVals) (dyna if shards == nil { return dynamicBlock{}, fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) } - zoneName := hcl.GetAttrExpr(specbSrc.GetAttribute(nZoneName)) repSpec := hclwrite.NewEmptyFile() repSpecb := repSpec.Body() - if zoneName != "" { + if zoneName := hcl.GetAttrExpr(specbSrc.GetAttribute(nZoneName)); zoneName != "" { repSpecb.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(zoneName)) } - regionArray, err := getDynamicBlockRegionConfigsRegionArray(d, root) + regionFor, err := getDynamicBlockRegionConfigsRegionArray(d, root) if err != nil { return dynamicBlock{}, err } - priorityBlock := hcl.TokensFromExpr("for priority in range(7, 0, -1) : ") - priorityBlock = append(priorityBlock, regionArray...) - repSpecb.SetAttributeRaw(nConfig, hcl.TokensFuncFlatten(priorityBlock)) + priorityFor := hcl.TokensFromExpr(fmt.Sprintf("for %s in range(%d, %d, -1) : ", nPriority, valMaxPriority, valMinPriority)) + priorityFor = append(priorityFor, regionFor...) + repSpecb.SetAttributeRaw(nConfig, hcl.TokensFuncFlatten(priorityFor)) tokens := hcl.TokensFromExpr(fmt.Sprintf("for i in range(%s) :", hcl.GetAttrExpr(shards))) tokens = append(tokens, hcl.EncloseBraces(repSpec.BuildTokens(nil), true)...) - tokens = hcl.EncloseBrackets(hcl.EncloseNewLines(tokens)) - d.tokens = tokens + d.tokens = hcl.EncloseBrackets(hcl.EncloseNewLines(tokens)) return d, nil } From 8f6e9677997cc7ba4f6143ed063a1b9df293b296 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Wed, 19 Mar 2025 13:20:19 +0100 Subject: [PATCH 27/36] EncloseBracketsNewLines --- internal/convert/convert.go | 4 ++-- internal/hcl/hcl.go | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index 7f8e675..c72cad0 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -335,7 +335,7 @@ func fillRegionConfigsDynamicBlock(specbSrc *hclwrite.Body, root attrVals) (dyna tokens := hcl.TokensFromExpr(fmt.Sprintf("for i in range(%s) :", hcl.GetAttrExpr(shards))) tokens = append(tokens, hcl.EncloseBraces(repSpec.BuildTokens(nil), true)...) - d.tokens = hcl.EncloseBrackets(hcl.EncloseNewLines(tokens)) + d.tokens = hcl.EncloseBracketsNewLines(tokens) return d, nil } @@ -538,7 +538,7 @@ func getDynamicBlockRegionConfigsRegionArray(d dynamicBlock, root attrVals) (hcl tokens := hcl.TokensFromExpr(fmt.Sprintf("for %s in %s :", nRegion, hcl.GetAttrExpr(d.forEach))) tokens = append(tokens, hcl.EncloseBraces(region.BuildTokens(nil), true)...) tokens = append(tokens, hcl.TokensFromExpr(fmt.Sprintf("if %s == %s", nPriority, priorityStr))...) - return hcl.EncloseBrackets(hcl.EncloseNewLines(tokens)), nil + return hcl.EncloseBracketsNewLines(tokens), nil } // transformDynamicBlockReferences changes value references in all attributes, e.g. regions_config.value.electable_nodes to region.electable_nodes diff --git a/internal/hcl/hcl.go b/internal/hcl/hcl.go index 43c1089..f068a22 100644 --- a/internal/hcl/hcl.go +++ b/internal/hcl/hcl.go @@ -86,7 +86,7 @@ func TokensArray(bodies []*hclwrite.Body) hclwrite.Tokens { for i := range bodies { tokens = append(tokens, TokensObject(bodies[i])) } - return EncloseBrackets(EncloseNewLines(joinTokens(tokens...))) + return EncloseBracketsNewLines(joinTokens(tokens...)) } // TokensArraySingle creates an array of one object. @@ -121,7 +121,7 @@ func TokensFuncMerge(tokens ...hclwrite.Tokens) hclwrite.Tokens { // TokensFuncFlatten creates the tokens for the HCL flatten function. func TokensFuncFlatten(tokens hclwrite.Tokens) hclwrite.Tokens { ret := TokensFromExpr("flatten") - return append(ret, EncloseParens(EncloseBrackets(EncloseNewLines(tokens)))...) + return append(ret, EncloseParens(EncloseBracketsNewLines(tokens))...) } // RemoveLeadingNewline removes the first newline if it exists to make the output prettier. @@ -192,4 +192,9 @@ func EncloseNewLines(tokens hclwrite.Tokens) hclwrite.Tokens { return append(ret, tokenNewLine) } +// EncloseBracketsNewLines encloses tokens with square brackets and newlines, [ \n ... \n ]. +func EncloseBracketsNewLines(tokens hclwrite.Tokens) hclwrite.Tokens { + return EncloseBrackets(EncloseNewLines(tokens)) +} + var tokenNewLine = &hclwrite.Token{Type: hclsyntax.TokenNewline, Bytes: []byte("\n")} From 0f2dfeb6eea16077fafbca5f2057addc86bec374 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Wed, 19 Mar 2025 13:27:40 +0100 Subject: [PATCH 28/36] fillRegionConfigsDynamicBlock doc --- internal/convert/convert.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index c72cad0..0069229 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -311,6 +311,7 @@ func fillBlockOpt(resourceb *hclwrite.Body, name string) { resourceb.SetAttributeRaw(name, hcl.TokensObject(block.Body())) } +// fillRegionConfigsDynamicBlock is used for dynamic blocks in region_configs func fillRegionConfigsDynamicBlock(specbSrc *hclwrite.Body, root attrVals) (dynamicBlock, error) { d, err := getDynamicBlock(specbSrc, nConfigSrc) if err != nil || !d.IsPresent() { From 876b6eb03d3fec10b90b48b8be687ccda7380640 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Wed, 19 Mar 2025 13:32:22 +0100 Subject: [PATCH 29/36] move shards closer to where it's used --- internal/convert/convert.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index 0069229..52cbe6f 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -317,10 +317,6 @@ func fillRegionConfigsDynamicBlock(specbSrc *hclwrite.Body, root attrVals) (dyna if err != nil || !d.IsPresent() { return dynamicBlock{}, err } - shards := specbSrc.GetAttribute(nNumShards) - if shards == nil { - return dynamicBlock{}, fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) - } repSpec := hclwrite.NewEmptyFile() repSpecb := repSpec.Body() if zoneName := hcl.GetAttrExpr(specbSrc.GetAttribute(nZoneName)); zoneName != "" { @@ -334,6 +330,10 @@ func fillRegionConfigsDynamicBlock(specbSrc *hclwrite.Body, root attrVals) (dyna 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) + } tokens := hcl.TokensFromExpr(fmt.Sprintf("for i in range(%s) :", hcl.GetAttrExpr(shards))) tokens = append(tokens, hcl.EncloseBraces(repSpec.BuildTokens(nil), true)...) d.tokens = hcl.EncloseBracketsNewLines(tokens) From 4ab2d3a06cb6cc1ff1d6936a0a788ab9c31c30d9 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Wed, 19 Mar 2025 18:02:19 +0100 Subject: [PATCH 30/36] add comment for priority loop --- internal/convert/convert.go | 4 +++- .../clu2adv/dynamic_regions_config_auto_scaling.out.tf | 1 + .../testdata/clu2adv/dynamic_regions_config_basic.out.tf | 1 + internal/hcl/hcl.go | 8 ++++++-- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index 52cbe6f..0882a00 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -33,6 +33,7 @@ const ( commentConfirmReferences = "Please review the changes and confirm that references to this resource are updated." commentMovedBlock = "Moved blocks" commentRemovedOld = "Note: Remember to remove or comment out the old cluster definitions." + commentPriorityFor = "Regions must be sorted by priority in descending order." ) var ( @@ -326,7 +327,8 @@ func fillRegionConfigsDynamicBlock(specbSrc *hclwrite.Body, root attrVals) (dyna if err != nil { return dynamicBlock{}, err } - priorityFor := hcl.TokensFromExpr(fmt.Sprintf("for %s in range(%d, %d, -1) : ", nPriority, valMaxPriority, valMinPriority)) + priorityFor := hcl.TokensComment(commentPriorityFor) + priorityFor = append(priorityFor, hcl.TokensFromExpr(fmt.Sprintf("for %s in range(%d, %d, -1) : ", nPriority, valMaxPriority, valMinPriority))...) priorityFor = append(priorityFor, regionFor...) repSpecb.SetAttributeRaw(nConfig, hcl.TokensFuncFlatten(priorityFor)) diff --git a/internal/convert/testdata/clu2adv/dynamic_regions_config_auto_scaling.out.tf b/internal/convert/testdata/clu2adv/dynamic_regions_config_auto_scaling.out.tf index 9dbadf6..62bbd5f 100644 --- a/internal/convert/testdata/clu2adv/dynamic_regions_config_auto_scaling.out.tf +++ b/internal/convert/testdata/clu2adv/dynamic_regions_config_auto_scaling.out.tf @@ -17,6 +17,7 @@ resource "mongodbatlas_advanced_cluster" "cluster" { replication_specs = [ for i in range(var.replication_specs.num_shards) : { region_configs = flatten([ + # Regions must be sorted by priority in descending order. for priority in range(7, 0, -1) : [ for region in var.replication_specs.regions_config : { provider_name = var.provider_name diff --git a/internal/convert/testdata/clu2adv/dynamic_regions_config_basic.out.tf b/internal/convert/testdata/clu2adv/dynamic_regions_config_basic.out.tf index be50ca3..a2f1050 100644 --- a/internal/convert/testdata/clu2adv/dynamic_regions_config_basic.out.tf +++ b/internal/convert/testdata/clu2adv/dynamic_regions_config_basic.out.tf @@ -6,6 +6,7 @@ resource "mongodbatlas_advanced_cluster" "dynamic_regions_config" { for i in range(var.replication_specs.num_shards) : { zone_name = var.zone_name region_configs = flatten([ + # Regions must be sorted by priority in descending order. for priority in range(7, 0, -1) : [ for region in var.replication_specs.regions_config : { provider_name = "AWS" diff --git a/internal/hcl/hcl.go b/internal/hcl/hcl.go index f068a22..8b71cfb 100644 --- a/internal/hcl/hcl.go +++ b/internal/hcl/hcl.go @@ -134,10 +134,14 @@ func RemoveLeadingNewline(tokens hclwrite.Tokens) hclwrite.Tokens { // AppendComment adds a comment at the end of the body. func AppendComment(body *hclwrite.Body, comment string) { - tokens := hclwrite.Tokens{ + body.AppendUnstructuredTokens(TokensComment(comment)) +} + +// TokensComment returns the tokens for a comment. +func TokensComment(comment string) hclwrite.Tokens { + return hclwrite.Tokens{ &hclwrite.Token{Type: hclsyntax.TokenComment, Bytes: []byte("# " + comment + "\n")}, } - body.AppendUnstructuredTokens(tokens) } // GetParser returns a parser for the given config and checks HCL syntax is valid From 32ca654d81322f14c2edc0029a7ab9a91f63b25d Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Wed, 19 Mar 2025 18:12:30 +0100 Subject: [PATCH 31/36] add dynamic block doc --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 36848cb..3e12749 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,8 @@ dynamic "tags" { #### Dynamic blocks in regions_config You can use `dynamic` blocks for `regions_config`. The plugin assumes that `for_each` has an expression which is evaluated to a `list` or `set` of objects. -Dynamic block and individual blocks for `regions_config` are not supported at the same time in a `replication_specs`. This is an example of how to use dynamic blocks in `regions_config`: +Dynamic block and individual blocks for `regions_config` are not supported at the same time in a `replication_specs`. If you need this use case, please send us feedback. +This is an example of how to use dynamic blocks in `regions_config`: ```hcl replication_specs { num_shards = var.replication_specs.num_shards @@ -96,7 +97,7 @@ Dynamic block and individual blocks for `regions_config` are not supported at th ### 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 currently supported only for `tags`, `labels` and `regions_config`. **Coming soon**: support for `replication_specs`. +- `dynamic` blocks are currently supported only for `tags`, `labels` and `regions_config`. See their limitations in their corresponding dynamic block sections above. **Coming soon**: support for `replication_specs`. ## Feedback From 0d61fe7e1d6fb5d2dba476bdcad7885745d6971c Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Wed, 19 Mar 2025 18:25:41 +0100 Subject: [PATCH 32/36] small doc adjustment --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3e12749..5b7b9b1 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ This is an example of how to use dynamic blocks in `regions_config`: ### 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 currently supported only for `tags`, `labels` and `regions_config`. See their limitations in their corresponding dynamic block sections above. **Coming soon**: support for `replication_specs`. +- `dynamic` blocks are currently supported only for `tags`, `labels` and `regions_config`. See limitations in their corresponding dynamic block sections above. **Coming soon**: support for `replication_specs`. ## Feedback From 5fa1cb71332d2884996a36c9ed2d7fa3a151479f Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Wed, 19 Mar 2025 19:00:11 +0100 Subject: [PATCH 33/36] rename to fillReplicationSpecsWithDynamicRegionConfigs --- internal/convert/convert.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index 0882a00..8482b2c 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -202,7 +202,7 @@ func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error { break } specbSrc := specSrc.Body() - d, err := fillRegionConfigsDynamicBlock(specbSrc, root) + d, err := fillReplicationSpecsWithDynamicRegionConfigs(specbSrc, root) if err != nil { return err } @@ -312,8 +312,8 @@ func fillBlockOpt(resourceb *hclwrite.Body, name string) { resourceb.SetAttributeRaw(name, hcl.TokensObject(block.Body())) } -// fillRegionConfigsDynamicBlock is used for dynamic blocks in region_configs -func fillRegionConfigsDynamicBlock(specbSrc *hclwrite.Body, root attrVals) (dynamicBlock, error) { +// fillReplicationSpecsWithDynamicRegionConfigs is used for dynamic blocks in region_configs +func fillReplicationSpecsWithDynamicRegionConfigs(specbSrc *hclwrite.Body, root attrVals) (dynamicBlock, error) { d, err := getDynamicBlock(specbSrc, nConfigSrc) if err != nil || !d.IsPresent() { return dynamicBlock{}, err From b8258ae29929182df01a84665d7ef79dd2db3040 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Wed, 19 Mar 2025 19:03:01 +0100 Subject: [PATCH 34/36] Update README.md Co-authored-by: Marco Suma --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5b7b9b1..9701644 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ dynamic "tags" { #### Dynamic blocks in regions_config You can use `dynamic` blocks for `regions_config`. The plugin assumes that `for_each` has an expression which is evaluated to a `list` or `set` of objects. -Dynamic block and individual blocks for `regions_config` are not supported at the same time in a `replication_specs`. If you need this use case, please send us feedback. +Dynamic block and individual blocks for `regions_config` are not supported at the same time in a `replication_specs`. If you need this use case, please send us [feedback](https://github.com/mongodb-labs/atlas-cli-plugin-terraform/issues). This is an example of how to use dynamic blocks in `regions_config`: ```hcl replication_specs { From f90d5a2a783575be955645e55d53757db363a00b Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Wed, 19 Mar 2025 19:16:06 +0100 Subject: [PATCH 35/36] link to limitations --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9701644..6c653c5 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ This is an example of how to use dynamic blocks in `regions_config`: ### 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 currently supported only for `tags`, `labels` and `regions_config`. See limitations in their corresponding dynamic block sections above. **Coming soon**: support for `replication_specs`. +- `dynamic` blocks are currently supported only for `tags`, `labels` and `regions_config`. See limitations for `regions_config` support in [its section](#dynamic-blocks-in-regions_config) above. **Coming soon**: support for `replication_specs`. ## Feedback From 2bec1f999dfc9ef78306aff8a8aa25c5fec49d6a Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Wed, 19 Mar 2025 19:32:34 +0100 Subject: [PATCH 36/36] how to handle limitation --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c653c5..69fd104 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,6 @@ dynamic "tags" { #### Dynamic blocks in regions_config You can use `dynamic` blocks for `regions_config`. The plugin assumes that `for_each` has an expression which is evaluated to a `list` or `set` of objects. -Dynamic block and individual blocks for `regions_config` are not supported at the same time in a `replication_specs`. If you need this use case, please send us [feedback](https://github.com/mongodb-labs/atlas-cli-plugin-terraform/issues). This is an example of how to use dynamic blocks in `regions_config`: ```hcl replication_specs { @@ -93,6 +92,9 @@ This is an example of how to use dynamic blocks in `regions_config`: } } ``` +Dynamic block and individual blocks for `regions_config` are not supported at the same time. If you need this use case, please send us [feedback](https://github.com/mongodb-labs/atlas-cli-plugin-terraform/issues). There are currently two main approaches to handle this: +- (Recommended) Remove the individual `regions_config` blocks and add their information to the variable you're using in the `for_each` expression, e.g. using [concat](https://developer.hashicorp.com/terraform/language/functions/concat) if you're using a list or [setunion](https://developer.hashicorp.com/terraform/language/functions/setunion) for sets. In this way, you don't need to change the generated `mongodb_advanced_cluster` configuration. +- Change the generated `mongodb_advanced_cluster` configuration to join the individual blocks to the code generated for the `dynamic` block. This approach is more error-prone. ### Limitations