Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
120 changes: 105 additions & 15 deletions internal/hcl/hcl.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,38 @@ package hcl

import (
"fmt"
"strconv"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/zclconf/go-cty/cty"
)

const (
resourceType = "resource"
cluster = "mongodbatlas_cluster"
advCluster = "mongodbatlas_advanced_cluster"
resourceType = "resource"
cluster = "mongodbatlas_cluster"
advCluster = "mongodbatlas_advanced_cluster"
strReplicationSpecs = "replication_specs"
strRegionConfigs = "region_configs"
strElectableSpecs = "electable_specs"
strProviderRegionName = "provider_region_name"
strRegionName = "region_name"
strProviderName = "provider_name"
strBackingProviderName = "backing_provider_name"
strProviderInstanceSizeName = "provider_instance_size_name"
strInstanceSize = "instance_size"
strClusterType = "cluster_type"
strPriority = "priority"

errFreeCluster = "free cluster (because no " + strReplicationSpecs + ")"
)

// ClusterToAdvancedCluster transforms all mongodbatlas_cluster definitions in a
// Terraform configuration file into mongodbatlas_advanced_cluster schema v2 definitions.
// All other resources and data sources are left untouched.
// TODO: at the moment it just changes the resource type.
// Note: hclwrite.Tokens are used instead of cty.Value so expressions like var.region can be preserved.
// cty.Value only supports resolved values.
func ClusterToAdvancedCluster(config []byte) ([]byte, error) {
parser, err := getParser(config)
if err != nil {
Expand All @@ -30,31 +46,105 @@ func ClusterToAdvancedCluster(config []byte) ([]byte, error) {
continue
}
resourceBody := resource.Body()

// TODO: Do the full transformation
labels[0] = advCluster
resource.SetLabels(labels)

if isFreeTier(resourceBody) {
Copy link
Contributor

Choose a reason for hiding this comment

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

cool approach of starting with one specific case.
I guess some operations might be more generally applicable, like moving instance_size, but we'll find out once we add more cases!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yes, I'll go now with one with num_shards

if err := fillFreeTier(resourceBody); err != nil {
return nil, err
}
}

resourceBody.AppendNewline()
appendComment(resourceBody, "Generated by atlas-cli-plugin-terraform.")
appendComment(resourceBody, "Please confirm that all references to this resource are updated.")
}
return parser.Bytes(), nil
}

func getParser(config []byte) (*hclwrite.File, error) {
parser, diags := hclwrite.ParseConfig(config, "", hcl.Pos{Line: 1, Column: 1})
if diags.HasErrors() {
return nil, fmt.Errorf("failed to parse Terraform config file: %s", diags.Error())
func isFreeTier(body *hclwrite.Body) bool {
return body.FirstMatchingBlock(strReplicationSpecs, nil) == nil
}

func fillFreeTier(body *hclwrite.Body) error {
const (
valClusterType = "REPLICASET"
valPriority = 7
)
body.SetAttributeValue(strClusterType, cty.StringVal(valClusterType))
regionConfig := hclwrite.NewEmptyFile()
regionConfigBody := regionConfig.Body()
setAttrInt(regionConfigBody, "priority", valPriority)
if err := moveAttribute(strProviderRegionName, strRegionName, body, regionConfigBody, errFreeCluster); err != nil {
return err
}
return parser, nil
if err := moveAttribute(strProviderName, strProviderName, body, regionConfigBody, errFreeCluster); err != nil {
return err
}
if err := moveAttribute(strBackingProviderName, strBackingProviderName, body, regionConfigBody, errFreeCluster); err != nil {
return err
}
electableSpec := hclwrite.NewEmptyFile()
if err := moveAttribute(strProviderInstanceSizeName, strInstanceSize, body, electableSpec.Body(), errFreeCluster); err != nil {
return err
}
regionConfigBody.SetAttributeRaw(strElectableSpecs, tokensObject(electableSpec))

replicationSpec := hclwrite.NewEmptyFile()
replicationSpec.Body().SetAttributeRaw(strRegionConfigs, tokensArrayObject(regionConfig))
body.SetAttributeRaw(strReplicationSpecs, tokensArrayObject(replicationSpec))
return nil
}

func moveAttribute(fromAttrName, toAttrName string, fromBody, toBody *hclwrite.Body, errPrefix string) error {
Copy link
Contributor

@EspenAlbert EspenAlbert Feb 3, 2025

Choose a reason for hiding this comment

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

I like the abstraction signature and you don't need to worry about the type of the attribute!

attr := fromBody.GetAttribute(fromAttrName)
if attr == nil {
return fmt.Errorf("%s: attribute %s not found", errPrefix, fromAttrName)
}
fromBody.RemoveAttribute(fromAttrName)
toBody.SetAttributeRaw(toAttrName, attr.Expr().BuildTokens(nil))
return nil
}

func setAttrInt(body *hclwrite.Body, attrName string, number int) {
tokens := hclwrite.Tokens{
{Type: hclsyntax.TokenNumberLit, Bytes: []byte(strconv.Itoa(number))},
}
body.SetAttributeRaw(attrName, tokens)
}

func tokensArrayObject(file *hclwrite.File) hclwrite.Tokens {
ret := hclwrite.Tokens{
{Type: hclsyntax.TokenOBrack, Bytes: []byte("[")},
}
ret = append(ret, tokensObject(file)...)
ret = append(ret,
&hclwrite.Token{Type: hclsyntax.TokenCBrack, Bytes: []byte("]")})
return ret
}

func tokensObject(file *hclwrite.File) hclwrite.Tokens {
ret := hclwrite.Tokens{
{Type: hclsyntax.TokenOBrack, Bytes: []byte("{")},
{Type: hclsyntax.TokenNewline, Bytes: []byte("\n")},
}
ret = append(ret, file.BuildTokens(nil)...)
ret = append(ret,
&hclwrite.Token{Type: hclsyntax.TokenCBrack, Bytes: []byte("}")})
return ret
}

func appendComment(body *hclwrite.Body, comment string) {
tokens := hclwrite.Tokens{
&hclwrite.Token{
Type: hclsyntax.TokenComment,
Bytes: []byte("# " + comment + "\n"),
},
&hclwrite.Token{Type: hclsyntax.TokenComment, Bytes: []byte("# " + comment + "\n")},
}
body.AppendUnstructuredTokens(tokens)
}

func getParser(config []byte) (*hclwrite.File, error) {
parser, diags := hclwrite.ParseConfig(config, "", hcl.Pos{Line: 1, Column: 1})
if diags.HasErrors() {
return nil, fmt.Errorf("failed to parse Terraform config file: %s", diags.Error())
}
return parser, nil
}
15 changes: 0 additions & 15 deletions internal/hcl/testdata/clu2adv/basic.in.tf

This file was deleted.

18 changes: 0 additions & 18 deletions internal/hcl/testdata/clu2adv/basic.out.tf

This file was deleted.

18 changes: 18 additions & 0 deletions internal/hcl/testdata/clu2adv/free_cluster_with_count.in.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
resource "resource1" "res1" {
name = "name1"
}

resource "mongodbatlas_cluster" "free_cluster" { # comment in the resource
# comment in own line
count = local.use_free_cluster ? 1 : 0
project_id = var.project_id # inline comment kept
name = var.cluster_name
provider_name = "TENANT" # inline comment for attribute moved is not kept
backing_provider_name = "AWS"
provider_region_name = var.region
provider_instance_size_name = "M0"
}

data "mongodbatlas_cluster" "cluster2" {
name = "name4"
}
29 changes: 29 additions & 0 deletions internal/hcl/testdata/clu2adv/free_cluster_with_count.out.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
resource "resource1" "res1" {
name = "name1"
}

resource "mongodbatlas_advanced_cluster" "free_cluster" { # comment in the resource
# comment in own line
count = local.use_free_cluster ? 1 : 0
project_id = var.project_id # inline comment kept
name = var.cluster_name
cluster_type = "REPLICASET"
replication_specs = [{
region_configs = [{
priority = 7
region_name = var.region
provider_name = "TENANT"
backing_provider_name = "AWS"
electable_specs = {
instance_size = "M0"
}
}]
}]

# Generated by atlas-cli-plugin-terraform.
# Please confirm that all references to this resource are updated.
}

data "mongodbatlas_cluster" "cluster2" {
name = "name4"
}