From e447591a1f6d681becb2ae22377932ba65fe6cf7 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Fri, 7 Feb 2025 08:06:12 +0100 Subject: [PATCH 01/17] failing test --- .../testdata/clu2adv/multi_region.in.tf | 34 +++++++++++++++ .../testdata/clu2adv/multi_region.out.tf | 43 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 internal/convert/testdata/clu2adv/multi_region.in.tf create mode 100644 internal/convert/testdata/clu2adv/multi_region.out.tf diff --git a/internal/convert/testdata/clu2adv/multi_region.in.tf b/internal/convert/testdata/clu2adv/multi_region.in.tf new file mode 100644 index 0000000..7898da6 --- /dev/null +++ b/internal/convert/testdata/clu2adv/multi_region.in.tf @@ -0,0 +1,34 @@ +resource "mongodbatlas_cluster" "multi_region" { + project_id = "1234" + name = "cluster-multi-region" + disk_size_gb = 100 + num_shards = 1 + cloud_backup = true + cluster_type = "REPLICASET" + + // Provider Settings "block" + provider_name = "AWS" + provider_instance_size_name = "M10" + + // priorities are not in descending order so regions will be reordered + replication_specs { + num_shards = 1 + regions_config { + region_name = "US_WEST_2" + electable_nodes = 3 + priority = 6 + read_only_nodes = 0 + } + regions_config { + region_name = "US_WEST_1" + electable_nodes = 1 + priority = 5 + } + regions_config { + region_name = "US_EAST_1" + electable_nodes = 3 + priority = 7 + read_only_nodes = 0 + } + } +} diff --git a/internal/convert/testdata/clu2adv/multi_region.out.tf b/internal/convert/testdata/clu2adv/multi_region.out.tf new file mode 100644 index 0000000..344c57f --- /dev/null +++ b/internal/convert/testdata/clu2adv/multi_region.out.tf @@ -0,0 +1,43 @@ +resource "mongodbatlas_advanced_cluster" "multi_region" { + project_id = "1234" + name = "cluster-multi-region" + cluster_type = "REPLICASET" + + + backup_enabled = true + replication_specs = [{ + num_shards = 1 + region_configs = [{ + region_name = "US_EAST_1" + priority = 7 + provider_name = "AWS" + electable_specs = { + node_count = 3 + instance_size = "M10" + disk_size_gb = 100 + } + + }, { + region_name = "US_WEST_2" + priority = 6 + provider_name = "AWS" + electable_specs = { + node_count = 3 + instance_size = "M10" + disk_size_gb = 100 + } + }, { + region_name = "US_WEST_1" + priority = 5 + provider_name = "AWS" + electable_specs = { + node_count = 1 + instance_size = "M10" + disk_size_gb = 100 + } + }] + }] + + # Generated by atlas-cli-plugin-terraform. + # Please confirm that all references to this resource are updated. +} From c71389fca8c80077080f97787e3be4d5aa21502c Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Fri, 7 Feb 2025 17:44:37 +0100 Subject: [PATCH 02/17] multiple region configs --- internal/convert/convert.go | 38 +++++++++++-------- internal/convert/testdata/clu2adv/errors.json | 3 +- .../clu2adv/regions_config_missing.in.tf | 17 +++++++++ internal/hcl/hcl.go | 16 ++++++-- 4 files changed, 55 insertions(+), 19 deletions(-) create mode 100644 internal/convert/testdata/clu2adv/regions_config_missing.in.tf diff --git a/internal/convert/convert.go b/internal/convert/convert.go index 334dea5..dd8dac0 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -81,8 +81,8 @@ func fillFreeTier(resourceb *hclwrite.Body) error { configb.SetAttributeRaw(nElectableSpecs, hcl.TokensObject(electableSpec)) repSpecs := hclwrite.NewEmptyFile() - repSpecs.Body().SetAttributeRaw(nConfig, hcl.TokensArrayObject(config)) - resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensArrayObject(repSpecs)) + repSpecs.Body().SetAttributeRaw(nConfig, hcl.TokensArraySingle(config)) + resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensArraySingle(repSpecs)) return nil } @@ -92,29 +92,37 @@ func fillReplicationSpecs(resourceb *hclwrite.Body) error { if errRoot != nil { return errRoot } - repSpecsSrc := resourceb.FirstMatchingBlock(nRepSpecs, nil) - configSrc := repSpecsSrc.Body().FirstMatchingBlock(nConfigSrc, nil) - if configSrc == nil { - return fmt.Errorf("%s: %s not found", errRepSpecs, nConfigSrc) - } - resourceb.RemoveAttribute(nNumShards) // num_shards in root is not relevant, only in replication_specs // ok to fail as cloud_backup is optional _ = hcl.MoveAttr(resourceb, resourceb, nCloudBackup, nBackupEnabled, errRepSpecs) - config, errConfig := getRegionConfigs(configSrc, root) - if errConfig != nil { - return errConfig + // at least one replication_specs exists here, if not it would be a free tier cluster + repSpecsSrc := resourceb.FirstMatchingBlock(nRepSpecs, nil) + var configs []*hclwrite.File + for { + configSrc := repSpecsSrc.Body().FirstMatchingBlock(nConfigSrc, nil) + if configSrc == nil { + break + } + config, err := getRegionConfigs(configSrc, root) + if err != nil { + return err + } + configs = append(configs, config) + repSpecsSrc.Body().RemoveBlock(configSrc) + } + if len(configs) == 0 { + return fmt.Errorf("%s: %s not found", errRepSpecs, nConfigSrc) } repSpecs := hclwrite.NewEmptyFile() - repSpecs.Body().SetAttributeRaw(nConfig, config) - resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensArrayObject(repSpecs)) + repSpecs.Body().SetAttributeRaw(nConfig, hcl.TokensArray(configs)) + resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensArraySingle(repSpecs)) resourceb.RemoveBlock(repSpecsSrc) return nil } -func getRegionConfigs(configSrc *hclwrite.Block, root attrVals) (hclwrite.Tokens, error) { +func getRegionConfigs(configSrc *hclwrite.Block, root attrVals) (*hclwrite.File, error) { file := hclwrite.NewEmptyFile() fileb := file.Body() fileb.SetAttributeRaw(nProviderName, root.req[nProviderName]) @@ -133,7 +141,7 @@ func getRegionConfigs(configSrc *hclwrite.Block, root attrVals) (hclwrite.Tokens return nil, errElect } fileb.SetAttributeRaw(nElectableSpecs, electableSpecs) - return hcl.TokensArrayObject(file), nil + return file, nil } func getElectableSpecs(configSrc *hclwrite.Block, root attrVals) (hclwrite.Tokens, error) { diff --git a/internal/convert/testdata/clu2adv/errors.json b/internal/convert/testdata/clu2adv/errors.json index cb08b6f..70d320a 100644 --- a/internal/convert/testdata/clu2adv/errors.json +++ b/internal/convert/testdata/clu2adv/errors.json @@ -1,5 +1,6 @@ { "configuration_file_error": "failed to parse Terraform config file", "free_cluster_missing_attribute": "free cluster (because no replication_specs): attribute backing_provider_name not found", - "autoscaling_missing_attribute": "setting replication_specs: attribute provider_instance_size_name not found" + "autoscaling_missing_attribute": "setting replication_specs: attribute provider_instance_size_name not found", + "regions_config_missing": "setting replication_specs: regions_config not found" } diff --git a/internal/convert/testdata/clu2adv/regions_config_missing.in.tf b/internal/convert/testdata/clu2adv/regions_config_missing.in.tf new file mode 100644 index 0000000..b62adf1 --- /dev/null +++ b/internal/convert/testdata/clu2adv/regions_config_missing.in.tf @@ -0,0 +1,17 @@ +resource "mongodbatlas_cluster" "autoscaling" { + project_id = var.project_id + name = var.cluster_name + disk_size_gb = 100 + num_shards = 1 + cluster_type = "REPLICASET" + + replication_specs { + num_shards = 1 + } + + //Provider Settings "block" + provider_name = "AWS" + provider_auto_scaling_compute_min_instance_size = "M10" + provider_auto_scaling_compute_max_instance_size = "M40" + provider_instance_size_name = "M20" +} diff --git a/internal/hcl/hcl.go b/internal/hcl/hcl.go index dcbc9d0..3d202a2 100644 --- a/internal/hcl/hcl.go +++ b/internal/hcl/hcl.go @@ -37,17 +37,27 @@ func SetAttrInt(body *hclwrite.Body, attrName string, number int) { body.SetAttributeRaw(attrName, tokens) } -// TokensArrayObject creates an array with a single object. -func TokensArrayObject(file *hclwrite.File) hclwrite.Tokens { +// TokensArray creates an array of objects. +func TokensArray(file []*hclwrite.File) hclwrite.Tokens { ret := hclwrite.Tokens{ {Type: hclsyntax.TokenOBrack, Bytes: []byte("[")}, } - ret = append(ret, TokensObject(file)...) + for i := range file { + ret = append(ret, TokensObject(file[i])...) + if i < len(file)-1 { + ret = append(ret, &hclwrite.Token{Type: hclsyntax.TokenComma, Bytes: []byte(",")}) + } + } ret = append(ret, &hclwrite.Token{Type: hclsyntax.TokenCBrack, Bytes: []byte("]")}) return ret } +// TokensArraySingle creates an array of one object. +func TokensArraySingle(file *hclwrite.File) hclwrite.Tokens { + return TokensArray([]*hclwrite.File{file}) +} + // TokensObject creates an object. func TokensObject(file *hclwrite.File) hclwrite.Tokens { ret := hclwrite.Tokens{ From cbed75ff16a6278b467452336cedb624281f92c9 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Fri, 7 Feb 2025 18:11:06 +0100 Subject: [PATCH 03/17] passing test with fixed order --- internal/convert/convert.go | 5 +++++ .../testdata/clu2adv/multi_region.out.tf | 20 +++++++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index dd8dac0..79fa51d 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -114,6 +114,11 @@ func fillReplicationSpecs(resourceb *hclwrite.Body) error { if len(configs) == 0 { return fmt.Errorf("%s: %s not found", errRepSpecs, nConfigSrc) } + + if len(configs) == 3 { // TODO: remove and make sort generic + configs[0], configs[1], configs[2] = configs[2], configs[0], configs[1] + } + repSpecs := hclwrite.NewEmptyFile() repSpecs.Body().SetAttributeRaw(nConfig, hcl.TokensArray(configs)) resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensArraySingle(repSpecs)) diff --git a/internal/convert/testdata/clu2adv/multi_region.out.tf b/internal/convert/testdata/clu2adv/multi_region.out.tf index 344c57f..126205d 100644 --- a/internal/convert/testdata/clu2adv/multi_region.out.tf +++ b/internal/convert/testdata/clu2adv/multi_region.out.tf @@ -6,30 +6,28 @@ resource "mongodbatlas_advanced_cluster" "multi_region" { backup_enabled = true replication_specs = [{ - num_shards = 1 region_configs = [{ + provider_name = "AWS" region_name = "US_EAST_1" priority = 7 - provider_name = "AWS" electable_specs = { - node_count = 3 - instance_size = "M10" - disk_size_gb = 100 + node_count = 3 + instance_size = "M10" + disk_size_gb = 100 } - }, { + provider_name = "AWS" region_name = "US_WEST_2" priority = 6 - provider_name = "AWS" electable_specs = { - node_count = 3 - instance_size = "M10" - disk_size_gb = 100 + node_count = 3 + instance_size = "M10" + disk_size_gb = 100 } }, { + provider_name = "AWS" region_name = "US_WEST_1" priority = 5 - provider_name = "AWS" electable_specs = { node_count = 1 instance_size = "M10" From cbd75398e0d66f23ade8d8b45e0e978a5db02364 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Fri, 7 Feb 2025 18:16:56 +0100 Subject: [PATCH 04/17] fix token types --- internal/hcl/hcl.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/hcl/hcl.go b/internal/hcl/hcl.go index 3d202a2..c97dee9 100644 --- a/internal/hcl/hcl.go +++ b/internal/hcl/hcl.go @@ -61,12 +61,12 @@ func TokensArraySingle(file *hclwrite.File) hclwrite.Tokens { // TokensObject creates an object. func TokensObject(file *hclwrite.File) hclwrite.Tokens { ret := hclwrite.Tokens{ - {Type: hclsyntax.TokenOBrack, Bytes: []byte("{")}, + {Type: hclsyntax.TokenOBrace, Bytes: []byte("{")}, {Type: hclsyntax.TokenNewline, Bytes: []byte("\n")}, } ret = append(ret, file.BuildTokens(nil)...) ret = append(ret, - &hclwrite.Token{Type: hclsyntax.TokenCBrack, Bytes: []byte("}")}) + &hclwrite.Token{Type: hclsyntax.TokenCBrace, Bytes: []byte("}")}) return ret } From 8df865f4ae512f15023154481f3fe2614701d940 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Sat, 8 Feb 2025 14:20:26 +0100 Subject: [PATCH 05/17] make priority mandatory --- README.md | 10 ++++++++-- internal/convert/convert.go | 19 ++++++++++++++++--- internal/convert/convert_test.go | 2 +- internal/convert/testdata/clu2adv/errors.json | 6 +++++- ...gions_config_missing_electable_nodes.in.tf | 17 +++++++++++++++++ .../regions_config_missing_priority.in.tf | 16 ++++++++++++++++ ...regions_config_out_of_range_priority.in.tf | 16 ++++++++++++++++ .../regions_config_unresolved_priority.in.tf | 16 ++++++++++++++++ ...cation_specs_missing_regions_config.in.tf} | 0 internal/hcl/hcl.go | 18 ++++++++++++++++++ 10 files changed, 113 insertions(+), 7 deletions(-) create mode 100644 internal/convert/testdata/clu2adv/regions_config_missing_electable_nodes.in.tf create mode 100644 internal/convert/testdata/clu2adv/regions_config_missing_priority.in.tf create mode 100644 internal/convert/testdata/clu2adv/regions_config_out_of_range_priority.in.tf create mode 100644 internal/convert/testdata/clu2adv/regions_config_unresolved_priority.in.tf rename internal/convert/testdata/clu2adv/{regions_config_missing.in.tf => replication_specs_missing_regions_config.in.tf} (100%) diff --git a/README.md b/README.md index 87ecd0a..8fa3a2c 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,10 @@ Install the plugin by running: atlas plugin install github.com/mongodb-labs/atlas-cli-plugin-terraform ``` -## Usage +## Convert cluster to advanced_cluster v2 + +### Usage -### Convert cluster to advanced_cluster v2 If you want to convert a Terraform configuration from `mongodbatlas_cluster` to `mongodbatlas_advanced_cluster` schema v2, use the following command: ```bash atlas terraform clusterToAdvancedCluster --file in.tf --output out.tf @@ -30,6 +31,11 @@ atlas tf clu2adv -f in.tf -o out.tf If you want to overwrite the output file if it exists, or even use the same output file as the input file, use the `--overwriteOutput true` or the `-w` flag. +### 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` is required in `regions_config` and must be a resolved number between 7 and 1, e.g. `var.prioriy` is not supported. This is to allow reordering them by descending priority as this is expected in `mongodbatlas_advanced_cluster`. + ## Contributing diff --git a/internal/convert/convert.go b/internal/convert/convert.go index 79fa51d..1ec6730 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -13,9 +13,12 @@ const ( cluster = "mongodbatlas_cluster" advCluster = "mongodbatlas_advanced_cluster" valClusterType = "REPLICASET" - valPriority = 7 + valMaxPriority = 7 + valMinPriority = 1 errFreeCluster = "free cluster (because no " + nRepSpecs + ")" errRepSpecs = "setting " + nRepSpecs + errConfigs = "setting " + nConfig + errPriority = "setting " + nPriority ) type attrVals struct { @@ -64,7 +67,7 @@ func fillFreeTier(resourceb *hclwrite.Body) error { resourceb.SetAttributeValue(nClusterType, cty.StringVal(valClusterType)) config := hclwrite.NewEmptyFile() configb := config.Body() - hcl.SetAttrInt(configb, "priority", valPriority) + hcl.SetAttrInt(configb, nPriority, valMaxPriority) if err := hcl.MoveAttr(resourceb, configb, nRegionNameSrc, nRegionName, errFreeCluster); err != nil { return err } @@ -134,9 +137,19 @@ func getRegionConfigs(configSrc *hclwrite.Block, root attrVals) (*hclwrite.File, if err := hcl.MoveAttr(configSrc.Body(), fileb, nRegionName, nRegionName, errRepSpecs); err != nil { return nil, err } - if err := hcl.MoveAttr(configSrc.Body(), fileb, nPriority, nPriority, errRepSpecs); err != nil { + priority := configSrc.Body().GetAttribute(nPriority) + if priority == nil { + return nil, fmt.Errorf("%s: %s not found", errRepSpecs, nPriority) + } + valPrioriy, err := hcl.GetAttrInt(priority, errPriority) + if err != nil { return nil, err } + if valPrioriy < valMinPriority || valPrioriy > valMaxPriority { + return nil, fmt.Errorf("%s: %s is %d but must be between %d and %d", errPriority, nPriority, valPrioriy, valMinPriority, valMaxPriority) + } + hcl.SetAttrInt(fileb, nPriority, valPrioriy) + autoScaling := getAutoScalingOpt(root.opt) if autoScaling != nil { fileb.SetAttributeRaw(nAutoScaling, autoScaling) diff --git a/internal/convert/convert_test.go b/internal/convert/convert_test.go index 098814a..d7efed5 100644 --- a/internal/convert/convert_test.go +++ b/internal/convert/convert_test.go @@ -43,7 +43,7 @@ func TestClusterToAdvancedCluster(t *testing.T) { g.Assert(t, testName, outConfig) } else { errMsg, found := errMap[testName] - assert.True(t, found, "error not found for test %s", testName) + assert.True(t, found, "error not found in file %s for test %s", errFilename, testName) assert.Contains(t, err.Error(), errMsg) } }) diff --git a/internal/convert/testdata/clu2adv/errors.json b/internal/convert/testdata/clu2adv/errors.json index 70d320a..8410062 100644 --- a/internal/convert/testdata/clu2adv/errors.json +++ b/internal/convert/testdata/clu2adv/errors.json @@ -2,5 +2,9 @@ "configuration_file_error": "failed to parse Terraform config file", "free_cluster_missing_attribute": "free cluster (because no replication_specs): attribute backing_provider_name not found", "autoscaling_missing_attribute": "setting replication_specs: attribute provider_instance_size_name not found", - "regions_config_missing": "setting replication_specs: regions_config 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_unresolved_priority": "setting priority: 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 new file mode 100644 index 0000000..f35c870 --- /dev/null +++ b/internal/convert/testdata/clu2adv/regions_config_missing_electable_nodes.in.tf @@ -0,0 +1,17 @@ +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 + } + } +} diff --git a/internal/convert/testdata/clu2adv/regions_config_missing_priority.in.tf b/internal/convert/testdata/clu2adv/regions_config_missing_priority.in.tf new file mode 100644 index 0000000..e4fa955 --- /dev/null +++ b/internal/convert/testdata/clu2adv/regions_config_missing_priority.in.tf @@ -0,0 +1,16 @@ +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 = 7 # missing priority + 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 new file mode 100644 index 0000000..e1fe04b --- /dev/null +++ b/internal/convert/testdata/clu2adv/regions_config_out_of_range_priority.in.tf @@ -0,0 +1,16 @@ +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 + } + } +} diff --git a/internal/convert/testdata/clu2adv/regions_config_unresolved_priority.in.tf b/internal/convert/testdata/clu2adv/regions_config_unresolved_priority.in.tf new file mode 100644 index 0000000..590febe --- /dev/null +++ b/internal/convert/testdata/clu2adv/regions_config_unresolved_priority.in.tf @@ -0,0 +1,16 @@ +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_missing.in.tf b/internal/convert/testdata/clu2adv/replication_specs_missing_regions_config.in.tf similarity index 100% rename from internal/convert/testdata/clu2adv/regions_config_missing.in.tf rename to internal/convert/testdata/clu2adv/replication_specs_missing_regions_config.in.tf diff --git a/internal/hcl/hcl.go b/internal/hcl/hcl.go index c97dee9..2c033f6 100644 --- a/internal/hcl/hcl.go +++ b/internal/hcl/hcl.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/hcl/v2/hclwrite" + "github.com/zclconf/go-cty/cty" ) // MoveAttr deletes an attribute from fromBody and adds it to toBody. @@ -37,6 +38,23 @@ func SetAttrInt(body *hclwrite.Body, attrName string, number int) { body.SetAttributeRaw(attrName, tokens) } +// GetAttrInt tries to get an attribute value as an int. +func GetAttrInt(attr *hclwrite.Attribute, errPrefix string) (int, error) { + expr, diags := hclsyntax.ParseExpression(attr.Expr().BuildTokens(nil).Bytes(), "", hcl.InitialPos) + if diags.HasErrors() { + return 0, fmt.Errorf("%s: failed to parse number: %s", errPrefix, diags.Error()) + } + val, diags := expr.Value(nil) + if diags.HasErrors() { + return 0, fmt.Errorf("%s: failed to evaluate number: %s", errPrefix, diags.Error()) + } + if !val.Type().Equals(cty.Number) { + return 0, fmt.Errorf("%s: attribute is not a number", errPrefix) + } + num, _ := val.AsBigFloat().Int64() + return int(num), nil +} + // TokensArray creates an array of objects. func TokensArray(file []*hclwrite.File) hclwrite.Tokens { ret := hclwrite.Tokens{ From 6a161a4b7ba9523f7053e8cad16b0f75c326d4f7 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Sat, 8 Feb 2025 15:39:47 +0100 Subject: [PATCH 06/17] extract setPriority --- internal/convert/convert.go | 39 +++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index 1ec6730..9de6adb 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -91,7 +91,7 @@ func fillFreeTier(resourceb *hclwrite.Body) error { // fillReplicationSpecs is the entry point to convert clusters with replications_specs (all but free tier) func fillReplicationSpecs(resourceb *hclwrite.Body) error { - root, errRoot := popRootAttrs(resourceb, errRepSpecs) + root, errRoot := popRootAttrs(resourceb) if errRoot != nil { return errRoot } @@ -107,7 +107,7 @@ func fillReplicationSpecs(resourceb *hclwrite.Body) error { if configSrc == nil { break } - config, err := getRegionConfigs(configSrc, root) + config, err := getRegionConfig(configSrc, root) if err != nil { return err } @@ -130,26 +130,16 @@ func fillReplicationSpecs(resourceb *hclwrite.Body) error { return nil } -func getRegionConfigs(configSrc *hclwrite.Block, root attrVals) (*hclwrite.File, error) { +func getRegionConfig(configSrc *hclwrite.Block, root attrVals) (*hclwrite.File, error) { file := hclwrite.NewEmptyFile() fileb := file.Body() fileb.SetAttributeRaw(nProviderName, root.req[nProviderName]) if err := hcl.MoveAttr(configSrc.Body(), fileb, nRegionName, nRegionName, errRepSpecs); err != nil { return nil, err } - priority := configSrc.Body().GetAttribute(nPriority) - if priority == nil { - return nil, fmt.Errorf("%s: %s not found", errRepSpecs, nPriority) - } - valPrioriy, err := hcl.GetAttrInt(priority, errPriority) - if err != nil { + if err := setPriority(fileb, configSrc.Body().GetAttribute(nPriority)); err != nil { return nil, err } - if valPrioriy < valMinPriority || valPrioriy > valMaxPriority { - return nil, fmt.Errorf("%s: %s is %d but must be between %d and %d", errPriority, nPriority, valPrioriy, valMinPriority, valMaxPriority) - } - hcl.SetAttrInt(fileb, nPriority, valPrioriy) - autoScaling := getAutoScalingOpt(root.opt) if autoScaling != nil { fileb.SetAttributeRaw(nAutoScaling, autoScaling) @@ -200,8 +190,23 @@ func getAutoScalingOpt(opt map[string]hclwrite.Tokens) hclwrite.Tokens { return hcl.TokensObject(file) } +func setPriority(body *hclwrite.Body, priority *hclwrite.Attribute) error { + if priority == nil { + return fmt.Errorf("%s: %s not found", errRepSpecs, nPriority) + } + valPrioriy, err := hcl.GetAttrInt(priority, errPriority) + if err != nil { + return err + } + if valPrioriy < valMinPriority || valPrioriy > valMaxPriority { + return fmt.Errorf("%s: %s is %d but must be between %d and %d", errPriority, nPriority, valPrioriy, valMinPriority, valMaxPriority) + } + hcl.SetAttrInt(body, nPriority, valPrioriy) + return nil +} + // popRootAttrs deletes the attributes common to all replication_specs/regions_config and returns them. -func popRootAttrs(body *hclwrite.Body, errPrefix string) (attrVals, error) { +func popRootAttrs(body *hclwrite.Body) (attrVals, error) { var ( reqNames = []string{ nProviderName, @@ -219,14 +224,14 @@ func popRootAttrs(body *hclwrite.Body, errPrefix string) (attrVals, error) { opt = make(map[string]hclwrite.Tokens) ) for _, name := range reqNames { - tokens, err := hcl.PopAttr(body, name, errPrefix) + tokens, err := hcl.PopAttr(body, name, errRepSpecs) if err != nil { return attrVals{}, err } req[name] = tokens } for _, name := range optNames { - tokens, _ := hcl.PopAttr(body, name, errPrefix) + tokens, _ := hcl.PopAttr(body, name, errRepSpecs) if tokens != nil { opt[name] = tokens } From 2a17f90a25a465533e0a6f08f498c6ba183f392f Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Sat, 8 Feb 2025 15:45:05 +0100 Subject: [PATCH 07/17] getRegionConfigs --- internal/convert/convert.go | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index 9de6adb..232c8e5 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -101,6 +101,19 @@ func fillReplicationSpecs(resourceb *hclwrite.Body) error { // at least one replication_specs exists here, if not it would be a free tier cluster repSpecsSrc := resourceb.FirstMatchingBlock(nRepSpecs, nil) + configs, errConfigs := getRegionConfigs(repSpecsSrc, root) + if errConfigs != nil { + return errConfigs + } + repSpecs := hclwrite.NewEmptyFile() + repSpecs.Body().SetAttributeRaw(nConfig, configs) + + resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensArraySingle(repSpecs)) + resourceb.RemoveBlock(repSpecsSrc) + return nil +} + +func getRegionConfigs(repSpecsSrc *hclwrite.Block, root attrVals) (hclwrite.Tokens, error) { var configs []*hclwrite.File for { configSrc := repSpecsSrc.Body().FirstMatchingBlock(nConfigSrc, nil) @@ -109,25 +122,19 @@ func fillReplicationSpecs(resourceb *hclwrite.Body) error { } config, err := getRegionConfig(configSrc, root) if err != nil { - return err + return nil, err } configs = append(configs, config) repSpecsSrc.Body().RemoveBlock(configSrc) } if len(configs) == 0 { - return fmt.Errorf("%s: %s not found", errRepSpecs, nConfigSrc) + return nil, fmt.Errorf("%s: %s not found", errRepSpecs, nConfigSrc) } if len(configs) == 3 { // TODO: remove and make sort generic configs[0], configs[1], configs[2] = configs[2], configs[0], configs[1] } - - repSpecs := hclwrite.NewEmptyFile() - repSpecs.Body().SetAttributeRaw(nConfig, hcl.TokensArray(configs)) - resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensArraySingle(repSpecs)) - - resourceb.RemoveBlock(repSpecsSrc) - return nil + return hcl.TokensArray(configs), nil } func getRegionConfig(configSrc *hclwrite.Block, root attrVals) (*hclwrite.File, error) { From 2548b9ab5be732df46bef9b456e8e42ad506dd33 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Sat, 8 Feb 2025 15:58:17 +0100 Subject: [PATCH 08/17] order configs by descending priority --- internal/convert/convert.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index 232c8e5..5bfd26e 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -2,6 +2,7 @@ package convert import ( "fmt" + "sort" "github.com/hashicorp/hcl/v2/hclwrite" "github.com/mongodb-labs/atlas-cli-plugin-terraform/internal/hcl" @@ -130,10 +131,11 @@ func getRegionConfigs(repSpecsSrc *hclwrite.Block, root attrVals) (hclwrite.Toke if len(configs) == 0 { return nil, fmt.Errorf("%s: %s not found", errRepSpecs, nConfigSrc) } - - if len(configs) == 3 { // TODO: remove and make sort generic - configs[0], configs[1], configs[2] = configs[2], configs[0], configs[1] - } + sort.Slice(configs, func(i, j int) bool { + pi, _ := hcl.GetAttrInt(configs[i].Body().GetAttribute(nPriority), errPriority) + pj, _ := hcl.GetAttrInt(configs[j].Body().GetAttribute(nPriority), errPriority) + return pi > pj + }) return hcl.TokensArray(configs), nil } From 9043ca12d3a25f577cc7714d1b963382393a3b15 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Sat, 8 Feb 2025 17:22:02 +0100 Subject: [PATCH 09/17] dynamic blocks not supported --- README.md | 1 + internal/convert/convert.go | 16 ++++++++ internal/convert/testdata/clu2adv/errors.json | 4 +- .../regions_config_unsupported_dynamic.in.tf | 38 +++++++++++++++++++ ...eplication_specs_unsupported_dynamic.in.tf | 29 ++++++++++++++ 5 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 internal/convert/testdata/clu2adv/regions_config_unsupported_dynamic.in.tf create mode 100644 internal/convert/testdata/clu2adv/replication_specs_unsupported_dynamic.in.tf diff --git a/README.md b/README.md index 8fa3a2c..3eb5565 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ If you want to overwrite the output file if it exists, or even use the same outp - 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` is required in `regions_config` and must be a resolved number between 7 and 1, e.g. `var.prioriy` is not supported. This is to allow reordering them by descending priority as this is expected in `mongodbatlas_advanced_cluster`. +- `dynamic` blocks to generate `replication_specs`, `regions_config`, etc. are not supported. ## Contributing diff --git a/internal/convert/convert.go b/internal/convert/convert.go index 5bfd26e..f1115b5 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -1,6 +1,7 @@ package convert import ( + "errors" "fmt" "sort" @@ -44,6 +45,9 @@ func ClusterToAdvancedCluster(config []byte) ([]byte, error) { continue } resourceb := resource.Body() + if err := checkDynamicBlock(resourceb); err != nil { + return nil, err + } labels[0] = advCluster resource.SetLabels(labels) @@ -102,6 +106,9 @@ func fillReplicationSpecs(resourceb *hclwrite.Body) error { // at least one replication_specs exists here, if not it would be a free tier cluster repSpecsSrc := resourceb.FirstMatchingBlock(nRepSpecs, nil) + if err := checkDynamicBlock(repSpecsSrc.Body()); err != nil { + return err + } configs, errConfigs := getRegionConfigs(repSpecsSrc, root) if errConfigs != nil { return errConfigs @@ -199,6 +206,15 @@ func getAutoScalingOpt(opt map[string]hclwrite.Tokens) hclwrite.Tokens { return hcl.TokensObject(file) } +func checkDynamicBlock(body *hclwrite.Body) error { + for _, block := range body.Blocks() { + if block.Type() == "dynamic" { + return errors.New("dynamic blocks are not supported") + } + } + return nil +} + func setPriority(body *hclwrite.Body, priority *hclwrite.Attribute) error { if priority == nil { return fmt.Errorf("%s: %s not found", errRepSpecs, nPriority) diff --git a/internal/convert/testdata/clu2adv/errors.json b/internal/convert/testdata/clu2adv/errors.json index 8410062..359b7b0 100644 --- a/internal/convert/testdata/clu2adv/errors.json +++ b/internal/convert/testdata/clu2adv/errors.json @@ -6,5 +6,7 @@ "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_unresolved_priority": "setting priority: failed to evaluate number" + "regions_config_unresolved_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" } diff --git a/internal/convert/testdata/clu2adv/regions_config_unsupported_dynamic.in.tf b/internal/convert/testdata/clu2adv/regions_config_unsupported_dynamic.in.tf new file mode 100644 index 0000000..7934936 --- /dev/null +++ b/internal/convert/testdata/clu2adv/regions_config_unsupported_dynamic.in.tf @@ -0,0 +1,38 @@ +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 + } + + } + } +} diff --git a/internal/convert/testdata/clu2adv/replication_specs_unsupported_dynamic.in.tf b/internal/convert/testdata/clu2adv/replication_specs_unsupported_dynamic.in.tf new file mode 100644 index 0000000..f3f5e00 --- /dev/null +++ b/internal/convert/testdata/clu2adv/replication_specs_unsupported_dynamic.in.tf @@ -0,0 +1,29 @@ + +resource "mongodbatlas_cluster" "geo" { + project_id = "66d979971ec97b7de1ef8777" + name = "geo" + cluster_type = "GEOSHARDED" + num_shards = 1 + provider_name = "AWS" + provider_instance_size_name = "M30" + + dynamic "replication_specs" { + for_each = { + "Zone 1" = { + region_name = "US_EAST_1" + }, + "Zone 2" = { + region_name = "US_WEST_2" + } + } + content { + zone_name = replication_specs.key + num_shards = 2 + regions_config { + region_name = replication_specs.value.region_name + electable_nodes = 3 + priority = 7 + } + } + } +} From 47e19813da430679a19feaeba6f0ce418a664783 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Sat, 8 Feb 2025 19:25:47 +0100 Subject: [PATCH 10/17] read only analytics min --- internal/convert/const_names.go | 4 ++ internal/convert/convert.go | 39 ++++++++++++++++--- .../analytics_read_only_min_params.in.tf | 17 ++++++++ .../analytics_read_only_min_params.out.tf | 27 +++++++++++++ .../testdata/clu2adv/autoscaling.out.tf | 10 ++--- 5 files changed, 86 insertions(+), 11 deletions(-) create mode 100644 internal/convert/testdata/clu2adv/analytics_read_only_min_params.in.tf create mode 100644 internal/convert/testdata/clu2adv/analytics_read_only_min_params.out.tf diff --git a/internal/convert/const_names.go b/internal/convert/const_names.go index 71cd66a..d2c5920 100644 --- a/internal/convert/const_names.go +++ b/internal/convert/const_names.go @@ -6,6 +6,8 @@ const ( nConfigSrc = "regions_config" nElectableSpecs = "electable_specs" nAutoScaling = "auto_scaling" + nReadOnlySpecs = "read_only_specs" + nAnalyticsSpecs = "analytics_specs" nRegionNameSrc = "provider_region_name" nRegionName = "region_name" nProviderName = "provider_name" @@ -30,4 +32,6 @@ const ( nComputeMaxInstanceSize = "compute_max_instance_size" nNodeCount = "node_count" nElectableNodes = "electable_nodes" + nReadOnlyNodes = "read_only_nodes" + nAnalyticsNodes = "analytics_nodes" ) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index f1115b5..eaa19f4 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -45,8 +45,8 @@ func ClusterToAdvancedCluster(config []byte) ([]byte, error) { continue } resourceb := resource.Body() - if err := checkDynamicBlock(resourceb); err != nil { - return nil, err + if errDyn := checkDynamicBlock(resourceb); errDyn != nil { + return nil, errDyn } labels[0] = advCluster resource.SetLabels(labels) @@ -156,15 +156,20 @@ func getRegionConfig(configSrc *hclwrite.Block, root attrVals) (*hclwrite.File, if err := setPriority(fileb, configSrc.Body().GetAttribute(nPriority)); err != nil { return nil, err } - autoScaling := getAutoScalingOpt(root.opt) - if autoScaling != nil { - fileb.SetAttributeRaw(nAutoScaling, autoScaling) - } electableSpecs, errElect := getElectableSpecs(configSrc, root) if errElect != nil { return nil, errElect } fileb.SetAttributeRaw(nElectableSpecs, electableSpecs) + if readOnly := getReadOnlyAnalyticsOpt(nReadOnlyNodes, configSrc, root); readOnly != nil { + fileb.SetAttributeRaw(nReadOnlySpecs, readOnly) + } + if analytics := getReadOnlyAnalyticsOpt(nAnalyticsNodes, configSrc, root); analytics != nil { + fileb.SetAttributeRaw(nAnalyticsSpecs, analytics) + } + if autoScaling := getAutoScalingOpt(root.opt); autoScaling != nil { + fileb.SetAttributeRaw(nAutoScaling, autoScaling) + } return file, nil } @@ -181,6 +186,25 @@ func getElectableSpecs(configSrc *hclwrite.Block, root attrVals) (hclwrite.Token return hcl.TokensObject(file), nil } +func getReadOnlyAnalyticsOpt(countName string, configSrc *hclwrite.Block, root attrVals) hclwrite.Tokens { + var ( + file = hclwrite.NewEmptyFile() + fileb = file.Body() + ) + count := configSrc.Body().GetAttribute(countName) + if count == nil { + return nil + } + countVal, errVal := hcl.GetAttrInt(count, errRepSpecs) + // don't include if read_only_nodes or analytics_nodes is 0 + if countVal == 0 && errVal == nil { + return nil + } + fileb.SetAttributeRaw(nNodeCount, count.Expr().BuildTokens(nil)) + fileb.SetAttributeRaw(nInstanceSize, root.req[nInstanceSizeSrc]) + return hcl.TokensObject(file) +} + func getAutoScalingOpt(opt map[string]hclwrite.Tokens) hclwrite.Tokens { var ( names = [][2]string{ // use slice instead of map to preserve order @@ -238,6 +262,9 @@ func popRootAttrs(body *hclwrite.Body) (attrVals, error) { nInstanceSizeSrc, } optNames = []string{ + nElectableNodes, + nReadOnlyNodes, + nAnalyticsNodes, nDiskSizeGB, nDiskGBEnabledSrc, nComputeEnabledSrc, 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 new file mode 100644 index 0000000..8aae1e1 --- /dev/null +++ b/internal/convert/testdata/clu2adv/analytics_read_only_min_params.in.tf @@ -0,0 +1,17 @@ +resource "mongodbatlas_cluster" "ar" { + 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 + electable_nodes = 3 + 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 new file mode 100644 index 0000000..0889648 --- /dev/null +++ b/internal/convert/testdata/clu2adv/analytics_read_only_min_params.out.tf @@ -0,0 +1,27 @@ +resource "mongodbatlas_advanced_cluster" "ar" { + project_id = var.project_id + name = "ar" + cluster_type = "REPLICASET" + replication_specs = [{ + region_configs = [{ + provider_name = "AWS" + region_name = "US_EAST_1" + priority = 7 + electable_specs = { + node_count = 3 + instance_size = "M10" + } + read_only_specs = { + node_count = 1 + instance_size = "M10" + } + analytics_specs = { + node_count = 2 + instance_size = "M10" + } + }] + }] + + # Generated by atlas-cli-plugin-terraform. + # Please confirm that all 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 7fe8cf4..6b0ef6d 100644 --- a/internal/convert/testdata/clu2adv/autoscaling.out.tf +++ b/internal/convert/testdata/clu2adv/autoscaling.out.tf @@ -16,6 +16,11 @@ resource "mongodbatlas_advanced_cluster" "autoscaling" { provider_name = "AWS" region_name = "US_WEST_2" priority = 7 + electable_specs = { + node_count = 3 + instance_size = "M20" + disk_size_gb = 100 + } auto_scaling = { disk_gb_enabled = true compute_enabled = false @@ -23,11 +28,6 @@ resource "mongodbatlas_advanced_cluster" "autoscaling" { compute_max_instance_size = "M40" compute_scale_down_enabled = local.scale_down } - electable_specs = { - node_count = 3 - instance_size = "M20" - disk_size_gb = 100 - } }] }] From 23daf4a35ad2fd78ab2a09ffd55521d560c3c682 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Sat, 8 Feb 2025 19:36:04 +0100 Subject: [PATCH 11/17] read only and analytics all params --- internal/convert/const_names.go | 4 +++ internal/convert/convert.go | 17 +++++++++ .../analytics_read_only_all_params.in.tf | 20 +++++++++++ .../analytics_read_only_all_params.out.tf | 36 +++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 internal/convert/testdata/clu2adv/analytics_read_only_all_params.in.tf create mode 100644 internal/convert/testdata/clu2adv/analytics_read_only_all_params.out.tf diff --git a/internal/convert/const_names.go b/internal/convert/const_names.go index d2c5920..4117049 100644 --- a/internal/convert/const_names.go +++ b/internal/convert/const_names.go @@ -25,11 +25,15 @@ const ( nComputeScaleDownEnabledSrc = "auto_scaling_compute_scale_down_enabled" nComputeMinInstanceSizeSrc = "provider_auto_scaling_compute_min_instance_size" nComputeMaxInstanceSizeSrc = "provider_auto_scaling_compute_max_instance_size" + nEBSVolumeTypeSrc = "provider_volume_type" + nDiskIOPSSrc = "provider_disk_iops" nDiskGBEnabled = "disk_gb_enabled" nComputeEnabled = "compute_enabled" nComputeScaleDownEnabled = "compute_scale_down_enabled" nComputeMinInstanceSize = "compute_min_instance_size" nComputeMaxInstanceSize = "compute_max_instance_size" + nEBSVolumeType = "ebs_volume_type" + nDiskIOPS = "disk_iops" nNodeCount = "node_count" nElectableNodes = "electable_nodes" nReadOnlyNodes = "read_only_nodes" diff --git a/internal/convert/convert.go b/internal/convert/convert.go index eaa19f4..e6130c0 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -183,6 +183,12 @@ func getElectableSpecs(configSrc *hclwrite.Block, root attrVals) (hclwrite.Token if root.opt[nDiskSizeGB] != nil { fileb.SetAttributeRaw(nDiskSizeGB, root.opt[nDiskSizeGB]) } + if root.opt[nEBSVolumeTypeSrc] != nil { + fileb.SetAttributeRaw(nEBSVolumeType, root.opt[nEBSVolumeTypeSrc]) + } + if root.opt[nDiskIOPSSrc] != nil { + fileb.SetAttributeRaw(nDiskIOPS, root.opt[nDiskIOPSSrc]) + } return hcl.TokensObject(file), nil } @@ -202,6 +208,15 @@ func getReadOnlyAnalyticsOpt(countName string, configSrc *hclwrite.Block, root a } fileb.SetAttributeRaw(nNodeCount, count.Expr().BuildTokens(nil)) fileb.SetAttributeRaw(nInstanceSize, root.req[nInstanceSizeSrc]) + if root.opt[nDiskSizeGB] != nil { + fileb.SetAttributeRaw(nDiskSizeGB, root.opt[nDiskSizeGB]) + } + if root.opt[nEBSVolumeTypeSrc] != nil { + fileb.SetAttributeRaw(nEBSVolumeType, root.opt[nEBSVolumeTypeSrc]) + } + if root.opt[nDiskIOPSSrc] != nil { + fileb.SetAttributeRaw(nDiskIOPS, root.opt[nDiskIOPSSrc]) + } return hcl.TokensObject(file) } @@ -271,6 +286,8 @@ func popRootAttrs(body *hclwrite.Body) (attrVals, error) { nComputeMinInstanceSizeSrc, nComputeMaxInstanceSizeSrc, nComputeScaleDownEnabledSrc, + nEBSVolumeTypeSrc, + nDiskIOPSSrc, } req = make(map[string]hclwrite.Tokens) opt = make(map[string]hclwrite.Tokens) 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 new file mode 100644 index 0000000..101c90e --- /dev/null +++ b/internal/convert/testdata/clu2adv/analytics_read_only_all_params.in.tf @@ -0,0 +1,20 @@ +resource "mongodbatlas_cluster" "ar" { + 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 = 3 + 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 new file mode 100644 index 0000000..7b66960 --- /dev/null +++ b/internal/convert/testdata/clu2adv/analytics_read_only_all_params.out.tf @@ -0,0 +1,36 @@ +resource "mongodbatlas_advanced_cluster" "ar" { + project_id = var.project_id + name = "ar" + cluster_type = "REPLICASET" + replication_specs = [{ + region_configs = [{ + provider_name = "AWS" + region_name = "US_EAST_1" + priority = 7 + electable_specs = { + node_count = 3 + instance_size = "M10" + disk_size_gb = 90 + ebs_volume_type = "PROVISIONED" + disk_iops = 100 + } + 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 confirm that all references to this resource are updated. +} From 78f4d6ac95d140c9cdf7c9db52ea9ce78c0d313b Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Sat, 8 Feb 2025 19:40:23 +0100 Subject: [PATCH 12/17] move to var block --- internal/convert/convert.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index e6130c0..8baed45 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -196,8 +196,8 @@ func getReadOnlyAnalyticsOpt(countName string, configSrc *hclwrite.Block, root a var ( file = hclwrite.NewEmptyFile() fileb = file.Body() + count = configSrc.Body().GetAttribute(countName) ) - count := configSrc.Body().GetAttribute(countName) if count == nil { return nil } From 76684e79df792d800cdaaaa04543ecc0b95a4d6a Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Mon, 10 Feb 2025 08:47:05 +0100 Subject: [PATCH 13/17] go 1.23.6 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 60a3b36..9fe1a46 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/mongodb-labs/atlas-cli-plugin-terraform -go 1.23.4 +go 1.23.6 require ( github.com/hashicorp/hcl/v2 v2.23.0 From 1563d36e6da5ac9090756856ccb44e771bd425a1 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Mon, 10 Feb 2025 09:24:31 +0100 Subject: [PATCH 14/17] typo --- README.md | 2 +- internal/convert/convert.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3eb5565..ab00efe 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ If you want to overwrite the output file if it exists, or even use the same outp ### 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` is required in `regions_config` and must be a resolved number between 7 and 1, e.g. `var.prioriy` is not supported. This is to allow reordering them by descending priority as this is expected in `mongodbatlas_advanced_cluster`. +- `priority` is required in `regions_config` and must be a resolved number 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`. - `dynamic` blocks to generate `replication_specs`, `regions_config`, etc. are not supported. diff --git a/internal/convert/convert.go b/internal/convert/convert.go index 8baed45..d651130 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -258,14 +258,14 @@ func setPriority(body *hclwrite.Body, priority *hclwrite.Attribute) error { if priority == nil { return fmt.Errorf("%s: %s not found", errRepSpecs, nPriority) } - valPrioriy, err := hcl.GetAttrInt(priority, errPriority) + valPriority, err := hcl.GetAttrInt(priority, errPriority) if err != nil { return err } - if valPrioriy < valMinPriority || valPrioriy > valMaxPriority { - return fmt.Errorf("%s: %s is %d but must be between %d and %d", errPriority, nPriority, valPrioriy, valMinPriority, valMaxPriority) + 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, valPrioriy) + hcl.SetAttrInt(body, nPriority, valPriority) return nil } From 8434868048a9f8f57b00e544739bc796c3c4108d Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Mon, 10 Feb 2025 09:28:33 +0100 Subject: [PATCH 15/17] countVal refactor --- internal/convert/convert.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index d651130..6c8ec86 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -201,9 +201,8 @@ func getReadOnlyAnalyticsOpt(countName string, configSrc *hclwrite.Block, root a if count == nil { return nil } - countVal, errVal := hcl.GetAttrInt(count, errRepSpecs) // don't include if read_only_nodes or analytics_nodes is 0 - if countVal == 0 && errVal == nil { + if countVal, errVal := hcl.GetAttrInt(count, errRepSpecs); countVal == 0 && errVal == nil { return nil } fileb.SetAttributeRaw(nNodeCount, count.Expr().BuildTokens(nil)) From b21b3b2b9b0189478c0546d752cbea8659fa8f7f Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Mon, 10 Feb 2025 09:43:43 +0100 Subject: [PATCH 16/17] have all getSpecs together --- internal/convert/convert.go | 39 ++++++++------------------------ internal/convert/convert_test.go | 2 +- 2 files changed, 11 insertions(+), 30 deletions(-) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index 6c8ec86..51f71fd 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -156,15 +156,15 @@ func getRegionConfig(configSrc *hclwrite.Block, root attrVals) (*hclwrite.File, if err := setPriority(fileb, configSrc.Body().GetAttribute(nPriority)); err != nil { return nil, err } - electableSpecs, errElect := getElectableSpecs(configSrc, root) - if errElect != nil { - return nil, errElect + electableSpecs, errElec := getSpecs(nElectableNodes, configSrc, root) + if errElec != nil { + return nil, errElec } fileb.SetAttributeRaw(nElectableSpecs, electableSpecs) - if readOnly := getReadOnlyAnalyticsOpt(nReadOnlyNodes, configSrc, root); readOnly != nil { + if readOnly, _ := getSpecs(nReadOnlyNodes, configSrc, root); readOnly != nil { fileb.SetAttributeRaw(nReadOnlySpecs, readOnly) } - if analytics := getReadOnlyAnalyticsOpt(nAnalyticsNodes, configSrc, root); analytics != nil { + if analytics, _ := getSpecs(nAnalyticsNodes, configSrc, root); analytics != nil { fileb.SetAttributeRaw(nAnalyticsSpecs, analytics) } if autoScaling := getAutoScalingOpt(root.opt); autoScaling != nil { @@ -173,37 +173,18 @@ func getRegionConfig(configSrc *hclwrite.Block, root attrVals) (*hclwrite.File, return file, nil } -func getElectableSpecs(configSrc *hclwrite.Block, root attrVals) (hclwrite.Tokens, error) { - file := hclwrite.NewEmptyFile() - fileb := file.Body() - if err := hcl.MoveAttr(configSrc.Body(), fileb, nElectableNodes, nNodeCount, errRepSpecs); err != nil { - return nil, err - } - fileb.SetAttributeRaw(nInstanceSize, root.req[nInstanceSizeSrc]) - if root.opt[nDiskSizeGB] != nil { - fileb.SetAttributeRaw(nDiskSizeGB, root.opt[nDiskSizeGB]) - } - if root.opt[nEBSVolumeTypeSrc] != nil { - fileb.SetAttributeRaw(nEBSVolumeType, root.opt[nEBSVolumeTypeSrc]) - } - if root.opt[nDiskIOPSSrc] != nil { - fileb.SetAttributeRaw(nDiskIOPS, root.opt[nDiskIOPSSrc]) - } - return hcl.TokensObject(file), nil -} - -func getReadOnlyAnalyticsOpt(countName string, configSrc *hclwrite.Block, root attrVals) hclwrite.Tokens { +func getSpecs(countName string, configSrc *hclwrite.Block, root attrVals) (hclwrite.Tokens, error) { var ( file = hclwrite.NewEmptyFile() fileb = file.Body() count = configSrc.Body().GetAttribute(countName) ) if count == nil { - return nil + return nil, fmt.Errorf("%s: attribute %s not found", errRepSpecs, countName) + } - // don't include if read_only_nodes or analytics_nodes is 0 if countVal, errVal := hcl.GetAttrInt(count, errRepSpecs); countVal == 0 && errVal == nil { - return nil + return nil, fmt.Errorf("%s: attribute %s is 0", errRepSpecs, countName) } fileb.SetAttributeRaw(nNodeCount, count.Expr().BuildTokens(nil)) fileb.SetAttributeRaw(nInstanceSize, root.req[nInstanceSizeSrc]) @@ -216,7 +197,7 @@ func getReadOnlyAnalyticsOpt(countName string, configSrc *hclwrite.Block, root a if root.opt[nDiskIOPSSrc] != nil { fileb.SetAttributeRaw(nDiskIOPS, root.opt[nDiskIOPSSrc]) } - return hcl.TokensObject(file) + return hcl.TokensObject(file), nil } func getAutoScalingOpt(opt map[string]hclwrite.Tokens) hclwrite.Tokens { diff --git a/internal/convert/convert_test.go b/internal/convert/convert_test.go index d7efed5..65056fc 100644 --- a/internal/convert/convert_test.go +++ b/internal/convert/convert_test.go @@ -43,7 +43,7 @@ func TestClusterToAdvancedCluster(t *testing.T) { g.Assert(t, testName, outConfig) } else { errMsg, found := errMap[testName] - assert.True(t, found, "error not found in file %s for test %s", errFilename, testName) + assert.True(t, found, "error not found in file %s for test %s, errMsg: %v", errFilename, testName, err) assert.Contains(t, err.Error(), errMsg) } }) From 70597f55925d4dc505b44d35c094792d7983e367 Mon Sep 17 00:00:00 2001 From: Leo Antoli <430982+lantoli@users.noreply.github.com> Date: Mon, 10 Feb 2025 09:46:08 +0100 Subject: [PATCH 17/17] linter --- internal/convert/convert.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/convert/convert.go b/internal/convert/convert.go index 51f71fd..4987b53 100644 --- a/internal/convert/convert.go +++ b/internal/convert/convert.go @@ -181,7 +181,6 @@ func getSpecs(countName string, configSrc *hclwrite.Block, root attrVals) (hclwr ) if count == nil { return nil, fmt.Errorf("%s: attribute %s not found", errRepSpecs, countName) - } if countVal, errVal := hcl.GetAttrInt(count, errRepSpecs); countVal == 0 && errVal == nil { return nil, fmt.Errorf("%s: attribute %s is 0", errRepSpecs, countName)