Skip to content

Commit 8fec80a

Browse files
lantoliCopilot
andauthored
feat: Support dynamic blocks in adv2v2 command (#67)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 97d04dc commit 8fec80a

28 files changed

+1920
-354
lines changed

Makefile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@ clean: ## Clean binary folders
2727
test: ## Run unit tests
2828
go test ./internal/... -timeout=30s -parallel=4 -race
2929

30+
.PHONY: lint-fix
31+
lint-fix: ## Fix Go linter issues
32+
@echo "==> Fixing linters errors..."
33+
$(shell go env GOPATH)/bin/fieldalignment -json -fix ./...
34+
golangci-lint run --fix
35+
36+
.PHONY: lint
37+
lint: ## Check Go linter issues
38+
@echo "==> Checking source code against linters..."
39+
golangci-lint run
40+
3041
.PHONY: test-update
3142
test-update: ## Run unit tests and update the golden files
3243
go test ./internal/... -timeout=30s -parallel=4 -race -update

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,6 @@ dynamic "replication_specs" {
120120

121121
### Limitations
122122

123-
- [`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`. This limitation doesn't apply if you're using `dynamic` blocks in `regions_config` or `replication_specs`.
124123
- `dynamic` blocks are supported with some [limitations](./docs/guide_clu2adv_dynamic_block.md).
125124

126125
## Feedback

internal/convert/adv2v2.go

Lines changed: 133 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
// AdvancedClusterToV2 transforms all mongodbatlas_advanced_cluster resource definitions in a
1212
// Terraform configuration file from SDKv2 schema to TPF (Terraform Plugin Framework) schema.
1313
// All other resources and data sources are left untouched.
14-
// TODO: Not implemented yet.
1514
func AdvancedClusterToV2(config []byte) ([]byte, error) {
1615
parser, err := hcl.GetParser(config)
1716
if err != nil {
@@ -37,6 +36,9 @@ func updateResource(resource *hclwrite.Block) (bool, error) {
3736
return false, nil
3837
}
3938
resourceb := resource.Body()
39+
if errDyn := checkDynamicBlock(resourceb); errDyn != nil {
40+
return false, errDyn
41+
}
4042
if hasExpectedBlocksAsAttributes(resourceb) {
4143
return false, nil
4244
}
@@ -58,89 +60,163 @@ func updateResource(resource *hclwrite.Block) (bool, error) {
5860
}
5961

6062
func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error {
61-
var repSpecs []*hclwrite.Body
62-
for {
63-
block := resourceb.FirstMatchingBlock(nRepSpecs, nil)
64-
if block == nil {
65-
break
66-
}
67-
resourceb.RemoveBlock(block)
63+
d, err := convertRepSpecsWithDynamicBlock(resourceb, diskSizeGB)
64+
if err != nil {
65+
return err
66+
}
67+
if d.IsPresent() {
68+
resourceb.RemoveBlock(d.block)
69+
resourceb.SetAttributeRaw(nRepSpecs, d.tokens)
70+
return nil
71+
}
72+
repSpecBlocks := collectBlocks(resourceb, nRepSpecs)
73+
if len(repSpecBlocks) == 0 {
74+
return fmt.Errorf("must have at least one replication_specs")
75+
}
76+
hasVariableShards := hasVariableNumShards(repSpecBlocks)
77+
var resultTokens []hclwrite.Tokens
78+
var resultBodies []*hclwrite.Body
79+
for _, block := range repSpecBlocks {
6880
blockb := block.Body()
69-
numShardsVal := 1 // default to 1 if num_shards not present
70-
if numShardsAttr := blockb.GetAttribute(nNumShards); numShardsAttr != nil {
71-
var err error
72-
if numShardsVal, err = hcl.GetAttrInt(numShardsAttr, errNumShards); err != nil {
73-
return err
81+
shardsAttr := blockb.GetAttribute(nNumShards)
82+
blockb.RemoveAttribute(nNumShards)
83+
dConfig, err := getDynamicBlock(blockb, nConfig)
84+
if err != nil {
85+
return err
86+
}
87+
if dConfig.IsPresent() {
88+
transformReferences(dConfig.content.Body(), getResourceName(dConfig.block), nRegion)
89+
copyAttributesSorted(dConfig.content.Body(), dConfig.content.Body().Attributes())
90+
processAllSpecs(dConfig.content.Body(), diskSizeGB)
91+
tokens := hcl.TokensFromExpr(buildForExpr(nRegion, hcl.GetAttrExpr(dConfig.forEach), false))
92+
tokens = append(tokens, hcl.TokensObject(dConfig.content.Body())...)
93+
blockb.SetAttributeRaw(nConfig, hcl.EncloseBracketsNewLines(tokens))
94+
blockb.RemoveBlock(dConfig.block)
95+
} else {
96+
var configs []*hclwrite.Body
97+
for _, configBlock := range collectBlocks(blockb, nConfig) {
98+
configBlockb := configBlock.Body()
99+
processAllSpecs(configBlockb, diskSizeGB)
100+
configs = append(configs, configBlockb)
74101
}
75-
blockb.RemoveAttribute(nNumShards)
102+
if len(configs) == 0 {
103+
return fmt.Errorf("replication_specs must have at least one region_configs")
104+
}
105+
blockb.SetAttributeRaw(nConfig, hcl.TokensArray(configs))
76106
}
77-
if err := convertConfig(blockb, diskSizeGB); err != nil {
78-
return err
107+
if hasVariableShards {
108+
resultTokens = append(resultTokens, processNumShardsWhenSomeIsVariable(shardsAttr, blockb))
109+
continue
110+
}
111+
numShardsVal := 1 // Default to 1 if num_shards is not set
112+
if shardsAttr != nil {
113+
numShardsVal, _ = hcl.GetAttrInt(shardsAttr, errNumShards)
79114
}
80115
for range numShardsVal {
81-
repSpecs = append(repSpecs, blockb)
116+
resultBodies = append(resultBodies, blockb)
82117
}
83118
}
84-
if len(repSpecs) == 0 {
85-
return fmt.Errorf("must have at least one replication_specs")
119+
if hasVariableShards {
120+
resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensFuncConcat(resultTokens...))
121+
} else {
122+
resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensArray(resultBodies))
86123
}
87-
resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensArray(repSpecs))
88124
return nil
89125
}
90126

91-
func convertConfig(repSpecs *hclwrite.Body, diskSizeGB hclwrite.Tokens) error {
92-
var configs []*hclwrite.Body
93-
for {
94-
block := repSpecs.FirstMatchingBlock(nConfig, nil)
95-
if block == nil {
96-
break
97-
}
98-
repSpecs.RemoveBlock(block)
99-
blockb := block.Body()
100-
fillSpecOpt(blockb, nElectableSpecs, diskSizeGB)
101-
fillSpecOpt(blockb, nReadOnlySpecs, diskSizeGB)
102-
fillSpecOpt(blockb, nAnalyticsSpecs, diskSizeGB)
103-
fillSpecOpt(blockb, nAutoScaling, nil) // auto_scaling doesn't need disk_size_gb
104-
fillSpecOpt(blockb, nAnalyticsAutoScaling, nil) // analytics_auto_scaling doesn't need disk_size_gb
105-
configs = append(configs, blockb)
127+
func convertRepSpecsWithDynamicBlock(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) (dynamicBlock, error) {
128+
dSpec, err := getDynamicBlock(resourceb, nRepSpecs)
129+
if err != nil || !dSpec.IsPresent() {
130+
return dynamicBlock{}, err
106131
}
107-
if len(configs) == 0 {
108-
return fmt.Errorf("replication_specs must have at least one region_configs")
132+
transformReferences(dSpec.content.Body(), nRepSpecs, nSpec)
133+
dConfig, err := convertConfigsWithDynamicBlock(dSpec.content.Body(), diskSizeGB)
134+
if err != nil {
135+
return dynamicBlock{}, err
109136
}
110-
repSpecs.SetAttributeRaw(nConfig, hcl.TokensArray(configs))
111-
return nil
137+
forSpec := hcl.TokensFromExpr(buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach), true))
138+
dSpec.tokens = hcl.TokensFuncFlatten(append(forSpec, dConfig.tokens...))
139+
return dSpec, nil
112140
}
113141

114-
func fillSpecOpt(resourceb *hclwrite.Body, name string, diskSizeGBTokens hclwrite.Tokens) {
115-
block := resourceb.FirstMatchingBlock(name, nil)
116-
if block == nil {
117-
return
142+
func convertConfigsWithDynamicBlock(specbSrc *hclwrite.Body, diskSizeGB hclwrite.Tokens) (dynamicBlock, error) {
143+
d, err := getDynamicBlock(specbSrc, nConfig)
144+
if err != nil {
145+
return dynamicBlock{}, err
118146
}
119-
if diskSizeGBTokens != nil {
120-
blockb := block.Body()
121-
blockb.RemoveAttribute(nDiskSizeGB)
122-
blockb.SetAttributeRaw(nDiskSizeGB, diskSizeGBTokens)
147+
configBody := d.content.Body()
148+
transformReferences(configBody, getResourceName(d.block), nRegion)
149+
regionConfigBody := hclwrite.NewEmptyFile().Body()
150+
copyAttributesSorted(regionConfigBody, configBody.Attributes())
151+
for _, block := range configBody.Blocks() {
152+
blockType := block.Type()
153+
blockBody := hclwrite.NewEmptyFile().Body()
154+
copyAttributesSorted(blockBody, block.Body().Attributes())
155+
if diskSizeGB != nil &&
156+
(blockType == nElectableSpecs || blockType == nReadOnlySpecs || blockType == nAnalyticsSpecs) {
157+
blockBody.SetAttributeRaw(nDiskSizeGB, diskSizeGB)
158+
}
159+
regionConfigBody.SetAttributeRaw(blockType, hcl.TokensObject(blockBody))
123160
}
124-
fillBlockOpt(resourceb, name)
161+
repSpecb := hclwrite.NewEmptyFile().Body()
162+
if zoneNameAttr := specbSrc.GetAttribute(nZoneName); zoneNameAttr != nil {
163+
repSpecb.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(
164+
transformReference(hcl.GetAttrExpr(zoneNameAttr), nRepSpecs, nSpec)))
165+
}
166+
regionTokens := hcl.TokensFromExpr(buildForExpr(nRegion, fmt.Sprintf("%s.%s", nSpec, nConfig), false))
167+
regionTokens = append(regionTokens, hcl.TokensObject(regionConfigBody)...)
168+
repSpecb.SetAttributeRaw(nConfig, hcl.EncloseBracketsNewLines(regionTokens))
169+
if numShardsAttr := specbSrc.GetAttribute(nNumShards); numShardsAttr != nil {
170+
tokens := hcl.TokensFromExpr(buildForExpr("i",
171+
fmt.Sprintf("range(%s)", transformReference(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec)), false))
172+
tokens = append(tokens, hcl.TokensObject(repSpecb)...)
173+
return dynamicBlock{tokens: hcl.EncloseBracketsNewLines(tokens)}, nil
174+
}
175+
return dynamicBlock{tokens: hcl.TokensArraySingle(repSpecb)}, nil
125176
}
126177

127178
// hasExpectedBlocksAsAttributes checks if any of the expected block names
128179
// exist as attributes in the resource body. In that case conversion is not done
129180
// as advanced cluster is not in a valid SDKv2 configuration.
130181
func hasExpectedBlocksAsAttributes(resourceb *hclwrite.Body) bool {
131-
expectedBlocks := []string{
132-
nRepSpecs,
133-
nTags,
134-
nLabels,
135-
nAdvConfig,
136-
nBiConnector,
137-
nPinnedFCV,
138-
nTimeouts,
139-
}
182+
expectedBlocks := []string{nRepSpecs, nTags, nLabels, nAdvConfig, nBiConnector, nPinnedFCV, nTimeouts}
140183
for name := range resourceb.Attributes() {
141184
if slices.Contains(expectedBlocks, name) {
142185
return true
143186
}
144187
}
145188
return false
146189
}
190+
191+
func copyAttributesSorted(targetBody *hclwrite.Body, sourceAttrs map[string]*hclwrite.Attribute) {
192+
var names []string
193+
for name := range sourceAttrs {
194+
names = append(names, name)
195+
}
196+
slices.Sort(names)
197+
for _, name := range names {
198+
expr := hcl.GetAttrExpr(sourceAttrs[name])
199+
targetBody.SetAttributeRaw(name, hcl.TokensFromExpr(expr))
200+
}
201+
}
202+
203+
func processAllSpecs(body *hclwrite.Body, diskSizeGB hclwrite.Tokens) {
204+
fillSpecOpt(body, nElectableSpecs, diskSizeGB)
205+
fillSpecOpt(body, nReadOnlySpecs, diskSizeGB)
206+
fillSpecOpt(body, nAnalyticsSpecs, diskSizeGB)
207+
fillSpecOpt(body, nAutoScaling, nil)
208+
fillSpecOpt(body, nAnalyticsAutoScaling, nil)
209+
}
210+
211+
func fillSpecOpt(resourceb *hclwrite.Body, name string, diskSizeGBTokens hclwrite.Tokens) {
212+
block := resourceb.FirstMatchingBlock(name, nil)
213+
if block == nil {
214+
return
215+
}
216+
if diskSizeGBTokens != nil {
217+
blockb := block.Body()
218+
blockb.RemoveAttribute(nDiskSizeGB)
219+
blockb.SetAttributeRaw(nDiskSizeGB, diskSizeGBTokens)
220+
}
221+
fillBlockOpt(resourceb, name)
222+
}

0 commit comments

Comments
 (0)