Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## (Unreleased)

ENHANCEMENTS:

* Supports `dynamic` block for `tags` and `labels`

## 1.0.0 (Mar 6, 2025)

NOTES:
Expand Down
42 changes: 38 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
This repository contains the Atlas CLI plugin for [Terraform's MongoDB Atlas Provider](https://registry.terraform.io/providers/mongodb/mongodbatlas/latest/docs).

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

## Installation

Expand All @@ -15,12 +15,22 @@ Install the plugin by running:
```bash
atlas plugin install github.com/mongodb-labs/atlas-cli-plugin-terraform
```

If you have it installed and want to update it to the latest version, run:
```bash
atlas plugin update mongodb-labs/atlas-cli-plugin-terraform
```

If you want to see the list of installed plugins or check if this plugin is installed, run:
```bash
atlas plugin list
```

## Convert mongodbatlas_cluster to mongodbatlas_advanced_cluster (preview provider v2)
## Convert mongodbatlas_cluster to mongodbatlas_advanced_cluster (preview provider 2.0.0)

### Usage

**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`.
**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`.

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

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.

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).

### Dynamic blocks

`dynamic` blocks are used to generate multiple nested blocks based on a set of values.
Given the different ways of using dynamic blocks, we recommend reviewing the output and making sure it fits your needs.

#### Dynamic blocks in tags and labels

You can use `dynamic` blocks for `tags` and `labels`. You can also combine the use of dynamic blocks in `tags` and `labels` with individual blocks in the same cluster definition, e.g.:
```hcl
tags {
key = "environment"
value = var.environment
}
dynamic "tags" {
for_each = var.tags
content {
key = tags.key
value = replace(tags.value, "/", "_")
}
}
```

### Limitations

- The plugin doesn't support `regions_config` without `electable_nodes` as there can be some issues with `priority` when they only have `analytics_nodes` and/or `electable_nodes`.
- [`priority`](https://registry.terraform.io/providers/mongodb/mongodbatlas/latest/docs/resources/cluster#priority-1) is required in `regions_config` and must be a numeric [literal expression](https://developer.hashicorp.com/nomad/docs/job-specification/hcl2/expressions#literal-expressions) between 7 and 1, e.g. `var.priority` is not supported. This is to allow reordering them by descending priority as this is expected in `mongodbatlas_advanced_cluster`.
- [`num_shards`](https://registry.terraform.io/providers/mongodb/mongodbatlas/latest/docs/resources/cluster#num_shards-2) in `replication_specs` must be a numeric [literal expression](https://developer.hashicorp.com/nomad/docs/job-specification/hcl2/expressions#literal-expressions), e.g. `var.num_shards` is not supported. This is to allow creating a `replication_specs` element per shard in `mongodbatlas_advanced_cluster`.
- `dynamic` blocks to generate `replication_specs`, `regions_config`, etc. are not supported.
- `dynamic` blocks are currently supported only for `tags` and `labels`. **Coming soon**: support for `replication_specs` and `regions_config`.

## Contributing

Expand Down
4 changes: 2 additions & 2 deletions internal/cli/clu2adv/clu2adv.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ func Builder() *cobra.Command {
o := &opts{fs: afero.NewOsFs()}
cmd := &cobra.Command{
Use: "clusterToAdvancedCluster",
Short: "Convert cluster to advanced_cluster v2",
Long: "Convert a Terraform configuration from mongodbatlas_cluster to mongodbatlas_advanced_cluster schema v2",
Short: "Convert cluster to advanced_cluster preview provider 2.0.0",
Long: "Convert a Terraform configuration from mongodbatlas_cluster to mongodbatlas_advanced_cluster preview provider 2.0.0",
Aliases: []string{"clu2adv"},
RunE: func(_ *cobra.Command, _ []string) error {
if err := o.PreRun(); err != nil {
Expand Down
3 changes: 3 additions & 0 deletions internal/convert/const_names.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,7 @@ const (
nMoved = "moved"
nFrom = "from"
nTo = "to"
nDynamic = "dynamic"
nForEach = "for_each"
nContent = "content"
)
96 changes: 88 additions & 8 deletions internal/convert/convert.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package convert

import (
"errors"
"fmt"
"slices"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -36,13 +36,17 @@ const (
commentRemovedOld = "Note: Remember to remove or comment out the old cluster definitions."
)

var (
dynamicBlockAllowList = []string{nTags, nLabels}
)

type attrVals struct {
req map[string]hclwrite.Tokens
opt map[string]hclwrite.Tokens
}

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

func fillTagsLabelsOpt(resourceb *hclwrite.Body, name string) error {
tokensDynamic, err := extractTagsLabelsDynamicBlock(resourceb, name)
if err != nil {
return err
}
tokensIndividual, err := extractTagsLabelsIndividual(resourceb, name)
if err != nil {
return err
}
if tokensDynamic != nil && tokensIndividual != nil {
resourceb.SetAttributeRaw(name, hcl.TokensFuncMerge(tokensDynamic, tokensIndividual))
return nil
}
if tokensDynamic != nil {
resourceb.SetAttributeRaw(name, tokensDynamic)
}
if tokensIndividual != nil {
resourceb.SetAttributeRaw(name, tokensIndividual)
}
return nil
}

func extractTagsLabelsDynamicBlock(resourceb *hclwrite.Body, name string) (hclwrite.Tokens, error) {
d, err := getDynamicBlock(resourceb, name)
if err != nil || d.forEach == nil {
return nil, err
}
key := d.content.Body().GetAttribute(nKey)
value := d.content.Body().GetAttribute(nValue)
if key == nil || value == nil {
return nil, fmt.Errorf("dynamic block %s: %s or %s not found", name, nKey, nValue)
}
keyExpr := replaceDynamicBlockExpr(key, name, nKey)
valueExpr := replaceDynamicBlockExpr(value, name, nValue)
collectionExpr := hcl.GetAttrExpr(d.forEach)
forExpr := fmt.Sprintf("for key, value in %s : %s => %s", collectionExpr, keyExpr, valueExpr)
tokens := hcl.TokensObjectFromExpr(forExpr)
if keyExpr == nKey && valueExpr == nValue { // expression can be simplified and use for_each expression
tokens = hcl.TokensFromExpr(collectionExpr)
}
resourceb.RemoveBlock(d.block)
return tokens, nil
}

func extractTagsLabelsIndividual(resourceb *hclwrite.Body, name string) (hclwrite.Tokens, error) {
var (
file = hclwrite.NewEmptyFile()
fileb = file.Body()
Expand All @@ -237,16 +285,16 @@ func fillTagsLabelsOpt(resourceb *hclwrite.Body, name string) error {
key := block.Body().GetAttribute(nKey)
value := block.Body().GetAttribute(nValue)
if key == nil || value == nil {
return fmt.Errorf("%s: %s or %s not found", name, nKey, nValue)
return nil, fmt.Errorf("%s: %s or %s not found", name, nKey, nValue)
}
setKeyValue(fileb, key, value)
resourceb.RemoveBlock(block)
found = true
}
if found {
resourceb.SetAttributeRaw(name, hcl.TokensObject(fileb))
if !found {
return nil, nil
}
return nil
return hcl.TokensObject(fileb), nil
}

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

func checkDynamicBlock(body *hclwrite.Body) error {
for _, block := range body.Blocks() {
if block.Type() == "dynamic" {
return errors.New("dynamic blocks are not supported")
name := getResourceName(block)
if block.Type() != nDynamic || slices.Contains(dynamicBlockAllowList, name) {
continue
}
return fmt.Errorf("dynamic blocks are not supported for %s", name)
}
return nil
}

type dynamicBlock struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice abstraction!

block *hclwrite.Block
forEach *hclwrite.Attribute
content *hclwrite.Block
}

func getDynamicBlock(body *hclwrite.Body, name string) (dynamicBlock, error) {
for _, block := range body.Blocks() {
if block.Type() != nDynamic || name != getResourceName(block) {
continue
}
blockb := block.Body()
forEach := blockb.GetAttribute(nForEach)
if forEach == nil {
return dynamicBlock{}, fmt.Errorf("dynamic block %s: attribute %s not found", name, nForEach)
}
content := blockb.FirstMatchingBlock(nContent, nil)
if content == nil {
return dynamicBlock{}, fmt.Errorf("dynamic block %s: block %s not found", name, nContent)
}
return dynamicBlock{forEach: forEach, block: block, content: content}, nil
}
return dynamicBlock{}, nil
}

func replaceDynamicBlockExpr(attr *hclwrite.Attribute, blockName, attrName string) string {
expr := hcl.GetAttrExpr(attr)
return strings.ReplaceAll(expr, fmt.Sprintf("%s.%s", blockName, attrName), attrName)
}

func setKeyValue(body *hclwrite.Body, key, value *hclwrite.Attribute) {
keyStr, err := hcl.GetAttrString(key, "")
if err == nil {
Expand Down
Loading