Skip to content

Commit c4722c9

Browse files
authored
feat: Converts clusters with multiple replications_specs, region_configs and shards (#22)
* remove version from pinned_fcv in example * failing test * multi repls * num_shards * error conditions * fillTagsLabelsOpt * fillRegionConfigs * feedback about literal expressions * change preview provider v2 doc
1 parent 063910b commit c4722c9

13 files changed

+333
-83
lines changed

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@ Install the plugin by running:
1616
atlas plugin install github.com/mongodb-labs/atlas-cli-plugin-terraform
1717
```
1818

19-
## Convert cluster to advanced_cluster v2
19+
## Convert mongodbatlas_cluster to mongodbatlas_advanced_cluster (preview provider v2)
2020

2121
### Usage
2222

23-
If you want to convert a Terraform configuration from `mongodbatlas_cluster` to `mongodbatlas_advanced_cluster` schema v2, use the following command:
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`.
24+
25+
If you want to convert a Terraform configuration from `mongodbatlas_cluster` to `mongodbatlas_advanced_cluster`, use the following command:
2426
```bash
2527
atlas terraform clusterToAdvancedCluster --file in.tf --output out.tf
2628
```
@@ -35,7 +37,8 @@ If you want to overwrite the output file if it exists, or even use the same outp
3537
### Limitations
3638

3739
- 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`.
38-
- `priority` is required in `regions_config` and must be a resolved number 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`.
40+
- [`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`.
41+
- [`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`.
3942
- `dynamic` blocks to generate `replication_specs`, `regions_config`, etc. are not supported.
4043

4144
## Contributing

internal/convert/const_names.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const (
4444
nElectableNodes = "electable_nodes"
4545
nReadOnlyNodes = "read_only_nodes"
4646
nAnalyticsNodes = "analytics_nodes"
47+
nZoneName = "zone_name"
4748
nKey = "key"
4849
nValue = "value"
4950
)

internal/convert/convert.go

Lines changed: 95 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const (
2424
errRepSpecs = "setting " + nRepSpecs
2525
errConfigs = "setting " + nConfig
2626
errPriority = "setting " + nPriority
27+
errNumShards = "setting " + nNumShards
2728
)
2829

2930
type attrVals struct {
@@ -34,8 +35,8 @@ type attrVals struct {
3435
// ClusterToAdvancedCluster transforms all mongodbatlas_cluster definitions in a
3536
// Terraform configuration file into mongodbatlas_advanced_cluster schema v2 definitions.
3637
// All other resources and data sources are left untouched.
37-
// Note: hclwrite.Tokens are used instead of cty.Value so expressions like var.region can be preserved.
38-
// cty.Value only supports resolved values.
38+
// Note: hclwrite.Tokens are used instead of cty.Value so expressions with interpolations like var.region can be preserved.
39+
// cty.Value only supports literal expressions.
3940
func ClusterToAdvancedCluster(config []byte) ([]byte, error) {
4041
parser, err := hcl.GetParser(config)
4142
if err != nil {
@@ -55,9 +56,9 @@ func ClusterToAdvancedCluster(config []byte) ([]byte, error) {
5556
resource.SetLabels(labels)
5657

5758
if resourceb.FirstMatchingBlock(nRepSpecs, nil) != nil {
58-
err = fillReplicationSpecs(resourceb)
59+
err = fillCluster(resourceb)
5960
} else {
60-
err = fillFreeTier(resourceb)
61+
err = fillFreeTierCluster(resourceb)
6162
}
6263
if err != nil {
6364
return nil, err
@@ -70,8 +71,8 @@ func ClusterToAdvancedCluster(config []byte) ([]byte, error) {
7071
return parser.Bytes(), nil
7172
}
7273

73-
// fillFreeTier is the entry point to convert clusters in free tier
74-
func fillFreeTier(resourceb *hclwrite.Body) error {
74+
// fillFreeTierCluster is the entry point to convert clusters in free tier
75+
func fillFreeTierCluster(resourceb *hclwrite.Body) error {
7576
resourceb.SetAttributeValue(nClusterType, cty.StringVal(valClusterType))
7677
config := hclwrite.NewEmptyFile()
7778
configb := config.Body()
@@ -97,73 +98,128 @@ func fillFreeTier(resourceb *hclwrite.Body) error {
9798
return nil
9899
}
99100

100-
// fillReplicationSpecs is the entry point to convert clusters with replications_specs (all but free tier)
101-
func fillReplicationSpecs(resourceb *hclwrite.Body) error {
101+
// fillCluster is the entry point to convert clusters with replications_specs (all but free tier)
102+
func fillCluster(resourceb *hclwrite.Body) error {
102103
root, errRoot := popRootAttrs(resourceb)
103104
if errRoot != nil {
104105
return errRoot
105106
}
106107
resourceb.RemoveAttribute(nNumShards) // num_shards in root is not relevant, only in replication_specs
107108
// ok to fail as cloud_backup is optional
108109
_ = hcl.MoveAttr(resourceb, resourceb, nCloudBackup, nBackupEnabled, errRepSpecs)
109-
110-
// at least one replication_specs exists here, if not it would be a free tier cluster
111-
repSpecsSrc := resourceb.FirstMatchingBlock(nRepSpecs, nil)
112-
if err := checkDynamicBlock(repSpecsSrc.Body()); err != nil {
110+
if err := fillReplicationSpecs(resourceb, root); err != nil {
113111
return err
114112
}
115-
configs, errConfigs := getRegionConfigs(repSpecsSrc, root)
116-
if errConfigs != nil {
117-
return errConfigs
118-
}
119-
repSpecs := hclwrite.NewEmptyFile()
120-
repSpecs.Body().SetAttributeRaw(nConfig, configs)
121-
resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensArraySingle(repSpecs.Body()))
122-
tags, errTags := getTagsLabelsOpt(resourceb, nTags)
123-
if errTags != nil {
124-
return errTags
125-
}
126-
if tags != nil {
127-
resourceb.SetAttributeRaw(nTags, tags)
128-
}
129-
labels, errLabels := getTagsLabelsOpt(resourceb, nLabels)
130-
if errLabels != nil {
131-
return errLabels
113+
if err := fillTagsLabelsOpt(resourceb, nTags); err != nil {
114+
return err
132115
}
133-
if labels != nil {
134-
resourceb.SetAttributeRaw(nLabels, labels)
116+
if err := fillTagsLabelsOpt(resourceb, nLabels); err != nil {
117+
return err
135118
}
136119
fillBlockOpt(resourceb, nTimeouts)
137120
fillBlockOpt(resourceb, nAdvConf)
138121
fillBlockOpt(resourceb, nBiConnector)
139122
fillBlockOpt(resourceb, nPinnedFCV)
140-
resourceb.RemoveBlock(repSpecsSrc)
141123
return nil
142124
}
143125

144-
func getRegionConfigs(repSpecsSrc *hclwrite.Block, root attrVals) (hclwrite.Tokens, error) {
126+
func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error {
127+
// at least one replication_specs exists here, if not it would be a free tier cluster
128+
var specbs []*hclwrite.Body
129+
for {
130+
var (
131+
specSrc = resourceb.FirstMatchingBlock(nRepSpecs, nil)
132+
spec = hclwrite.NewEmptyFile()
133+
specb = spec.Body()
134+
)
135+
if specSrc == nil {
136+
break
137+
}
138+
specbSrc := specSrc.Body()
139+
if err := checkDynamicBlock(specbSrc); err != nil {
140+
return err
141+
}
142+
// ok to fail as zone_name is optional
143+
_ = hcl.MoveAttr(specbSrc, specb, nZoneName, nZoneName, errRepSpecs)
144+
shards := specbSrc.GetAttribute(nNumShards)
145+
if shards == nil {
146+
return fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards)
147+
}
148+
shardsVal, err := hcl.GetAttrInt(shards, errNumShards)
149+
if err != nil {
150+
return err
151+
}
152+
if err := fillRegionConfigs(specb, specbSrc, root); err != nil {
153+
return err
154+
}
155+
for range shardsVal {
156+
specbs = append(specbs, specb)
157+
}
158+
resourceb.RemoveBlock(specSrc)
159+
}
160+
resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensArray(specbs))
161+
return nil
162+
}
163+
164+
func fillTagsLabelsOpt(resourceb *hclwrite.Body, name string) error {
165+
var (
166+
file = hclwrite.NewEmptyFile()
167+
fileb = file.Body()
168+
found = false
169+
)
170+
for {
171+
block := resourceb.FirstMatchingBlock(name, nil)
172+
if block == nil {
173+
break
174+
}
175+
key := block.Body().GetAttribute(nKey)
176+
value := block.Body().GetAttribute(nValue)
177+
if key == nil || value == nil {
178+
return fmt.Errorf("%s: %s or %s not found", name, nKey, nValue)
179+
}
180+
setKeyValue(fileb, key, value)
181+
resourceb.RemoveBlock(block)
182+
found = true
183+
}
184+
if found {
185+
resourceb.SetAttributeRaw(name, hcl.TokensObject(fileb))
186+
}
187+
return nil
188+
}
189+
190+
func fillBlockOpt(resourceb *hclwrite.Body, name string) {
191+
block := resourceb.FirstMatchingBlock(name, nil)
192+
if block == nil {
193+
return
194+
}
195+
resourceb.RemoveBlock(block)
196+
resourceb.SetAttributeRaw(name, hcl.TokensObject(block.Body()))
197+
}
198+
199+
func fillRegionConfigs(specb, specbSrc *hclwrite.Body, root attrVals) error {
145200
var configs []*hclwrite.Body
146201
for {
147-
configSrc := repSpecsSrc.Body().FirstMatchingBlock(nConfigSrc, nil)
202+
configSrc := specbSrc.FirstMatchingBlock(nConfigSrc, nil)
148203
if configSrc == nil {
149204
break
150205
}
151206
config, err := getRegionConfig(configSrc, root)
152207
if err != nil {
153-
return nil, err
208+
return err
154209
}
155210
configs = append(configs, config.Body())
156-
repSpecsSrc.Body().RemoveBlock(configSrc)
211+
specbSrc.RemoveBlock(configSrc)
157212
}
158213
if len(configs) == 0 {
159-
return nil, fmt.Errorf("%s: %s not found", errRepSpecs, nConfigSrc)
214+
return fmt.Errorf("%s: %s not found", errRepSpecs, nConfigSrc)
160215
}
161216
sort.Slice(configs, func(i, j int) bool {
162217
pi, _ := hcl.GetAttrInt(configs[i].GetAttribute(nPriority), errPriority)
163218
pj, _ := hcl.GetAttrInt(configs[j].GetAttribute(nPriority), errPriority)
164219
return pi > pj
165220
})
166-
return hcl.TokensArray(configs), nil
221+
specb.SetAttributeRaw(nConfig, hcl.TokensArray(configs))
222+
return nil
167223
}
168224

169225
func getRegionConfig(configSrc *hclwrite.Block, root attrVals) (*hclwrite.File, error) {
@@ -245,41 +301,6 @@ func getAutoScalingOpt(opt map[string]hclwrite.Tokens) hclwrite.Tokens {
245301
return hcl.TokensObject(fileb)
246302
}
247303

248-
func getTagsLabelsOpt(resourceb *hclwrite.Body, name string) (hclwrite.Tokens, error) {
249-
var (
250-
file = hclwrite.NewEmptyFile()
251-
fileb = file.Body()
252-
found = false
253-
)
254-
for {
255-
block := resourceb.FirstMatchingBlock(name, nil)
256-
if block == nil {
257-
break
258-
}
259-
key := block.Body().GetAttribute(nKey)
260-
value := block.Body().GetAttribute(nValue)
261-
if key == nil || value == nil {
262-
return nil, fmt.Errorf("%s: %s or %s not found", name, nKey, nValue)
263-
}
264-
setKeyValue(fileb, key, value)
265-
resourceb.RemoveBlock(block)
266-
found = true
267-
}
268-
if !found {
269-
return nil, nil
270-
}
271-
return hcl.TokensObject(fileb), nil
272-
}
273-
274-
func fillBlockOpt(resourceb *hclwrite.Body, name string) {
275-
block := resourceb.FirstMatchingBlock(name, nil)
276-
if block == nil {
277-
return
278-
}
279-
resourceb.RemoveBlock(block)
280-
resourceb.SetAttributeRaw(name, hcl.TokensObject(block.Body()))
281-
}
282-
283304
func checkDynamicBlock(body *hclwrite.Body) error {
284305
for _, block := range body.Blocks() {
285306
if block.Type() == "dynamic" {
@@ -297,7 +318,7 @@ func setKeyValue(body *hclwrite.Body, key, value *hclwrite.Attribute) {
297318
}
298319
} else {
299320
keyStr = strings.TrimSpace(string(key.Expr().BuildTokens(nil).Bytes()))
300-
keyStr = "(" + keyStr + ")" // wrap in parentheses so unresolved expressions can be used as attribute names
321+
keyStr = "(" + keyStr + ")" // wrap in parentheses so non-literal expressions can be used as attribute names
301322
}
302323
body.SetAttributeRaw(keyStr, value.Expr().BuildTokens(nil))
303324
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ resource "mongodbatlas_cluster" "this" {
1616
pinned_fcv {
1717
# comments in pinned_fcv are kept
1818
expiration_date = var.fcv_date
19-
version = var.fcv_date
2019
}
2120
replication_specs {
2221
num_shards = 1

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ resource "mongodbatlas_advanced_cluster" "this" {
2929
pinned_fcv = {
3030
# comments in pinned_fcv are kept
3131
expiration_date = var.fcv_date
32-
version = var.fcv_date
3332
}
3433

3534
# Generated by atlas-cli-plugin-terraform.

internal/convert/testdata/clu2adv/errors.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
"regions_config_missing_electable_nodes": "setting replication_specs: attribute electable_nodes not found",
77
"regions_config_missing_priority": "setting replication_specs: priority not found",
88
"regions_config_out_of_range_priority": "setting priority: priority is 0 but must be between 1 and 7",
9-
"regions_config_unresolved_priority": "setting priority: failed to evaluate number",
9+
"regions_config_non_literal_priority": "setting priority: failed to evaluate number",
1010
"replication_specs_unsupported_dynamic": "dynamic blocks are not supported",
11-
"regions_config_unsupported_dynamic": "dynamic blocks are not supported"
11+
"regions_config_unsupported_dynamic": "dynamic blocks are not supported",
12+
"replication_specs_non_literal_num_shards": "setting num_shards: failed to evaluate number",
13+
"replication_specs_missing_num_shards": "num_shards not found"
1214
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
resource "mongodbatlas_cluster" "multirep" {
2+
project_id = var.project_id
3+
name = "multirep"
4+
disk_size_gb = 80
5+
num_shards = 1
6+
cloud_backup = false
7+
cluster_type = "GEOSHARDED"
8+
provider_name = "AWS"
9+
provider_instance_size_name = "M10"
10+
replication_specs {
11+
zone_name = "Zone 1"
12+
num_shards = 1
13+
regions_config {
14+
region_name = "US_EAST_1"
15+
electable_nodes = 3
16+
priority = 7
17+
}
18+
}
19+
replication_specs {
20+
zone_name = "Zone 2"
21+
num_shards = 1
22+
regions_config {
23+
region_name = "US_WEST_2"
24+
electable_nodes = 3
25+
priority = 7
26+
}
27+
}
28+
}
29+
30+
resource "mongodbatlas_cluster" "geo" {
31+
project_id = "1234"
32+
name = "geo"
33+
disk_size_gb = 80
34+
num_shards = 1
35+
cloud_backup = false
36+
cluster_type = "GEOSHARDED"
37+
provider_name = "AWS"
38+
provider_instance_size_name = "M10"
39+
replication_specs {
40+
zone_name = "Zone 1"
41+
num_shards = 2
42+
regions_config {
43+
region_name = "US_EAST_1"
44+
electable_nodes = 3
45+
priority = 7
46+
read_only_nodes = 0
47+
}
48+
}
49+
replication_specs {
50+
zone_name = "Zone 2"
51+
num_shards = 3
52+
regions_config {
53+
region_name = "US_WEST_2"
54+
electable_nodes = 3
55+
priority = 7
56+
read_only_nodes = 0
57+
}
58+
}
59+
}

0 commit comments

Comments
 (0)