Skip to content

Commit 3318080

Browse files
authored
chore: Convert free clusters with count (#13)
* test to implement * v2 schema * pass tests * fix linter * clarify doc * comments * refactor to improve readability * refactor isFreeTier * typo * rename consts * more comments * add error checks
1 parent af30720 commit 3318080

File tree

9 files changed

+198
-54
lines changed

9 files changed

+198
-54
lines changed

internal/hcl/hcl.go

Lines changed: 105 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,38 @@ package hcl
22

33
import (
44
"fmt"
5+
"strconv"
56

67
"github.com/hashicorp/hcl/v2"
78
"github.com/hashicorp/hcl/v2/hclsyntax"
89
"github.com/hashicorp/hcl/v2/hclwrite"
10+
"github.com/zclconf/go-cty/cty"
911
)
1012

1113
const (
12-
resourceType = "resource"
13-
cluster = "mongodbatlas_cluster"
14-
advCluster = "mongodbatlas_advanced_cluster"
14+
resourceType = "resource"
15+
cluster = "mongodbatlas_cluster"
16+
advCluster = "mongodbatlas_advanced_cluster"
17+
nameReplicationSpecs = "replication_specs"
18+
nameRegionConfigs = "region_configs"
19+
nameElectableSpecs = "electable_specs"
20+
nameProviderRegionName = "provider_region_name"
21+
nameRegionName = "region_name"
22+
nameProviderName = "provider_name"
23+
nameBackingProviderName = "backing_provider_name"
24+
nameProviderInstanceSizeName = "provider_instance_size_name"
25+
nameInstanceSize = "instance_size"
26+
nameClusterType = "cluster_type"
27+
namePriority = "priority"
28+
29+
errFreeCluster = "free cluster (because no " + nameReplicationSpecs + ")"
1530
)
1631

1732
// ClusterToAdvancedCluster transforms all mongodbatlas_cluster definitions in a
1833
// Terraform configuration file into mongodbatlas_advanced_cluster schema v2 definitions.
1934
// All other resources and data sources are left untouched.
20-
// TODO: at the moment it just changes the resource type.
35+
// Note: hclwrite.Tokens are used instead of cty.Value so expressions like var.region can be preserved.
36+
// cty.Value only supports resolved values.
2137
func ClusterToAdvancedCluster(config []byte) ([]byte, error) {
2238
parser, err := getParser(config)
2339
if err != nil {
@@ -30,31 +46,105 @@ func ClusterToAdvancedCluster(config []byte) ([]byte, error) {
3046
continue
3147
}
3248
resourceBody := resource.Body()
33-
34-
// TODO: Do the full transformation
3549
labels[0] = advCluster
3650
resource.SetLabels(labels)
51+
52+
if isFreeTier(resourceBody) {
53+
if err := fillFreeTier(resourceBody); err != nil {
54+
return nil, err
55+
}
56+
}
57+
3758
resourceBody.AppendNewline()
3859
appendComment(resourceBody, "Generated by atlas-cli-plugin-terraform.")
3960
appendComment(resourceBody, "Please confirm that all references to this resource are updated.")
4061
}
4162
return parser.Bytes(), nil
4263
}
4364

44-
func getParser(config []byte) (*hclwrite.File, error) {
45-
parser, diags := hclwrite.ParseConfig(config, "", hcl.Pos{Line: 1, Column: 1})
46-
if diags.HasErrors() {
47-
return nil, fmt.Errorf("failed to parse Terraform config file: %s", diags.Error())
65+
func isFreeTier(body *hclwrite.Body) bool {
66+
return body.FirstMatchingBlock(nameReplicationSpecs, nil) == nil
67+
}
68+
69+
func fillFreeTier(body *hclwrite.Body) error {
70+
const (
71+
valClusterType = "REPLICASET"
72+
valPriority = 7
73+
)
74+
body.SetAttributeValue(nameClusterType, cty.StringVal(valClusterType))
75+
regionConfig := hclwrite.NewEmptyFile()
76+
regionConfigBody := regionConfig.Body()
77+
setAttrInt(regionConfigBody, "priority", valPriority)
78+
if err := moveAttribute(nameProviderRegionName, nameRegionName, body, regionConfigBody, errFreeCluster); err != nil {
79+
return err
4880
}
49-
return parser, nil
81+
if err := moveAttribute(nameProviderName, nameProviderName, body, regionConfigBody, errFreeCluster); err != nil {
82+
return err
83+
}
84+
if err := moveAttribute(nameBackingProviderName, nameBackingProviderName, body, regionConfigBody, errFreeCluster); err != nil {
85+
return err
86+
}
87+
electableSpec := hclwrite.NewEmptyFile()
88+
if err := moveAttribute(nameProviderInstanceSizeName, nameInstanceSize, body, electableSpec.Body(), errFreeCluster); err != nil {
89+
return err
90+
}
91+
regionConfigBody.SetAttributeRaw(nameElectableSpecs, tokensObject(electableSpec))
92+
93+
replicationSpec := hclwrite.NewEmptyFile()
94+
replicationSpec.Body().SetAttributeRaw(nameRegionConfigs, tokensArrayObject(regionConfig))
95+
body.SetAttributeRaw(nameReplicationSpecs, tokensArrayObject(replicationSpec))
96+
return nil
97+
}
98+
99+
func moveAttribute(fromAttrName, toAttrName string, fromBody, toBody *hclwrite.Body, errPrefix string) error {
100+
attr := fromBody.GetAttribute(fromAttrName)
101+
if attr == nil {
102+
return fmt.Errorf("%s: attribute %s not found", errPrefix, fromAttrName)
103+
}
104+
fromBody.RemoveAttribute(fromAttrName)
105+
toBody.SetAttributeRaw(toAttrName, attr.Expr().BuildTokens(nil))
106+
return nil
107+
}
108+
109+
func setAttrInt(body *hclwrite.Body, attrName string, number int) {
110+
tokens := hclwrite.Tokens{
111+
{Type: hclsyntax.TokenNumberLit, Bytes: []byte(strconv.Itoa(number))},
112+
}
113+
body.SetAttributeRaw(attrName, tokens)
114+
}
115+
116+
func tokensArrayObject(file *hclwrite.File) hclwrite.Tokens {
117+
ret := hclwrite.Tokens{
118+
{Type: hclsyntax.TokenOBrack, Bytes: []byte("[")},
119+
}
120+
ret = append(ret, tokensObject(file)...)
121+
ret = append(ret,
122+
&hclwrite.Token{Type: hclsyntax.TokenCBrack, Bytes: []byte("]")})
123+
return ret
124+
}
125+
126+
func tokensObject(file *hclwrite.File) hclwrite.Tokens {
127+
ret := hclwrite.Tokens{
128+
{Type: hclsyntax.TokenOBrack, Bytes: []byte("{")},
129+
{Type: hclsyntax.TokenNewline, Bytes: []byte("\n")},
130+
}
131+
ret = append(ret, file.BuildTokens(nil)...)
132+
ret = append(ret,
133+
&hclwrite.Token{Type: hclsyntax.TokenCBrack, Bytes: []byte("}")})
134+
return ret
50135
}
51136

52137
func appendComment(body *hclwrite.Body, comment string) {
53138
tokens := hclwrite.Tokens{
54-
&hclwrite.Token{
55-
Type: hclsyntax.TokenComment,
56-
Bytes: []byte("# " + comment + "\n"),
57-
},
139+
&hclwrite.Token{Type: hclsyntax.TokenComment, Bytes: []byte("# " + comment + "\n")},
58140
}
59141
body.AppendUnstructuredTokens(tokens)
60142
}
143+
144+
func getParser(config []byte) (*hclwrite.File, error) {
145+
parser, diags := hclwrite.ParseConfig(config, "", hcl.Pos{Line: 1, Column: 1})
146+
if diags.HasErrors() {
147+
return nil, fmt.Errorf("failed to parse Terraform config file: %s", diags.Error())
148+
}
149+
return parser, nil
150+
}

internal/hcl/hcl_test.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package hcl_test
22

33
import (
4+
"encoding/json"
45
"path/filepath"
56
"strings"
67
"testing"
@@ -14,14 +15,20 @@ import (
1415

1516
func TestClusterToAdvancedCluster(t *testing.T) {
1617
const (
17-
root = "testdata/clu2adv"
18-
inSuffix = ".in.tf"
19-
outSuffix = ".out.tf"
18+
root = "testdata/clu2adv"
19+
inSuffix = ".in.tf"
20+
outSuffix = ".out.tf"
21+
errFilename = "errors.json"
2022
)
23+
fs := afero.NewOsFs()
24+
errMap := make(map[string]string)
25+
errContent, err := afero.ReadFile(fs, filepath.Join(root, errFilename))
26+
require.NoError(t, err)
27+
err = json.Unmarshal(errContent, &errMap)
28+
require.NoError(t, err)
2129
g := goldie.New(t,
2230
goldie.WithFixtureDir(root),
2331
goldie.WithNameSuffix(outSuffix))
24-
fs := afero.NewOsFs()
2532
pattern := filepath.Join(root, "*"+inSuffix)
2633
inputFiles, err := afero.Glob(fs, pattern)
2734
require.NoError(t, err)
@@ -31,7 +38,12 @@ func TestClusterToAdvancedCluster(t *testing.T) {
3138
inConfig, err := afero.ReadFile(fs, inputFile)
3239
require.NoError(t, err)
3340
outConfig, err := hcl.ClusterToAdvancedCluster(inConfig)
34-
require.NoError(t, err)
35-
g.Assert(t, testName, outConfig)
41+
if err == nil {
42+
g.Assert(t, testName, outConfig)
43+
} else {
44+
errMsg, found := errMap[testName]
45+
assert.True(t, found, "error not found for test %s", testName)
46+
assert.Contains(t, err.Error(), errMsg)
47+
}
3648
}
3749
}

internal/hcl/testdata/clu2adv/basic.in.tf

Lines changed: 0 additions & 15 deletions
This file was deleted.

internal/hcl/testdata/clu2adv/basic.out.tf

Lines changed: 0 additions & 18 deletions
This file was deleted.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
resource this is an invalid HCL configuration file
2+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"configuration_file_error": "failed to parse Terraform config file",
3+
"free_cluster_missing_attribute": "free cluster (because no replication_specs): attribute backing_provider_name not found"
4+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
resource "resource1" "res1" {
2+
name = "name1"
3+
}
4+
5+
resource "mongodbatlas_cluster" "free_cluster" { # comment in the resource
6+
# comment in own line in the beginning
7+
count = local.use_free_cluster ? 1 : 0
8+
project_id = var.project_id # inline comment kept
9+
name = var.cluster_name
10+
# comment in own line in the middle is deleted
11+
provider_name = "TENANT" # inline comment for attribute moved is not kept
12+
provider_region_name = var.region
13+
provider_instance_size_name = "M0"
14+
# comment in own line at the end happens before replication_specs
15+
}
16+
17+
data "mongodbatlas_cluster" "cluster2" {
18+
name = "name4"
19+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
resource "resource1" "res1" {
2+
name = "name1"
3+
}
4+
5+
resource "mongodbatlas_cluster" "free_cluster" { # comment in the resource
6+
# comment in own line in the beginning
7+
count = local.use_free_cluster ? 1 : 0
8+
project_id = var.project_id # inline comment kept
9+
name = var.cluster_name
10+
# comment in own line in the middle is deleted
11+
provider_name = "TENANT" # inline comment for attribute moved is not kept
12+
backing_provider_name = "AWS"
13+
provider_region_name = var.region
14+
provider_instance_size_name = "M0"
15+
# comment in own line at the end happens before replication_specs
16+
}
17+
18+
data "mongodbatlas_cluster" "cluster2" {
19+
name = "name4"
20+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
resource "resource1" "res1" {
2+
name = "name1"
3+
}
4+
5+
resource "mongodbatlas_advanced_cluster" "free_cluster" { # comment in the resource
6+
# comment in own line in the beginning
7+
count = local.use_free_cluster ? 1 : 0
8+
project_id = var.project_id # inline comment kept
9+
name = var.cluster_name
10+
# comment in own line at the end happens before replication_specs
11+
cluster_type = "REPLICASET"
12+
replication_specs = [{
13+
region_configs = [{
14+
priority = 7
15+
region_name = var.region
16+
provider_name = "TENANT"
17+
backing_provider_name = "AWS"
18+
electable_specs = {
19+
instance_size = "M0"
20+
}
21+
}]
22+
}]
23+
24+
# Generated by atlas-cli-plugin-terraform.
25+
# Please confirm that all references to this resource are updated.
26+
}
27+
28+
data "mongodbatlas_cluster" "cluster2" {
29+
name = "name4"
30+
}

0 commit comments

Comments
 (0)