Skip to content

Commit 3d13e27

Browse files
authored
feat: Supports dynamic blocks in tags and labels (#31)
* tags * pass tests * allow expressions * expression_individual * change format style * pass tests * full example * refactor * small refactors * rename var * readme * typo * doc for update plugin * typo * doc examples link * change v2 to 2.0.0 * changelog * refactor key / value attributes in dynamic blocks
1 parent c4563c4 commit 3d13e27

File tree

8 files changed

+530
-38
lines changed

8 files changed

+530
-38
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## (Unreleased)
22

3+
ENHANCEMENTS:
4+
5+
* Supports `dynamic` block for `tags` and `labels`
6+
37
## 1.0.0 (Mar 6, 2025)
48

59
NOTES:

README.md

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
This repository contains the Atlas CLI plugin for [Terraform's MongoDB Atlas Provider](https://registry.terraform.io/providers/mongodb/mongodbatlas/latest/docs).
66

77
It has the following commands to help with your Terraform configurations:
8-
- **clusterToAdvancedCluster**: Convert a `mongodbatlas_cluster` Terraform configuration to `mongodbatlas_advanced_cluster` (preview provider v2).
8+
- **clusterToAdvancedCluster**: Convert a `mongodbatlas_cluster` Terraform configuration to `mongodbatlas_advanced_cluster` (preview provider 2.0.0).
99

1010
## Installation
1111

@@ -15,12 +15,22 @@ Install the plugin by running:
1515
```bash
1616
atlas plugin install github.com/mongodb-labs/atlas-cli-plugin-terraform
1717
```
18+
19+
If you have it installed and want to update it to the latest version, run:
20+
```bash
21+
atlas plugin update mongodb-labs/atlas-cli-plugin-terraform
22+
```
23+
24+
If you want to see the list of installed plugins or check if this plugin is installed, run:
25+
```bash
26+
atlas plugin list
27+
```
1828

19-
## Convert mongodbatlas_cluster to mongodbatlas_advanced_cluster (preview provider v2)
29+
## Convert mongodbatlas_cluster to mongodbatlas_advanced_cluster (preview provider 2.0.0)
2030

2131
### Usage
2232

23-
**Note**: In order to use the **Preview for MongoDB Atlas Provider v2** of `mongodbatlas_advanced_cluster`, you need to set the environment variable `MONGODB_ATLAS_PREVIEW_PROVIDER_V2_ADVANCED_CLUSTER` to `true`.
33+
**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`.
2434

2535
If you want to convert a Terraform configuration from `mongodbatlas_cluster` to `mongodbatlas_advanced_cluster`, use the following command:
2636
```bash
@@ -38,12 +48,36 @@ If you want to overwrite the output file if it exists, or even use the same outp
3848

3949
You can use the `--watch` or the `-w` flag to keep the plugin running and watching for changes in the input file. You can have input and output files open in an editor and see easily how changes to the input file affect the output file.
4050

51+
You can find [here](https://github.com/mongodb-labs/atlas-cli-plugin-terraform/tree/main/internal/convert/testdata/clu2adv) some examples of input files (suffix .in.tf) and the corresponding output files (suffix .out.tf).
52+
53+
### Dynamic blocks
54+
55+
`dynamic` blocks are used to generate multiple nested blocks based on a set of values.
56+
Given the different ways of using dynamic blocks, we recommend reviewing the output and making sure it fits your needs.
57+
58+
#### Dynamic blocks in tags and labels
59+
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.:
61+
```hcl
62+
tags {
63+
key = "environment"
64+
value = var.environment
65+
}
66+
dynamic "tags" {
67+
for_each = var.tags
68+
content {
69+
key = tags.key
70+
value = replace(tags.value, "/", "_")
71+
}
72+
}
73+
```
74+
4175
### Limitations
4276

4377
- 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`.
4478
- [`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`.
4579
- [`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`.
46-
- `dynamic` blocks to generate `replication_specs`, `regions_config`, etc. are not supported.
80+
- `dynamic` blocks are currently supported only for `tags` and `labels`. **Coming soon**: support for `replication_specs` and `regions_config`.
4781

4882
## Contributing
4983

internal/cli/clu2adv/clu2adv.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ func Builder() *cobra.Command {
1010
o := &opts{fs: afero.NewOsFs()}
1111
cmd := &cobra.Command{
1212
Use: "clusterToAdvancedCluster",
13-
Short: "Convert cluster to advanced_cluster v2",
14-
Long: "Convert a Terraform configuration from mongodbatlas_cluster to mongodbatlas_advanced_cluster schema v2",
13+
Short: "Convert cluster to advanced_cluster preview provider 2.0.0",
14+
Long: "Convert a Terraform configuration from mongodbatlas_cluster to mongodbatlas_advanced_cluster preview provider 2.0.0",
1515
Aliases: []string{"clu2adv"},
1616
RunE: func(_ *cobra.Command, _ []string) error {
1717
if err := o.PreRun(); err != nil {

internal/convert/const_names.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,7 @@ const (
5151
nMoved = "moved"
5252
nFrom = "from"
5353
nTo = "to"
54+
nDynamic = "dynamic"
55+
nForEach = "for_each"
56+
nContent = "content"
5457
)

internal/convert/convert.go

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package convert
22

33
import (
4-
"errors"
54
"fmt"
5+
"slices"
66
"sort"
77
"strconv"
88
"strings"
@@ -36,13 +36,17 @@ const (
3636
commentRemovedOld = "Note: Remember to remove or comment out the old cluster definitions."
3737
)
3838

39+
var (
40+
dynamicBlockAllowList = []string{nTags, nLabels}
41+
)
42+
3943
type attrVals struct {
4044
req map[string]hclwrite.Tokens
4145
opt map[string]hclwrite.Tokens
4246
}
4347

4448
// ClusterToAdvancedCluster transforms all mongodbatlas_cluster definitions in a
45-
// Terraform configuration file into mongodbatlas_advanced_cluster schema v2 definitions.
49+
// Terraform configuration file into mongodbatlas_advanced_cluster schema 2.0.0.
4650
// All other resources and data sources are left untouched.
4751
// Note: hclwrite.Tokens are used instead of cty.Value so expressions with interpolations like var.region can be preserved.
4852
// cty.Value only supports literal expressions.
@@ -224,6 +228,50 @@ func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error {
224228
}
225229

226230
func fillTagsLabelsOpt(resourceb *hclwrite.Body, name string) error {
231+
tokensDynamic, err := extractTagsLabelsDynamicBlock(resourceb, name)
232+
if err != nil {
233+
return err
234+
}
235+
tokensIndividual, err := extractTagsLabelsIndividual(resourceb, name)
236+
if err != nil {
237+
return err
238+
}
239+
if tokensDynamic != nil && tokensIndividual != nil {
240+
resourceb.SetAttributeRaw(name, hcl.TokensFuncMerge(tokensDynamic, tokensIndividual))
241+
return nil
242+
}
243+
if tokensDynamic != nil {
244+
resourceb.SetAttributeRaw(name, tokensDynamic)
245+
}
246+
if tokensIndividual != nil {
247+
resourceb.SetAttributeRaw(name, tokensIndividual)
248+
}
249+
return nil
250+
}
251+
252+
func extractTagsLabelsDynamicBlock(resourceb *hclwrite.Body, name string) (hclwrite.Tokens, error) {
253+
d, err := getDynamicBlock(resourceb, name)
254+
if err != nil || d.forEach == nil {
255+
return nil, err
256+
}
257+
key := d.content.Body().GetAttribute(nKey)
258+
value := d.content.Body().GetAttribute(nValue)
259+
if key == nil || value == nil {
260+
return nil, fmt.Errorf("dynamic block %s: %s or %s not found", name, nKey, nValue)
261+
}
262+
keyExpr := replaceDynamicBlockExpr(key, name, nKey)
263+
valueExpr := replaceDynamicBlockExpr(value, name, nValue)
264+
collectionExpr := hcl.GetAttrExpr(d.forEach)
265+
forExpr := fmt.Sprintf("for key, value in %s : %s => %s", collectionExpr, keyExpr, valueExpr)
266+
tokens := hcl.TokensObjectFromExpr(forExpr)
267+
if keyExpr == nKey && valueExpr == nValue { // expression can be simplified and use for_each expression
268+
tokens = hcl.TokensFromExpr(collectionExpr)
269+
}
270+
resourceb.RemoveBlock(d.block)
271+
return tokens, nil
272+
}
273+
274+
func extractTagsLabelsIndividual(resourceb *hclwrite.Body, name string) (hclwrite.Tokens, error) {
227275
var (
228276
file = hclwrite.NewEmptyFile()
229277
fileb = file.Body()
@@ -237,16 +285,16 @@ func fillTagsLabelsOpt(resourceb *hclwrite.Body, name string) error {
237285
key := block.Body().GetAttribute(nKey)
238286
value := block.Body().GetAttribute(nValue)
239287
if key == nil || value == nil {
240-
return fmt.Errorf("%s: %s or %s not found", name, nKey, nValue)
288+
return nil, fmt.Errorf("%s: %s or %s not found", name, nKey, nValue)
241289
}
242290
setKeyValue(fileb, key, value)
243291
resourceb.RemoveBlock(block)
244292
found = true
245293
}
246-
if found {
247-
resourceb.SetAttributeRaw(name, hcl.TokensObject(fileb))
294+
if !found {
295+
return nil, nil
248296
}
249-
return nil
297+
return hcl.TokensObject(fileb), nil
250298
}
251299

252300
func fillBlockOpt(resourceb *hclwrite.Body, name string) {
@@ -394,13 +442,45 @@ func getResourceLabel(resource *hclwrite.Block) string {
394442

395443
func checkDynamicBlock(body *hclwrite.Body) error {
396444
for _, block := range body.Blocks() {
397-
if block.Type() == "dynamic" {
398-
return errors.New("dynamic blocks are not supported")
445+
name := getResourceName(block)
446+
if block.Type() != nDynamic || slices.Contains(dynamicBlockAllowList, name) {
447+
continue
399448
}
449+
return fmt.Errorf("dynamic blocks are not supported for %s", name)
400450
}
401451
return nil
402452
}
403453

454+
type dynamicBlock struct {
455+
block *hclwrite.Block
456+
forEach *hclwrite.Attribute
457+
content *hclwrite.Block
458+
}
459+
460+
func getDynamicBlock(body *hclwrite.Body, name string) (dynamicBlock, error) {
461+
for _, block := range body.Blocks() {
462+
if block.Type() != nDynamic || name != getResourceName(block) {
463+
continue
464+
}
465+
blockb := block.Body()
466+
forEach := blockb.GetAttribute(nForEach)
467+
if forEach == nil {
468+
return dynamicBlock{}, fmt.Errorf("dynamic block %s: attribute %s not found", name, nForEach)
469+
}
470+
content := blockb.FirstMatchingBlock(nContent, nil)
471+
if content == nil {
472+
return dynamicBlock{}, fmt.Errorf("dynamic block %s: block %s not found", name, nContent)
473+
}
474+
return dynamicBlock{forEach: forEach, block: block, content: content}, nil
475+
}
476+
return dynamicBlock{}, nil
477+
}
478+
479+
func replaceDynamicBlockExpr(attr *hclwrite.Attribute, blockName, attrName string) string {
480+
expr := hcl.GetAttrExpr(attr)
481+
return strings.ReplaceAll(expr, fmt.Sprintf("%s.%s", blockName, attrName), attrName)
482+
}
483+
404484
func setKeyValue(body *hclwrite.Body, key, value *hclwrite.Attribute) {
405485
keyStr, err := hcl.GetAttrString(key, "")
406486
if err == nil {

0 commit comments

Comments
 (0)