Skip to content
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
5eceea1
tags & labels
lantoli Aug 21, 2025
552f37b
failing region_configs test
lantoli Aug 21, 2025
8fc0daa
failing replication_specs test
lantoli Aug 21, 2025
f33ed40
implementation for basic tests
lantoli Aug 21, 2025
18fe32f
group num_shard tests
lantoli Aug 21, 2025
4192605
support mix of variable and numerical num_shards
lantoli Aug 22, 2025
abb154c
remove num_shard comment
lantoli Aug 22, 2025
751a804
simplify convert code so we don't hardcode destination attributes
lantoli Aug 22, 2025
4609386
use hcl.TokensFuncFlatten
lantoli Aug 22, 2025
3ef174b
allow numerical and variable num_shards in clu2adv
lantoli Aug 22, 2025
e2b2c9f
refactor common code to shared.go
lantoli Aug 22, 2025
658e124
allow different names
lantoli Aug 22, 2025
6d08f97
order attributes to make tests deterministic
lantoli Aug 22, 2025
aa0b30c
include tags in rep_spec test
lantoli Aug 22, 2025
31ad9bc
disk_size_gb in dynamic blocks
lantoli Aug 22, 2025
90fc849
rename test files removing basic from filename
lantoli Aug 22, 2025
bb8ec13
support all specs with dynamic blocks
lantoli Aug 22, 2025
12b8e05
terraform fmt
lantoli Aug 22, 2025
1bdacc5
reduce duplication
lantoli Aug 22, 2025
472bac3
refactor buildForExpr
lantoli Aug 22, 2025
a425f53
simplify use of TokensFuncConcat
lantoli Aug 22, 2025
4d843b9
collectBlocks
lantoli Aug 22, 2025
feeff66
some refactors
lantoli Aug 22, 2025
be5d01a
reduce long funcs
lantoli Aug 22, 2025
0ef8321
reduce empty lines
lantoli Aug 22, 2025
383396a
trailingSpace in buildForExpr
lantoli Aug 23, 2025
26fe17d
checkDynamicBlock
lantoli Aug 23, 2025
4acd041
move tags and labels funcs to shared.go
lantoli Aug 23, 2025
6255619
refactor fillReplicationSpecs
lantoli Aug 23, 2025
54645df
refactor fillReplicationSpecs
lantoli Aug 23, 2025
4a1bb60
inline back some functions in adv2v2
lantoli Aug 23, 2025
c8240d5
convertRepSpecsWithDynamicBlock
lantoli Aug 24, 2025
3ae7484
renames in clu2adv
lantoli Aug 24, 2025
0ada06a
reduce duplication in adv2v2
lantoli Aug 24, 2025
a177614
remove unneeded code
lantoli Aug 24, 2025
49f81b8
rename dynamic block helper functions
lantoli Aug 24, 2025
4f0cca7
reduce use of transformReference
lantoli Aug 25, 2025
f7921d5
inline back convertConfig
lantoli Aug 25, 2025
7b6afc3
refactor new block creation
lantoli Aug 25, 2025
07a68c4
simplify convertRepSpecs
lantoli Aug 25, 2025
65fa29e
simplify copyAttributesSorted
lantoli Aug 25, 2025
9027f4d
Improve collectBlocks
lantoli Aug 25, 2025
308d00b
make name more explicit with processNumShardsWhenSomeIsVariable
lantoli Aug 25, 2025
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
11 changes: 11 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ clean: ## Clean binary folders
test: ## Run unit tests
go test ./internal/... -timeout=30s -parallel=4 -race

.PHONY: lint-fix
lint-fix: ## Fix Go linter issues
@echo "==> Fixing linters errors..."
$(shell go env GOPATH)/bin/fieldalignment -json -fix ./...
golangci-lint run --fix

.PHONY: lint
lint: ## Check Go linter issues
@echo "==> Checking source code against linters..."
golangci-lint run

.PHONY: test-update
test-update: ## Run unit tests and update the golden files
go test ./internal/... -timeout=30s -parallel=4 -race -update
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ dynamic "replication_specs" {

### Limitations

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

Choose a reason for hiding this comment

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

nice!

- `dynamic` blocks are supported with some [limitations](./docs/guide_clu2adv_dynamic_block.md).

## Feedback
Expand Down
190 changes: 133 additions & 57 deletions internal/convert/adv2v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
// AdvancedClusterToV2 transforms all mongodbatlas_advanced_cluster resource definitions in a
// Terraform configuration file from SDKv2 schema to TPF (Terraform Plugin Framework) schema.
// All other resources and data sources are left untouched.
// TODO: Not implemented yet.
func AdvancedClusterToV2(config []byte) ([]byte, error) {
parser, err := hcl.GetParser(config)
if err != nil {
Expand All @@ -37,6 +36,9 @@ func updateResource(resource *hclwrite.Block) (bool, error) {
return false, nil
}
resourceb := resource.Body()
if errDyn := checkDynamicBlock(resourceb); errDyn != nil {
return false, errDyn
}
if hasExpectedBlocksAsAttributes(resourceb) {
return false, nil
}
Expand All @@ -58,89 +60,163 @@ func updateResource(resource *hclwrite.Block) (bool, error) {
}

func convertRepSpecs(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) error {
var repSpecs []*hclwrite.Body
for {
block := resourceb.FirstMatchingBlock(nRepSpecs, nil)
if block == nil {
break
}
resourceb.RemoveBlock(block)
d, err := convertRepSpecsWithDynamicBlock(resourceb, diskSizeGB)
if err != nil {
return err
}
if d.IsPresent() {
resourceb.RemoveBlock(d.block)
resourceb.SetAttributeRaw(nRepSpecs, d.tokens)
return nil
}
repSpecBlocks := collectBlocks(resourceb, nRepSpecs)
if len(repSpecBlocks) == 0 {
return fmt.Errorf("must have at least one replication_specs")
}
hasVariableShards := hasVariableNumShards(repSpecBlocks)
var resultTokens []hclwrite.Tokens
var resultBodies []*hclwrite.Body
for _, block := range repSpecBlocks {
blockb := block.Body()
numShardsVal := 1 // default to 1 if num_shards not present
if numShardsAttr := blockb.GetAttribute(nNumShards); numShardsAttr != nil {
var err error
if numShardsVal, err = hcl.GetAttrInt(numShardsAttr, errNumShards); err != nil {
return err
shardsAttr := blockb.GetAttribute(nNumShards)
blockb.RemoveAttribute(nNumShards)
dConfig, err := getDynamicBlock(blockb, nConfig)
if err != nil {
return err
}
if dConfig.IsPresent() {
transformReferences(dConfig.content.Body(), getResourceName(dConfig.block), nRegion)
copyAttributesSorted(dConfig.content.Body(), dConfig.content.Body().Attributes())
processAllSpecs(dConfig.content.Body(), diskSizeGB)
tokens := hcl.TokensFromExpr(buildForExpr(nRegion, hcl.GetAttrExpr(dConfig.forEach), false))
tokens = append(tokens, hcl.TokensObject(dConfig.content.Body())...)
blockb.SetAttributeRaw(nConfig, hcl.EncloseBracketsNewLines(tokens))
blockb.RemoveBlock(dConfig.block)
} else {
var configs []*hclwrite.Body
for _, configBlock := range collectBlocks(blockb, nConfig) {
configBlockb := configBlock.Body()
processAllSpecs(configBlockb, diskSizeGB)
configs = append(configs, configBlockb)
}
blockb.RemoveAttribute(nNumShards)
if len(configs) == 0 {
return fmt.Errorf("replication_specs must have at least one region_configs")
}
blockb.SetAttributeRaw(nConfig, hcl.TokensArray(configs))
}
if err := convertConfig(blockb, diskSizeGB); err != nil {
return err
if hasVariableShards {
resultTokens = append(resultTokens, processNumShards(shardsAttr, blockb))
continue
}
numShardsVal := 1 // Default to 1 if num_shards is not set
if shardsAttr != nil {
numShardsVal, _ = hcl.GetAttrInt(shardsAttr, errNumShards)
}
for range numShardsVal {
repSpecs = append(repSpecs, blockb)
resultBodies = append(resultBodies, blockb)
}
}
if len(repSpecs) == 0 {
return fmt.Errorf("must have at least one replication_specs")
if hasVariableShards {
resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensFuncConcat(resultTokens...))
} else {
resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensArray(resultBodies))
}
resourceb.SetAttributeRaw(nRepSpecs, hcl.TokensArray(repSpecs))
return nil
}

func convertConfig(repSpecs *hclwrite.Body, diskSizeGB hclwrite.Tokens) error {
var configs []*hclwrite.Body
for {
block := repSpecs.FirstMatchingBlock(nConfig, nil)
if block == nil {
break
}
repSpecs.RemoveBlock(block)
blockb := block.Body()
fillSpecOpt(blockb, nElectableSpecs, diskSizeGB)
fillSpecOpt(blockb, nReadOnlySpecs, diskSizeGB)
fillSpecOpt(blockb, nAnalyticsSpecs, diskSizeGB)
fillSpecOpt(blockb, nAutoScaling, nil) // auto_scaling doesn't need disk_size_gb
fillSpecOpt(blockb, nAnalyticsAutoScaling, nil) // analytics_auto_scaling doesn't need disk_size_gb
configs = append(configs, blockb)
func convertRepSpecsWithDynamicBlock(resourceb *hclwrite.Body, diskSizeGB hclwrite.Tokens) (dynamicBlock, error) {
dSpec, err := getDynamicBlock(resourceb, nRepSpecs)
if err != nil || !dSpec.IsPresent() {
return dynamicBlock{}, err
}
if len(configs) == 0 {
return fmt.Errorf("replication_specs must have at least one region_configs")
transformReferences(dSpec.content.Body(), nRepSpecs, nSpec)
dConfig, err := convertConfigsWithDynamicBlock(dSpec.content.Body(), diskSizeGB)
if err != nil {
return dynamicBlock{}, err
}
repSpecs.SetAttributeRaw(nConfig, hcl.TokensArray(configs))
return nil
forSpec := hcl.TokensFromExpr(buildForExpr(nSpec, hcl.GetAttrExpr(dSpec.forEach), true))
dSpec.tokens = hcl.TokensFuncFlatten(append(forSpec, dConfig.tokens...))
return dSpec, nil
}

func fillSpecOpt(resourceb *hclwrite.Body, name string, diskSizeGBTokens hclwrite.Tokens) {
block := resourceb.FirstMatchingBlock(name, nil)
if block == nil {
return
func convertConfigsWithDynamicBlock(specbSrc *hclwrite.Body, diskSizeGB hclwrite.Tokens) (dynamicBlock, error) {
d, err := getDynamicBlock(specbSrc, nConfig)
if err != nil {
return dynamicBlock{}, err
}
if diskSizeGBTokens != nil {
blockb := block.Body()
blockb.RemoveAttribute(nDiskSizeGB)
blockb.SetAttributeRaw(nDiskSizeGB, diskSizeGBTokens)
configBody := d.content.Body()
transformReferences(configBody, getResourceName(d.block), nRegion)
regionConfigBody := hclwrite.NewEmptyFile().Body()
copyAttributesSorted(regionConfigBody, configBody.Attributes())
for _, block := range configBody.Blocks() {
blockType := block.Type()
blockBody := hclwrite.NewEmptyFile().Body()
copyAttributesSorted(blockBody, block.Body().Attributes())
if diskSizeGB != nil &&
(blockType == nElectableSpecs || blockType == nReadOnlySpecs || blockType == nAnalyticsSpecs) {
blockBody.SetAttributeRaw(nDiskSizeGB, diskSizeGB)
}
regionConfigBody.SetAttributeRaw(blockType, hcl.TokensObject(blockBody))
}
fillBlockOpt(resourceb, name)
repSpecb := hclwrite.NewEmptyFile().Body()
if zoneNameAttr := specbSrc.GetAttribute(nZoneName); zoneNameAttr != nil {
repSpecb.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(
transformReference(hcl.GetAttrExpr(zoneNameAttr), nRepSpecs, nSpec)))
}
regionTokens := hcl.TokensFromExpr(buildForExpr(nRegion, fmt.Sprintf("%s.%s", nSpec, nConfig), false))
regionTokens = append(regionTokens, hcl.TokensObject(regionConfigBody)...)
repSpecb.SetAttributeRaw(nConfig, hcl.EncloseBracketsNewLines(regionTokens))
if numShardsAttr := specbSrc.GetAttribute(nNumShards); numShardsAttr != nil {
tokens := hcl.TokensFromExpr(buildForExpr("i",
fmt.Sprintf("range(%s)", transformReference(hcl.GetAttrExpr(numShardsAttr), nRepSpecs, nSpec)), false))
tokens = append(tokens, hcl.TokensObject(repSpecb)...)
return dynamicBlock{tokens: hcl.EncloseBracketsNewLines(tokens)}, nil
}
return dynamicBlock{tokens: hcl.TokensArraySingle(repSpecb)}, nil
}

// hasExpectedBlocksAsAttributes checks if any of the expected block names
// exist as attributes in the resource body. In that case conversion is not done
// as advanced cluster is not in a valid SDKv2 configuration.
func hasExpectedBlocksAsAttributes(resourceb *hclwrite.Body) bool {
expectedBlocks := []string{
nRepSpecs,
nTags,
nLabels,
nAdvConfig,
nBiConnector,
nPinnedFCV,
nTimeouts,
}
expectedBlocks := []string{nRepSpecs, nTags, nLabels, nAdvConfig, nBiConnector, nPinnedFCV, nTimeouts}
for name := range resourceb.Attributes() {
if slices.Contains(expectedBlocks, name) {
return true
}
}
return false
}

func copyAttributesSorted(targetBody *hclwrite.Body, sourceAttrs map[string]*hclwrite.Attribute) {
var names []string
for name := range sourceAttrs {
names = append(names, name)
}
slices.Sort(names)
for _, name := range names {
expr := hcl.GetAttrExpr(sourceAttrs[name])
targetBody.SetAttributeRaw(name, hcl.TokensFromExpr(expr))
}
}

func processAllSpecs(body *hclwrite.Body, diskSizeGB hclwrite.Tokens) {
fillSpecOpt(body, nElectableSpecs, diskSizeGB)
fillSpecOpt(body, nReadOnlySpecs, diskSizeGB)
fillSpecOpt(body, nAnalyticsSpecs, diskSizeGB)
fillSpecOpt(body, nAutoScaling, nil)
fillSpecOpt(body, nAnalyticsAutoScaling, nil)
}

func fillSpecOpt(resourceb *hclwrite.Body, name string, diskSizeGBTokens hclwrite.Tokens) {
block := resourceb.FirstMatchingBlock(name, nil)
if block == nil {
return
}
if diskSizeGBTokens != nil {
blockb := block.Body()
blockb.RemoveAttribute(nDiskSizeGB)
blockb.SetAttributeRaw(nDiskSizeGB, diskSizeGBTokens)
}
fillBlockOpt(resourceb, name)
}
Loading