Skip to content

Commit 7b00e4e

Browse files
lantolimarcosuma
andauthored
feat: Supports dynamic blocks in replication_specs (#37)
* change condition to make it clearer * failing test * all dynamic blocks in replication_specs * initial fillReplicationSpecsWithDynamicBlock * pass tests * rename test * test with different var names * doc * typo * reference to guide * Update README.md Co-authored-by: Marco Suma <[email protected]> * remove error-prone sentence * add guide * doc adjustment * doc small change --------- Co-authored-by: Marco Suma <[email protected]>
1 parent 1bb46dd commit 7b00e4e

13 files changed

+488
-81
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
ENHANCEMENTS:
44

5-
* Supports `dynamic` block for `tags`, `labels` and `regions_config`
5+
* Supports `dynamic` blocks for `tags`, `labels`, `regions_config` and `replication_specs`
66

77
## 1.0.0 (Mar 6, 2025)
88

README.md

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ atlas plugin list
3030

3131
### Usage
3232

33+
You can find more information in the [Migration Guide: Cluster to Advanced Cluster](https://registry.terraform.io/providers/mongodb/mongodbatlas/latest/docs/guides/cluster-to-advanced-cluster-migration-guide).
34+
3335
**Note**: In order to use the **Preview for MongoDB Atlas Provider 2.0.0** of `mongodbatlas_advanced_cluster`, you need to set the environment variable `MONGODB_ATLAS_PREVIEW_PROVIDER_V2_ADVANCED_CLUSTER` to `true`.
3436

3537
If you want to convert a Terraform configuration from `mongodbatlas_cluster` to `mongodbatlas_advanced_cluster`, use the following command:
@@ -75,31 +77,51 @@ dynamic "tags" {
7577

7678
#### Dynamic blocks in regions_config
7779

78-
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.
80+
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. See this [guide](./docs/guide_clu2adv_dynamic_block.md) to learn more about some limitations.
7981
This is an example of how to use dynamic blocks in `regions_config`:
8082
```hcl
81-
replication_specs {
82-
num_shards = var.replication_specs.num_shards
83-
zone_name = var.replication_specs.zone_name # only needed if you're using zones
83+
replication_specs {
84+
num_shards = var.replication_specs.num_shards
85+
zone_name = var.replication_specs.zone_name # only needed if you're using zones
86+
dynamic "regions_config" {
87+
for_each = var.replication_specs.regions_config
88+
content {
89+
priority = regions_config.value.priority
90+
region_name = regions_config.value.region_name
91+
electable_nodes = regions_config.value.electable_nodes
92+
read_only_nodes = regions_config.value.read_only_nodes
93+
}
94+
}
95+
}
96+
```
97+
98+
#### Dynamic blocks in replication_specs
99+
100+
You can use `dynamic` blocks for `replication_specs`. The plugin assumes that `for_each` has an expression which is evaluated to a `list` of objects. See this [guide](./docs/guide_clu2adv_dynamic_block.md) to learn more about some limitations.
101+
This is an example of how to use dynamic blocks in `replication_specs`:
102+
```hcl
103+
dynamic "replication_specs" {
104+
for_each = var.replication_specs
105+
content {
106+
num_shards = replication_specs.value.num_shards
107+
zone_name = replication_specs.value.zone_name # only needed if you're using zones
84108
dynamic "regions_config" {
85-
for_each = var.replication_specs.regions_config
109+
for_each = replication_specs.value.regions_config
86110
content {
87-
priority = regions_config.value.priority
88-
region_name = regions_config.value.region_name
89111
electable_nodes = regions_config.value.electable_nodes
112+
priority = regions_config.value.priority
90113
read_only_nodes = regions_config.value.read_only_nodes
114+
region_name = regions_config.value.region_name
91115
}
92116
}
93117
}
118+
}
94119
```
95-
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:
96-
- (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.
97-
- 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.
98120

99121
### Limitations
100122

101123
- [`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`.
102-
- `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`.
124+
- `dynamic` blocks are supported with some [limitations](./docs/guide_clu2adv_dynamic_block.md).
103125

104126
## Feedback
105127

docs/guide_clu2adv_dynamic_block.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Guide to handle dynamic block limitations in regions_config and replication_specs
2+
3+
The plugin command to convert `mongodbatlas_cluster` resources to `mongodbatlas_advanced_cluster` supports `dynamic` blocks for `regions_config` and `replication_specs`. However, there are some limitations when using `dynamic` blocks in these fields. This guide explains how to handle these limitations.
4+
5+
If you need to use the plugin for use cases not yet supported, please send us [feedback](https://github.com/mongodb-labs/atlas-cli-plugin-terraform/issues).
6+
7+
## Dynamic block and individual blocks in the same resource
8+
9+
Dynamic block and individual blocks for `regions_config` or `replication_specs` are not supported at the same time. The recommended way to handle this is to remove the individual `regions_config` or `replication_specs` blocks and use a local variable to add the individual block information to the variable you're using in the `for_each` expression, 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.
10+
11+
Let's see an example with `regions_config`, it is the same for `replication_specs`. In the original configuration file, the `mongodb_cluster` resource is used inside a module that receives the `regions_config` elements in a `list` variable and we want to add an additional `region_config` with a read-only node.
12+
```hcl
13+
variable "replication_specs" {
14+
type = object({
15+
num_shards = number
16+
regions_config = list(object({
17+
region_name = string
18+
electable_nodes = number
19+
priority = number
20+
read_only_nodes = number
21+
}))
22+
})
23+
}
24+
25+
resource "mongodbatlas_cluster" "this" {
26+
project_id = var.project_id
27+
name = var.cluster_name
28+
cluster_type = var.cluster_type
29+
provider_name = var.provider_name
30+
provider_instance_size_name = var.provider_instance_size_name
31+
replication_specs {
32+
num_shards = var.replication_specs.num_shards
33+
dynamic "regions_config" {
34+
for_each = var.replication_specs.regions_config
35+
content {
36+
region_name = regions_config.value.region_name
37+
electable_nodes = regions_config.value.electable_nodes
38+
priority = regions_config.value.priority
39+
read_only_nodes = regions_config.value.read_only_nodes
40+
}
41+
}
42+
regions_config { # individual region
43+
region_name = "US_EAST_1"
44+
read_only_nodes = 1
45+
}
46+
}
47+
}
48+
```
49+
50+
We modify the configuration file to create an intermediate `local` variable to merge the `regions_config` variable elements and the additional `region_config`:
51+
```hcl
52+
variable "replication_specs" {
53+
type = object({
54+
num_shards = number
55+
regions_config = list(object({
56+
region_name = string
57+
electable_nodes = number
58+
priority = number
59+
read_only_nodes = number
60+
}))
61+
})
62+
}
63+
64+
locals {
65+
regions_config_all = concat(
66+
var.replication_specs.regions_config,
67+
[
68+
{
69+
region_name = "US_EAST_1"
70+
electable_nodes = 0
71+
priority = 0
72+
read_only_nodes = 1
73+
},
74+
]
75+
)
76+
}
77+
78+
resource "mongodbatlas_cluster" "this" {
79+
project_id = var.project_id
80+
name = var.cluster_name
81+
cluster_type = var.cluster_type
82+
provider_name = var.provider_name
83+
provider_instance_size_name = var.provider_instance_size_name
84+
replication_specs {
85+
num_shards = var.replication_specs.num_shards
86+
dynamic "regions_config" {
87+
for_each = local.regions_config_all # changed to use the local variable
88+
content {
89+
region_name = regions_config.value.region_name
90+
electable_nodes = regions_config.value.electable_nodes
91+
priority = regions_config.value.priority
92+
read_only_nodes = regions_config.value.read_only_nodes
93+
}
94+
}
95+
}
96+
}
97+
```
98+
This modified configuration file has the same behavior as the original one, but it doesn't have individual blocks anymore, only the `dynamic` block, so it is supported by the plugin.

internal/convert/const_names.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,5 @@ const (
5555
nForEach = "for_each"
5656
nContent = "content"
5757
nRegion = "region"
58+
nSpec = "spec"
5859
)

internal/convert/convert.go

Lines changed: 58 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const (
3737
)
3838

3939
var (
40-
dynamicBlockAllowList = []string{nTags, nLabels, nConfigSrc}
40+
dynamicBlockAllowList = []string{nTags, nLabels, nConfigSrc, nRepSpecs}
4141
)
4242

4343
type attrVals struct {
@@ -91,17 +91,22 @@ func convertResource(block *hclwrite.Block) (bool, error) {
9191
}
9292

9393
var err error
94-
if blockb.FirstMatchingBlock(nRepSpecs, nil) != nil {
95-
err = fillCluster(blockb)
96-
} else {
94+
if isFreeTierCluster(blockb) {
9795
err = fillFreeTierCluster(blockb)
96+
} else {
97+
err = fillCluster(blockb)
9898
}
9999
if err != nil {
100100
return false, err
101101
}
102102
return true, nil
103103
}
104104

105+
func isFreeTierCluster(resourceb *hclwrite.Body) bool {
106+
d, _ := getDynamicBlock(resourceb, nRepSpecs)
107+
return resourceb.FirstMatchingBlock(nRepSpecs, nil) == nil && !d.IsPresent()
108+
}
109+
105110
func convertDataSource(block *hclwrite.Block) bool {
106111
if block.Type() != dataSourceType {
107112
return false
@@ -190,6 +195,15 @@ func fillCluster(resourceb *hclwrite.Body) error {
190195
}
191196

192197
func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error {
198+
d, err := fillReplicationSpecsWithDynamicBlock(resourceb, root)
199+
if err != nil {
200+
return err
201+
}
202+
if d.IsPresent() {
203+
resourceb.RemoveBlock(d.block)
204+
resourceb.SetAttributeRaw(nRepSpecs, d.tokens)
205+
return nil
206+
}
193207
// at least one replication_specs exists here, if not it would be a free tier cluster
194208
var specbs []*hclwrite.Body
195209
for {
@@ -202,7 +216,7 @@ func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error {
202216
break
203217
}
204218
specbSrc := specSrc.Body()
205-
d, err := fillReplicationSpecsWithDynamicRegionConfigs(specbSrc, root)
219+
d, err := fillReplicationSpecsWithDynamicRegionConfigs(specbSrc, root, false)
206220
if err != nil {
207221
return err
208222
}
@@ -312,8 +326,26 @@ func fillBlockOpt(resourceb *hclwrite.Body, name string) {
312326
resourceb.SetAttributeRaw(name, hcl.TokensObject(block.Body()))
313327
}
314328

329+
// fillReplicationSpecsWithDynamicBlock used for dynamic blocks in replication_specs
330+
func fillReplicationSpecsWithDynamicBlock(resourceb *hclwrite.Body, root attrVals) (dynamicBlock, error) {
331+
dSpec, err := getDynamicBlock(resourceb, nRepSpecs)
332+
if err != nil || !dSpec.IsPresent() {
333+
return dynamicBlock{}, err
334+
}
335+
transformDynamicBlockReferences(dSpec.content.Body(), nRepSpecs, nSpec)
336+
dConfig, err := fillReplicationSpecsWithDynamicRegionConfigs(dSpec.content.Body(), root, true)
337+
if err != nil {
338+
return dynamicBlock{}, err
339+
}
340+
forSpec := hcl.TokensFromExpr(fmt.Sprintf("for %s in %s : ", nSpec, hcl.GetAttrExpr(dSpec.forEach)))
341+
forSpec = append(forSpec, dConfig.tokens...)
342+
tokens := hcl.TokensFuncFlatten(forSpec)
343+
dSpec.tokens = tokens
344+
return dSpec, nil
345+
}
346+
315347
// fillReplicationSpecsWithDynamicRegionConfigs is used for dynamic blocks in region_configs
316-
func fillReplicationSpecsWithDynamicRegionConfigs(specbSrc *hclwrite.Body, root attrVals) (dynamicBlock, error) {
348+
func fillReplicationSpecsWithDynamicRegionConfigs(specbSrc *hclwrite.Body, root attrVals, transformRegionReferences bool) (dynamicBlock, error) {
317349
d, err := getDynamicBlock(specbSrc, nConfigSrc)
318350
if err != nil || !d.IsPresent() {
319351
return dynamicBlock{}, err
@@ -323,7 +355,11 @@ func fillReplicationSpecsWithDynamicRegionConfigs(specbSrc *hclwrite.Body, root
323355
if zoneName := hcl.GetAttrExpr(specbSrc.GetAttribute(nZoneName)); zoneName != "" {
324356
repSpecb.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(zoneName))
325357
}
326-
regionFor, err := getDynamicBlockRegionConfigsRegionArray(d, root)
358+
forEach := hcl.GetAttrExpr(d.forEach)
359+
if transformRegionReferences {
360+
forEach = replaceDynamicBlockReferences(forEach, nRepSpecs, nSpec)
361+
}
362+
regionFor, err := getDynamicBlockRegionConfigsRegionArray(forEach, d.content, root)
327363
if err != nil {
328364
return dynamicBlock{}, err
329365
}
@@ -414,7 +450,7 @@ func getSpecs(configSrc *hclwrite.Block, countName string, root attrVals, isDyna
414450
}
415451
tokens := hcl.TokensObject(fileb)
416452
if isDynamicBlock {
417-
tokens = encloseDynamicBlockRegionSpec(tokens, countName)
453+
tokens = append(hcl.TokensFromExpr(fmt.Sprintf("%s == 0 ? null :", hcl.GetAttrExpr(count))), tokens...)
418454
}
419455
return tokens, nil
420456
}
@@ -520,41 +556,38 @@ func replaceDynamicBlockExpr(attr *hclwrite.Attribute, blockName, attrName strin
520556
return strings.ReplaceAll(expr, fmt.Sprintf("%s.%s", blockName, attrName), attrName)
521557
}
522558

523-
func encloseDynamicBlockRegionSpec(specTokens hclwrite.Tokens, countName string) hclwrite.Tokens {
524-
tokens := hcl.TokensFromExpr(fmt.Sprintf("%s.%s > 0 ?", nRegion, countName))
525-
tokens = append(tokens, specTokens...)
526-
return append(tokens, hcl.TokensFromExpr(": null")...)
527-
}
528-
529559
// getDynamicBlockRegionConfigsRegionArray returns the region array for a dynamic block in replication_specs.
530560
// e.g. [ for region in var.replication_specs.regions_config : { ... } if priority == region.priority ]
531-
func getDynamicBlockRegionConfigsRegionArray(d dynamicBlock, root attrVals) (hclwrite.Tokens, error) {
532-
transformDynamicBlockReferences(d.content.Body())
533-
priorityStr := hcl.GetAttrExpr(d.content.Body().GetAttribute(nPriority))
561+
func getDynamicBlockRegionConfigsRegionArray(forEach string, configSrc *hclwrite.Block, root attrVals) (hclwrite.Tokens, error) {
562+
transformDynamicBlockReferences(configSrc.Body(), nConfigSrc, nRegion)
563+
priorityStr := hcl.GetAttrExpr(configSrc.Body().GetAttribute(nPriority))
534564
if priorityStr == "" {
535565
return nil, fmt.Errorf("%s: %s not found", errRepSpecs, nPriority)
536566
}
537-
region, err := getRegionConfig(d.content, root, true)
567+
region, err := getRegionConfig(configSrc, root, true)
538568
if err != nil {
539569
return nil, err
540570
}
541-
tokens := hcl.TokensFromExpr(fmt.Sprintf("for %s in %s :", nRegion, hcl.GetAttrExpr(d.forEach)))
571+
tokens := hcl.TokensFromExpr(fmt.Sprintf("for %s in %s :", nRegion, forEach))
542572
tokens = append(tokens, hcl.EncloseBraces(region.BuildTokens(nil), true)...)
543573
tokens = append(tokens, hcl.TokensFromExpr(fmt.Sprintf("if %s == %s", nPriority, priorityStr))...)
544574
return hcl.EncloseBracketsNewLines(tokens), nil
545575
}
546576

547-
// transformDynamicBlockReferences changes value references in all attributes, e.g. regions_config.value.electable_nodes to region.electable_nodes
548-
func transformDynamicBlockReferences(configSrcb *hclwrite.Body) {
577+
func transformDynamicBlockReferences(configSrcb *hclwrite.Body, blockName, varName string) {
549578
for name, attr := range configSrcb.Attributes() {
550-
expr := hcl.GetAttrExpr(attr)
551-
expr = strings.ReplaceAll(expr,
552-
fmt.Sprintf("%s.%s.", nConfigSrc, nValue),
553-
fmt.Sprintf("%s.", nRegion))
579+
expr := replaceDynamicBlockReferences(hcl.GetAttrExpr(attr), blockName, varName)
554580
configSrcb.SetAttributeRaw(name, hcl.TokensFromExpr(expr))
555581
}
556582
}
557583

584+
// replaceDynamicBlockReferences changes value references, e.g. regions_config.value.electable_nodes to region.electable_nodes
585+
func replaceDynamicBlockReferences(expr, blockName, varName string) string {
586+
return strings.ReplaceAll(expr,
587+
fmt.Sprintf("%s.%s.", blockName, nValue),
588+
fmt.Sprintf("%s.", varName))
589+
}
590+
558591
func sortConfigsByPriority(configs []*hclwrite.Body) []*hclwrite.Body {
559592
for _, config := range configs {
560593
if _, err := hcl.GetAttrInt(config.GetAttribute(nPriority), errPriority); err != nil {

internal/convert/testdata/clu2adv/dynamic_regions_config_auto_scaling.out.tf

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,27 +23,27 @@ resource "mongodbatlas_advanced_cluster" "cluster" {
2323
provider_name = var.provider_name
2424
region_name = region.region_name
2525
priority = region.priority
26-
electable_specs = region.electable_nodes > 0 ? {
26+
electable_specs = region.electable_nodes == 0 ? null : {
2727
node_count = region.electable_nodes
2828
instance_size = var.provider_instance_size_name
2929
disk_size_gb = var.disk_size_gb
3030
ebs_volume_type = var.provider_volume_type
3131
disk_iops = var.provider_disk_iops
32-
} : null
33-
read_only_specs = region.read_only_nodes > 0 ? {
32+
}
33+
read_only_specs = region.read_only_nodes == 0 ? null : {
3434
node_count = region.read_only_nodes
3535
instance_size = var.provider_instance_size_name
3636
disk_size_gb = var.disk_size_gb
3737
ebs_volume_type = var.provider_volume_type
3838
disk_iops = var.provider_disk_iops
39-
} : null
40-
analytics_specs = region.analytics_nodes > 0 ? {
39+
}
40+
analytics_specs = region.analytics_nodes == 0 ? null : {
4141
node_count = region.analytics_nodes
4242
instance_size = var.provider_instance_size_name
4343
disk_size_gb = var.disk_size_gb
4444
ebs_volume_type = var.provider_volume_type
4545
disk_iops = var.provider_disk_iops
46-
} : null
46+
}
4747
auto_scaling = {
4848
disk_gb_enabled = var.auto_scaling_disk_gb_enabled
4949
}

0 commit comments

Comments
 (0)