Skip to content

Commit 1bb46dd

Browse files
lantolimarcosuma
andauthored
feat: Supports dynamic blocks in regions_config (#33)
* doc assumption for dynamic blocks in tags and labels * dynamic_regions_config example * range for priorities * allow dynamic block in regions_config * doc * update comment * minimum implementation to have test failing because difference in golden file * export enclose funcs * create EncloseNewLines and remove SetAttrExpr * root replication_specs * remove priority checks about numerical literal * reuse getRegionConfig from dynamic block logic * only sort by priority if all priorities are numerical literals * remove limitations for priority and electable_nodes * use config in dynamic blocks from individual * passing test * add auto_scaling example * fix region_configs name replacement * refactor isDynamicBlock * go back to unexported tokenNewLine * add analytics specs * example in readme * clarify num_shards limitation * feedback section * getDynamicBlockRegionConfigsRegionArray * refactor fillRegionConfigsDynamicBlock * EncloseBracketsNewLines * fillRegionConfigsDynamicBlock doc * move shards closer to where it's used * add comment for priority loop * add dynamic block doc * small doc adjustment * rename to fillReplicationSpecsWithDynamicRegionConfigs * Update README.md Co-authored-by: Marco Suma <[email protected]> * link to limitations * how to handle limitation --------- Co-authored-by: Marco Suma <[email protected]>
1 parent e9c2134 commit 1bb46dd

30 files changed

+585
-206
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` and `labels`
5+
* Supports `dynamic` block for `tags`, `labels` and `regions_config`
66

77
## 1.0.0 (Mar 6, 2025)
88

README.md

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ Given the different ways of using dynamic blocks, we recommend reviewing the out
5757

5858
#### Dynamic blocks in tags and labels
5959

60-
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.:
60+
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.
61+
You can also combine the use of dynamic blocks in `tags` and `labels` with individual blocks in the same cluster definition, e.g.:
6162
```hcl
6263
tags {
6364
key = "environment"
@@ -72,12 +73,37 @@ dynamic "tags" {
7273
}
7374
```
7475

76+
#### Dynamic blocks in regions_config
77+
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.
79+
This is an example of how to use dynamic blocks in `regions_config`:
80+
```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
84+
dynamic "regions_config" {
85+
for_each = var.replication_specs.regions_config
86+
content {
87+
priority = regions_config.value.priority
88+
region_name = regions_config.value.region_name
89+
electable_nodes = regions_config.value.electable_nodes
90+
read_only_nodes = regions_config.value.read_only_nodes
91+
}
92+
}
93+
}
94+
```
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.
98+
7599
### Limitations
76100

77-
- 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`.
78-
- [`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`.
79-
- [`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`.
80-
- `dynamic` blocks are currently supported only for `tags` and `labels`. **Coming soon**: support for `replication_specs` and `regions_config`.
101+
- [`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`.
103+
104+
## Feedback
105+
106+
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.
81107

82108
## Contributing
83109

internal/convert/const_names.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,5 @@ const (
5454
nDynamic = "dynamic"
5555
nForEach = "for_each"
5656
nContent = "content"
57+
nRegion = "region"
5758
)

internal/convert/convert.go

Lines changed: 123 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,22 @@ const (
2222
advClusterPlural = "mongodbatlas_advanced_clusters"
2323
valClusterType = "REPLICASET"
2424
valMaxPriority = 7
25-
valMinPriority = 1
26-
27-
errFreeCluster = "free cluster (because no " + nRepSpecs + ")"
28-
errRepSpecs = "setting " + nRepSpecs
29-
errConfigs = "setting " + nConfig
30-
errPriority = "setting " + nPriority
31-
errNumShards = "setting " + nNumShards
25+
valMinPriority = 0
26+
errFreeCluster = "free cluster (because no " + nRepSpecs + ")"
27+
errRepSpecs = "setting " + nRepSpecs
28+
errConfigs = "setting " + nConfig
29+
errPriority = "setting " + nPriority
30+
errNumShards = "setting " + nNumShards
3231

3332
commentGeneratedBy = "Generated by atlas-cli-plugin-terraform."
34-
commentConfirmReferences = "Please confirm that all references to this resource are updated."
33+
commentConfirmReferences = "Please review the changes and confirm that references to this resource are updated."
3534
commentMovedBlock = "Moved blocks"
3635
commentRemovedOld = "Note: Remember to remove or comment out the old cluster definitions."
36+
commentPriorityFor = "Regions must be sorted by priority in descending order."
3737
)
3838

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

4343
type attrVals struct {
@@ -129,8 +129,8 @@ func fillMovedBlocks(body *hclwrite.Body, moveLabels []string) {
129129
for i, moveLabel := range moveLabels {
130130
block := body.AppendNewBlock(nMoved, nil)
131131
blockb := block.Body()
132-
hcl.SetAttrExpr(blockb, nFrom, fmt.Sprintf("%s.%s", cluster, moveLabel))
133-
hcl.SetAttrExpr(blockb, nTo, fmt.Sprintf("%s.%s", advCluster, moveLabel))
132+
blockb.SetAttributeRaw(nFrom, hcl.TokensFromExpr(fmt.Sprintf("%s.%s", cluster, moveLabel)))
133+
blockb.SetAttributeRaw(nTo, hcl.TokensFromExpr(fmt.Sprintf("%s.%s", advCluster, moveLabel)))
134134
if i < len(moveLabels)-1 {
135135
body.AppendNewline()
136136
}
@@ -202,9 +202,15 @@ func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error {
202202
break
203203
}
204204
specbSrc := specSrc.Body()
205-
if err := checkDynamicBlock(specbSrc); err != nil {
205+
d, err := fillReplicationSpecsWithDynamicRegionConfigs(specbSrc, root)
206+
if err != nil {
206207
return err
207208
}
209+
if d.IsPresent() {
210+
resourceb.RemoveBlock(specSrc)
211+
resourceb.SetAttributeRaw(nRepSpecs, d.tokens)
212+
return nil
213+
}
208214
// ok to fail as zone_name is optional
209215
_ = hcl.MoveAttr(specbSrc, specb, nZoneName, nZoneName, errRepSpecs)
210216
shards := specbSrc.GetAttribute(nNumShards)
@@ -251,7 +257,7 @@ func fillTagsLabelsOpt(resourceb *hclwrite.Body, name string) error {
251257

252258
func extractTagsLabelsDynamicBlock(resourceb *hclwrite.Body, name string) (hclwrite.Tokens, error) {
253259
d, err := getDynamicBlock(resourceb, name)
254-
if err != nil || d.forEach == nil {
260+
if err != nil || !d.IsPresent() {
255261
return nil, err
256262
}
257263
key := d.content.Body().GetAttribute(nKey)
@@ -306,14 +312,44 @@ func fillBlockOpt(resourceb *hclwrite.Body, name string) {
306312
resourceb.SetAttributeRaw(name, hcl.TokensObject(block.Body()))
307313
}
308314

315+
// fillReplicationSpecsWithDynamicRegionConfigs is used for dynamic blocks in region_configs
316+
func fillReplicationSpecsWithDynamicRegionConfigs(specbSrc *hclwrite.Body, root attrVals) (dynamicBlock, error) {
317+
d, err := getDynamicBlock(specbSrc, nConfigSrc)
318+
if err != nil || !d.IsPresent() {
319+
return dynamicBlock{}, err
320+
}
321+
repSpec := hclwrite.NewEmptyFile()
322+
repSpecb := repSpec.Body()
323+
if zoneName := hcl.GetAttrExpr(specbSrc.GetAttribute(nZoneName)); zoneName != "" {
324+
repSpecb.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(zoneName))
325+
}
326+
regionFor, err := getDynamicBlockRegionConfigsRegionArray(d, root)
327+
if err != nil {
328+
return dynamicBlock{}, err
329+
}
330+
priorityFor := hcl.TokensComment(commentPriorityFor)
331+
priorityFor = append(priorityFor, hcl.TokensFromExpr(fmt.Sprintf("for %s in range(%d, %d, -1) : ", nPriority, valMaxPriority, valMinPriority))...)
332+
priorityFor = append(priorityFor, regionFor...)
333+
repSpecb.SetAttributeRaw(nConfig, hcl.TokensFuncFlatten(priorityFor))
334+
335+
shards := specbSrc.GetAttribute(nNumShards)
336+
if shards == nil {
337+
return dynamicBlock{}, fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards)
338+
}
339+
tokens := hcl.TokensFromExpr(fmt.Sprintf("for i in range(%s) :", hcl.GetAttrExpr(shards)))
340+
tokens = append(tokens, hcl.EncloseBraces(repSpec.BuildTokens(nil), true)...)
341+
d.tokens = hcl.EncloseBracketsNewLines(tokens)
342+
return d, nil
343+
}
344+
309345
func fillRegionConfigs(specb, specbSrc *hclwrite.Body, root attrVals) error {
310346
var configs []*hclwrite.Body
311347
for {
312348
configSrc := specbSrc.FirstMatchingBlock(nConfigSrc, nil)
313349
if configSrc == nil {
314350
break
315351
}
316-
config, err := getRegionConfig(configSrc, root)
352+
config, err := getRegionConfig(configSrc, root, false)
317353
if err != nil {
318354
return err
319355
}
@@ -323,34 +359,28 @@ func fillRegionConfigs(specb, specbSrc *hclwrite.Body, root attrVals) error {
323359
if len(configs) == 0 {
324360
return fmt.Errorf("%s: %s not found", errRepSpecs, nConfigSrc)
325361
}
326-
sort.Slice(configs, func(i, j int) bool {
327-
pi, _ := hcl.GetAttrInt(configs[i].GetAttribute(nPriority), errPriority)
328-
pj, _ := hcl.GetAttrInt(configs[j].GetAttribute(nPriority), errPriority)
329-
return pi > pj
330-
})
362+
configs = sortConfigsByPriority(configs)
331363
specb.SetAttributeRaw(nConfig, hcl.TokensArray(configs))
332364
return nil
333365
}
334366

335-
func getRegionConfig(configSrc *hclwrite.Block, root attrVals) (*hclwrite.File, error) {
367+
func getRegionConfig(configSrc *hclwrite.Block, root attrVals, isDynamicBlock bool) (*hclwrite.File, error) {
336368
file := hclwrite.NewEmptyFile()
337369
fileb := file.Body()
338370
fileb.SetAttributeRaw(nProviderName, root.req[nProviderName])
339371
if err := hcl.MoveAttr(configSrc.Body(), fileb, nRegionName, nRegionName, errRepSpecs); err != nil {
340372
return nil, err
341373
}
342-
if err := setPriority(fileb, configSrc.Body().GetAttribute(nPriority)); err != nil {
374+
if err := hcl.MoveAttr(configSrc.Body(), fileb, nPriority, nPriority, errRepSpecs); err != nil {
343375
return nil, err
344376
}
345-
electableSpecs, errElec := getSpecs(configSrc, nElectableNodes, root)
346-
if errElec != nil {
347-
return nil, errElec
377+
if electable, _ := getSpecs(configSrc, nElectableNodes, root, isDynamicBlock); electable != nil {
378+
fileb.SetAttributeRaw(nElectableSpecs, electable)
348379
}
349-
fileb.SetAttributeRaw(nElectableSpecs, electableSpecs)
350-
if readOnly, _ := getSpecs(configSrc, nReadOnlyNodes, root); readOnly != nil {
380+
if readOnly, _ := getSpecs(configSrc, nReadOnlyNodes, root, isDynamicBlock); readOnly != nil {
351381
fileb.SetAttributeRaw(nReadOnlySpecs, readOnly)
352382
}
353-
if analytics, _ := getSpecs(configSrc, nAnalyticsNodes, root); analytics != nil {
383+
if analytics, _ := getSpecs(configSrc, nAnalyticsNodes, root, isDynamicBlock); analytics != nil {
354384
fileb.SetAttributeRaw(nAnalyticsSpecs, analytics)
355385
}
356386
if autoScaling := getAutoScalingOpt(root.opt); autoScaling != nil {
@@ -359,7 +389,7 @@ func getRegionConfig(configSrc *hclwrite.Block, root attrVals) (*hclwrite.File,
359389
return file, nil
360390
}
361391

362-
func getSpecs(configSrc *hclwrite.Block, countName string, root attrVals) (hclwrite.Tokens, error) {
392+
func getSpecs(configSrc *hclwrite.Block, countName string, root attrVals, isDynamicBlock bool) (hclwrite.Tokens, error) {
363393
var (
364394
file = hclwrite.NewEmptyFile()
365395
fileb = file.Body()
@@ -382,7 +412,11 @@ func getSpecs(configSrc *hclwrite.Block, countName string, root attrVals) (hclwr
382412
if root.opt[nDiskIOPSSrc] != nil {
383413
fileb.SetAttributeRaw(nDiskIOPS, root.opt[nDiskIOPSSrc])
384414
}
385-
return hcl.TokensObject(fileb), nil
415+
tokens := hcl.TokensObject(fileb)
416+
if isDynamicBlock {
417+
tokens = encloseDynamicBlockRegionSpec(tokens, countName)
418+
}
419+
return tokens, nil
386420
}
387421

388422
func getAutoScalingOpt(opt map[string]hclwrite.Tokens) hclwrite.Tokens {
@@ -440,6 +474,17 @@ func getResourceLabel(resource *hclwrite.Block) string {
440474
return labels[1]
441475
}
442476

477+
type dynamicBlock struct {
478+
block *hclwrite.Block
479+
forEach *hclwrite.Attribute
480+
content *hclwrite.Block
481+
tokens hclwrite.Tokens
482+
}
483+
484+
func (d dynamicBlock) IsPresent() bool {
485+
return d.block != nil
486+
}
487+
443488
func checkDynamicBlock(body *hclwrite.Body) error {
444489
for _, block := range body.Blocks() {
445490
name := getResourceName(block)
@@ -451,12 +496,6 @@ func checkDynamicBlock(body *hclwrite.Body) error {
451496
return nil
452497
}
453498

454-
type dynamicBlock struct {
455-
block *hclwrite.Block
456-
forEach *hclwrite.Attribute
457-
content *hclwrite.Block
458-
}
459-
460499
func getDynamicBlock(body *hclwrite.Body, name string) (dynamicBlock, error) {
461500
for _, block := range body.Blocks() {
462501
if block.Type() != nDynamic || name != getResourceName(block) {
@@ -481,6 +520,55 @@ func replaceDynamicBlockExpr(attr *hclwrite.Attribute, blockName, attrName strin
481520
return strings.ReplaceAll(expr, fmt.Sprintf("%s.%s", blockName, attrName), attrName)
482521
}
483522

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+
529+
// getDynamicBlockRegionConfigsRegionArray returns the region array for a dynamic block in replication_specs.
530+
// 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))
534+
if priorityStr == "" {
535+
return nil, fmt.Errorf("%s: %s not found", errRepSpecs, nPriority)
536+
}
537+
region, err := getRegionConfig(d.content, root, true)
538+
if err != nil {
539+
return nil, err
540+
}
541+
tokens := hcl.TokensFromExpr(fmt.Sprintf("for %s in %s :", nRegion, hcl.GetAttrExpr(d.forEach)))
542+
tokens = append(tokens, hcl.EncloseBraces(region.BuildTokens(nil), true)...)
543+
tokens = append(tokens, hcl.TokensFromExpr(fmt.Sprintf("if %s == %s", nPriority, priorityStr))...)
544+
return hcl.EncloseBracketsNewLines(tokens), nil
545+
}
546+
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) {
549+
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))
554+
configSrcb.SetAttributeRaw(name, hcl.TokensFromExpr(expr))
555+
}
556+
}
557+
558+
func sortConfigsByPriority(configs []*hclwrite.Body) []*hclwrite.Body {
559+
for _, config := range configs {
560+
if _, err := hcl.GetAttrInt(config.GetAttribute(nPriority), errPriority); err != nil {
561+
return configs // don't sort priorities if any is not a numerical literal
562+
}
563+
}
564+
sort.Slice(configs, func(i, j int) bool {
565+
pi, _ := hcl.GetAttrInt(configs[i].GetAttribute(nPriority), errPriority)
566+
pj, _ := hcl.GetAttrInt(configs[j].GetAttribute(nPriority), errPriority)
567+
return pi > pj
568+
})
569+
return configs
570+
}
571+
484572
func setKeyValue(body *hclwrite.Body, key, value *hclwrite.Attribute) {
485573
keyStr, err := hcl.GetAttrString(key, "")
486574
if err == nil {
@@ -494,21 +582,6 @@ func setKeyValue(body *hclwrite.Body, key, value *hclwrite.Attribute) {
494582
body.SetAttributeRaw(keyStr, value.Expr().BuildTokens(nil))
495583
}
496584

497-
func setPriority(body *hclwrite.Body, priority *hclwrite.Attribute) error {
498-
if priority == nil {
499-
return fmt.Errorf("%s: %s not found", errRepSpecs, nPriority)
500-
}
501-
valPriority, err := hcl.GetAttrInt(priority, errPriority)
502-
if err != nil {
503-
return err
504-
}
505-
if valPriority < valMinPriority || valPriority > valMaxPriority {
506-
return fmt.Errorf("%s: %s is %d but must be between %d and %d", errPriority, nPriority, valPriority, valMinPriority, valMaxPriority)
507-
}
508-
hcl.SetAttrInt(body, nPriority, valPriority)
509-
return nil
510-
}
511-
512585
// popRootAttrs deletes the attributes common to all replication_specs/regions_config and returns them.
513586
func popRootAttrs(body *hclwrite.Body) (attrVals, error) {
514587
var (

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,5 @@ resource "mongodbatlas_advanced_cluster" "this" {
3232
}
3333

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

internal/convert/testdata/clu2adv/analytics_read_only_all_params.in.tf

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,24 @@ resource "mongodbatlas_cluster" "ar" {
1818
}
1919
}
2020
}
21+
22+
resource "mongodbatlas_cluster" "ar_not_electable" {
23+
project_id = var.project_id
24+
name = "ar"
25+
cluster_type = "REPLICASET"
26+
provider_name = "AWS"
27+
provider_instance_size_name = "M10"
28+
disk_size_gb = 90
29+
provider_volume_type = "PROVISIONED"
30+
provider_disk_iops = 100
31+
replication_specs {
32+
num_shards = 1
33+
regions_config {
34+
region_name = "US_EAST_1"
35+
priority = 7
36+
electable_nodes = 0
37+
analytics_nodes = 2
38+
read_only_nodes = 1
39+
}
40+
}
41+
}

0 commit comments

Comments
 (0)