-
Notifications
You must be signed in to change notification settings - Fork 1
feat: Supports dynamic blocks in regions_config #33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 33 commits
c7857b0
72a2fbc
26d2183
cf5e3f7
771845c
5dfbede
17a6444
0f10bb9
44a4bc7
130009b
b1dc050
c5e6d5f
ae8471b
3f93627
ff3fc22
87bf0fc
b4db307
969da96
55b93fd
6186dcd
1883ff0
032c072
4684c7c
861a6c4
165256e
11314b5
b2c3161
8f6e967
0f2dfeb
876b6eb
4ab2d3a
32ca654
0d61fe7
5fa1cb7
b8258ae
f90d5a2
2bec1f9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -57,7 +57,8 @@ Given the different ways of using dynamic blocks, we recommend reviewing the out | |
|
||
#### Dynamic blocks in tags and labels | ||
|
||
You can use `dynamic` blocks for `tags` and `labels`. You can also combine the use of dynamic blocks in `tags` and `labels` with individual blocks in the same cluster definition, e.g.: | ||
You can use `dynamic` blocks for `tags` and `labels`. The plugin assumes that `for_each` has an expression which is evaluated to a `map` of strings. | ||
You can also combine the use of dynamic blocks in `tags` and `labels` with individual blocks in the same cluster definition, e.g.: | ||
```hcl | ||
tags { | ||
key = "environment" | ||
|
@@ -72,12 +73,35 @@ dynamic "tags" { | |
} | ||
``` | ||
|
||
#### Dynamic blocks in regions_config | ||
|
||
You can use `dynamic` blocks for `regions_config`. The plugin assumes that `for_each` has an expression which is evaluated to a `list` or `set` of objects. | ||
Dynamic block and individual blocks for `regions_config` are not supported at the same time in a `replication_specs`. If you need this use case, please send us feedback. | ||
This is an example of how to use dynamic blocks in `regions_config`: | ||
```hcl | ||
replication_specs { | ||
num_shards = var.replication_specs.num_shards | ||
zone_name = var.replication_specs.zone_name # only needed if you're using zones | ||
dynamic "regions_config" { | ||
for_each = var.replication_specs.regions_config | ||
content { | ||
priority = regions_config.value.priority | ||
region_name = regions_config.value.region_name | ||
electable_nodes = regions_config.value.electable_nodes | ||
read_only_nodes = regions_config.value.read_only_nodes | ||
} | ||
} | ||
} | ||
``` | ||
|
||
### Limitations | ||
|
||
- 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`. | ||
- [`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`. | ||
- [`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`. | ||
- `dynamic` blocks are currently supported only for `tags` and `labels`. **Coming soon**: support for `replication_specs` and `regions_config`. | ||
- [`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`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so great to see our limitations going away. Great stuff @lantoli There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I am not fully clear why we can support this case but not when is a regular literal replication_specs block. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. because in the literal case we physically replicate the block num_shard times so we avoid to introduce for loops and the adv_cluster is straighthforward. However in the module case as we need to create the loops and introduce some complexity, it's ok to also iterate through the priorities. We could potentially support that case if some customers are interested |
||
- `dynamic` blocks are currently supported only for `tags`, `labels` and `regions_config`. See limitations in their corresponding dynamic block sections above. **Coming soon**: support for `replication_specs`. | ||
|
||
|
||
## Feedback | ||
|
||
If you find any issues or have any suggestions, please open an [issue](https://github.com/mongodb-labs/atlas-cli-plugin-terraform/issues) in this repository. | ||
|
||
## Contributing | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -54,4 +54,5 @@ const ( | |
nDynamic = "dynamic" | ||
nForEach = "for_each" | ||
nContent = "content" | ||
nRegion = "region" | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,22 +22,22 @@ const ( | |
advClusterPlural = "mongodbatlas_advanced_clusters" | ||
valClusterType = "REPLICASET" | ||
valMaxPriority = 7 | ||
valMinPriority = 1 | ||
|
||
errFreeCluster = "free cluster (because no " + nRepSpecs + ")" | ||
errRepSpecs = "setting " + nRepSpecs | ||
errConfigs = "setting " + nConfig | ||
errPriority = "setting " + nPriority | ||
errNumShards = "setting " + nNumShards | ||
valMinPriority = 0 | ||
errFreeCluster = "free cluster (because no " + nRepSpecs + ")" | ||
errRepSpecs = "setting " + nRepSpecs | ||
errConfigs = "setting " + nConfig | ||
errPriority = "setting " + nPriority | ||
errNumShards = "setting " + nNumShards | ||
|
||
commentGeneratedBy = "Generated by atlas-cli-plugin-terraform." | ||
commentConfirmReferences = "Please confirm that all references to this resource are updated." | ||
commentConfirmReferences = "Please review the changes and confirm that references to this resource are updated." | ||
commentMovedBlock = "Moved blocks" | ||
commentRemovedOld = "Note: Remember to remove or comment out the old cluster definitions." | ||
commentPriorityFor = "Regions must be sorted by priority in descending order." | ||
) | ||
|
||
var ( | ||
dynamicBlockAllowList = []string{nTags, nLabels} | ||
dynamicBlockAllowList = []string{nTags, nLabels, nConfigSrc} | ||
) | ||
|
||
type attrVals struct { | ||
|
@@ -129,8 +129,8 @@ func fillMovedBlocks(body *hclwrite.Body, moveLabels []string) { | |
for i, moveLabel := range moveLabels { | ||
block := body.AppendNewBlock(nMoved, nil) | ||
blockb := block.Body() | ||
hcl.SetAttrExpr(blockb, nFrom, fmt.Sprintf("%s.%s", cluster, moveLabel)) | ||
hcl.SetAttrExpr(blockb, nTo, fmt.Sprintf("%s.%s", advCluster, moveLabel)) | ||
blockb.SetAttributeRaw(nFrom, hcl.TokensFromExpr(fmt.Sprintf("%s.%s", cluster, moveLabel))) | ||
blockb.SetAttributeRaw(nTo, hcl.TokensFromExpr(fmt.Sprintf("%s.%s", advCluster, moveLabel))) | ||
if i < len(moveLabels)-1 { | ||
body.AppendNewline() | ||
} | ||
|
@@ -202,9 +202,15 @@ func fillReplicationSpecs(resourceb *hclwrite.Body, root attrVals) error { | |
break | ||
} | ||
specbSrc := specSrc.Body() | ||
if err := checkDynamicBlock(specbSrc); err != nil { | ||
d, err := fillRegionConfigsDynamicBlock(specbSrc, root) | ||
if err != nil { | ||
return err | ||
} | ||
if d.IsPresent() { | ||
resourceb.RemoveBlock(specSrc) | ||
resourceb.SetAttributeRaw(nRepSpecs, d.tokens) | ||
return nil | ||
} | ||
// ok to fail as zone_name is optional | ||
_ = hcl.MoveAttr(specbSrc, specb, nZoneName, nZoneName, errRepSpecs) | ||
shards := specbSrc.GetAttribute(nNumShards) | ||
|
@@ -251,7 +257,7 @@ func fillTagsLabelsOpt(resourceb *hclwrite.Body, name string) error { | |
|
||
func extractTagsLabelsDynamicBlock(resourceb *hclwrite.Body, name string) (hclwrite.Tokens, error) { | ||
d, err := getDynamicBlock(resourceb, name) | ||
if err != nil || d.forEach == nil { | ||
if err != nil || !d.IsPresent() { | ||
return nil, err | ||
} | ||
key := d.content.Body().GetAttribute(nKey) | ||
|
@@ -306,14 +312,44 @@ func fillBlockOpt(resourceb *hclwrite.Body, name string) { | |
resourceb.SetAttributeRaw(name, hcl.TokensObject(block.Body())) | ||
} | ||
|
||
// fillRegionConfigsDynamicBlock is used for dynamic blocks in region_configs | ||
func fillRegionConfigsDynamicBlock(specbSrc *hclwrite.Body, root attrVals) (dynamicBlock, error) { | ||
d, err := getDynamicBlock(specbSrc, nConfigSrc) | ||
if err != nil || !d.IsPresent() { | ||
return dynamicBlock{}, err | ||
} | ||
repSpec := hclwrite.NewEmptyFile() | ||
repSpecb := repSpec.Body() | ||
if zoneName := hcl.GetAttrExpr(specbSrc.GetAttribute(nZoneName)); zoneName != "" { | ||
EspenAlbert marked this conversation as resolved.
Show resolved
Hide resolved
|
||
repSpecb.SetAttributeRaw(nZoneName, hcl.TokensFromExpr(zoneName)) | ||
} | ||
regionFor, err := getDynamicBlockRegionConfigsRegionArray(d, root) | ||
if err != nil { | ||
return dynamicBlock{}, err | ||
} | ||
priorityFor := hcl.TokensComment(commentPriorityFor) | ||
priorityFor = append(priorityFor, hcl.TokensFromExpr(fmt.Sprintf("for %s in range(%d, %d, -1) : ", nPriority, valMaxPriority, valMinPriority))...) | ||
priorityFor = append(priorityFor, regionFor...) | ||
repSpecb.SetAttributeRaw(nConfig, hcl.TokensFuncFlatten(priorityFor)) | ||
|
||
shards := specbSrc.GetAttribute(nNumShards) | ||
if shards == nil { | ||
return dynamicBlock{}, fmt.Errorf("%s: %s not found", errRepSpecs, nNumShards) | ||
EspenAlbert marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
tokens := hcl.TokensFromExpr(fmt.Sprintf("for i in range(%s) :", hcl.GetAttrExpr(shards))) | ||
tokens = append(tokens, hcl.EncloseBraces(repSpec.BuildTokens(nil), true)...) | ||
d.tokens = hcl.EncloseBracketsNewLines(tokens) | ||
return d, nil | ||
} | ||
|
||
func fillRegionConfigs(specb, specbSrc *hclwrite.Body, root attrVals) error { | ||
var configs []*hclwrite.Body | ||
for { | ||
configSrc := specbSrc.FirstMatchingBlock(nConfigSrc, nil) | ||
if configSrc == nil { | ||
break | ||
} | ||
config, err := getRegionConfig(configSrc, root) | ||
config, err := getRegionConfig(configSrc, root, false) | ||
if err != nil { | ||
return err | ||
} | ||
|
@@ -323,34 +359,28 @@ func fillRegionConfigs(specb, specbSrc *hclwrite.Body, root attrVals) error { | |
if len(configs) == 0 { | ||
return fmt.Errorf("%s: %s not found", errRepSpecs, nConfigSrc) | ||
} | ||
sort.Slice(configs, func(i, j int) bool { | ||
pi, _ := hcl.GetAttrInt(configs[i].GetAttribute(nPriority), errPriority) | ||
pj, _ := hcl.GetAttrInt(configs[j].GetAttribute(nPriority), errPriority) | ||
return pi > pj | ||
}) | ||
configs = sortConfigsByPriority(configs) | ||
specb.SetAttributeRaw(nConfig, hcl.TokensArray(configs)) | ||
return nil | ||
} | ||
|
||
func getRegionConfig(configSrc *hclwrite.Block, root attrVals) (*hclwrite.File, error) { | ||
func getRegionConfig(configSrc *hclwrite.Block, root attrVals, isDynamicBlock bool) (*hclwrite.File, error) { | ||
file := hclwrite.NewEmptyFile() | ||
fileb := file.Body() | ||
fileb.SetAttributeRaw(nProviderName, root.req[nProviderName]) | ||
if err := hcl.MoveAttr(configSrc.Body(), fileb, nRegionName, nRegionName, errRepSpecs); err != nil { | ||
return nil, err | ||
} | ||
if err := setPriority(fileb, configSrc.Body().GetAttribute(nPriority)); err != nil { | ||
if err := hcl.MoveAttr(configSrc.Body(), fileb, nPriority, nPriority, errRepSpecs); err != nil { | ||
return nil, err | ||
} | ||
electableSpecs, errElec := getSpecs(configSrc, nElectableNodes, root) | ||
if errElec != nil { | ||
return nil, errElec | ||
if electable, _ := getSpecs(configSrc, nElectableNodes, root, isDynamicBlock); electable != nil { | ||
fileb.SetAttributeRaw(nElectableSpecs, electable) | ||
} | ||
fileb.SetAttributeRaw(nElectableSpecs, electableSpecs) | ||
if readOnly, _ := getSpecs(configSrc, nReadOnlyNodes, root); readOnly != nil { | ||
if readOnly, _ := getSpecs(configSrc, nReadOnlyNodes, root, isDynamicBlock); readOnly != nil { | ||
fileb.SetAttributeRaw(nReadOnlySpecs, readOnly) | ||
} | ||
if analytics, _ := getSpecs(configSrc, nAnalyticsNodes, root); analytics != nil { | ||
if analytics, _ := getSpecs(configSrc, nAnalyticsNodes, root, isDynamicBlock); analytics != nil { | ||
fileb.SetAttributeRaw(nAnalyticsSpecs, analytics) | ||
} | ||
if autoScaling := getAutoScalingOpt(root.opt); autoScaling != nil { | ||
|
@@ -359,7 +389,7 @@ func getRegionConfig(configSrc *hclwrite.Block, root attrVals) (*hclwrite.File, | |
return file, nil | ||
} | ||
|
||
func getSpecs(configSrc *hclwrite.Block, countName string, root attrVals) (hclwrite.Tokens, error) { | ||
func getSpecs(configSrc *hclwrite.Block, countName string, root attrVals, isDynamicBlock bool) (hclwrite.Tokens, error) { | ||
var ( | ||
file = hclwrite.NewEmptyFile() | ||
fileb = file.Body() | ||
|
@@ -382,7 +412,11 @@ func getSpecs(configSrc *hclwrite.Block, countName string, root attrVals) (hclwr | |
if root.opt[nDiskIOPSSrc] != nil { | ||
fileb.SetAttributeRaw(nDiskIOPS, root.opt[nDiskIOPSSrc]) | ||
} | ||
return hcl.TokensObject(fileb), nil | ||
tokens := hcl.TokensObject(fileb) | ||
if isDynamicBlock { | ||
tokens = encloseDynamicBlockRegionSpec(tokens, countName) | ||
} | ||
return tokens, nil | ||
} | ||
|
||
func getAutoScalingOpt(opt map[string]hclwrite.Tokens) hclwrite.Tokens { | ||
|
@@ -440,6 +474,17 @@ func getResourceLabel(resource *hclwrite.Block) string { | |
return labels[1] | ||
} | ||
|
||
type dynamicBlock struct { | ||
block *hclwrite.Block | ||
forEach *hclwrite.Attribute | ||
content *hclwrite.Block | ||
tokens hclwrite.Tokens | ||
} | ||
|
||
func (d dynamicBlock) IsPresent() bool { | ||
return d.block != nil | ||
} | ||
|
||
func checkDynamicBlock(body *hclwrite.Body) error { | ||
for _, block := range body.Blocks() { | ||
name := getResourceName(block) | ||
|
@@ -451,12 +496,6 @@ func checkDynamicBlock(body *hclwrite.Body) error { | |
return nil | ||
} | ||
|
||
type dynamicBlock struct { | ||
block *hclwrite.Block | ||
forEach *hclwrite.Attribute | ||
content *hclwrite.Block | ||
} | ||
|
||
func getDynamicBlock(body *hclwrite.Body, name string) (dynamicBlock, error) { | ||
for _, block := range body.Blocks() { | ||
if block.Type() != nDynamic || name != getResourceName(block) { | ||
|
@@ -481,6 +520,55 @@ func replaceDynamicBlockExpr(attr *hclwrite.Attribute, blockName, attrName strin | |
return strings.ReplaceAll(expr, fmt.Sprintf("%s.%s", blockName, attrName), attrName) | ||
} | ||
|
||
func encloseDynamicBlockRegionSpec(specTokens hclwrite.Tokens, countName string) hclwrite.Tokens { | ||
tokens := hcl.TokensFromExpr(fmt.Sprintf("%s.%s > 0 ?", nRegion, countName)) | ||
tokens = append(tokens, specTokens...) | ||
return append(tokens, hcl.TokensFromExpr(": null")...) | ||
} | ||
|
||
// getDynamicBlockRegionConfigsRegionArray returns the region array for a dynamic block in replication_specs. | ||
// e.g. [ for region in var.replication_specs.regions_config : { ... } if priority == region.priority ] | ||
func getDynamicBlockRegionConfigsRegionArray(d dynamicBlock, root attrVals) (hclwrite.Tokens, error) { | ||
transformDynamicBlockReferences(d.content.Body()) | ||
priorityStr := hcl.GetAttrExpr(d.content.Body().GetAttribute(nPriority)) | ||
if priorityStr == "" { | ||
return nil, fmt.Errorf("%s: %s not found", errRepSpecs, nPriority) | ||
} | ||
region, err := getRegionConfig(d.content, root, true) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @AgustinBettati FYI region config object creation is reused in individual and dynamic block |
||
if err != nil { | ||
return nil, err | ||
} | ||
tokens := hcl.TokensFromExpr(fmt.Sprintf("for %s in %s :", nRegion, hcl.GetAttrExpr(d.forEach))) | ||
tokens = append(tokens, hcl.EncloseBraces(region.BuildTokens(nil), true)...) | ||
tokens = append(tokens, hcl.TokensFromExpr(fmt.Sprintf("if %s == %s", nPriority, priorityStr))...) | ||
return hcl.EncloseBracketsNewLines(tokens), nil | ||
} | ||
|
||
// transformDynamicBlockReferences changes value references in all attributes, e.g. regions_config.value.electable_nodes to region.electable_nodes | ||
func transformDynamicBlockReferences(configSrcb *hclwrite.Body) { | ||
for name, attr := range configSrcb.Attributes() { | ||
expr := hcl.GetAttrExpr(attr) | ||
expr = strings.ReplaceAll(expr, | ||
fmt.Sprintf("%s.%s.", nConfigSrc, nValue), | ||
fmt.Sprintf("%s.", nRegion)) | ||
configSrcb.SetAttributeRaw(name, hcl.TokensFromExpr(expr)) | ||
} | ||
} | ||
|
||
func sortConfigsByPriority(configs []*hclwrite.Body) []*hclwrite.Body { | ||
for _, config := range configs { | ||
if _, err := hcl.GetAttrInt(config.GetAttribute(nPriority), errPriority); err != nil { | ||
return configs // don't sort priorities if any is not a numerical literal | ||
} | ||
} | ||
sort.Slice(configs, func(i, j int) bool { | ||
pi, _ := hcl.GetAttrInt(configs[i].GetAttribute(nPriority), errPriority) | ||
pj, _ := hcl.GetAttrInt(configs[j].GetAttribute(nPriority), errPriority) | ||
return pi > pj | ||
}) | ||
return configs | ||
} | ||
|
||
func setKeyValue(body *hclwrite.Body, key, value *hclwrite.Attribute) { | ||
keyStr, err := hcl.GetAttrString(key, "") | ||
if err == nil { | ||
|
@@ -494,21 +582,6 @@ func setKeyValue(body *hclwrite.Body, key, value *hclwrite.Attribute) { | |
body.SetAttributeRaw(keyStr, value.Expr().BuildTokens(nil)) | ||
} | ||
|
||
func setPriority(body *hclwrite.Body, priority *hclwrite.Attribute) error { | ||
if priority == nil { | ||
return fmt.Errorf("%s: %s not found", errRepSpecs, nPriority) | ||
} | ||
valPriority, err := hcl.GetAttrInt(priority, errPriority) | ||
if err != nil { | ||
return err | ||
} | ||
if valPriority < valMinPriority || valPriority > valMaxPriority { | ||
return fmt.Errorf("%s: %s is %d but must be between %d and %d", errPriority, nPriority, valPriority, valMinPriority, valMaxPriority) | ||
} | ||
hcl.SetAttrInt(body, nPriority, valPriority) | ||
return nil | ||
} | ||
|
||
// popRootAttrs deletes the attributes common to all replication_specs/regions_config and returns them. | ||
func popRootAttrs(body *hclwrite.Body) (attrVals, error) { | ||
var ( | ||
|
Uh oh!
There was an error while loading. Please reload this page.