opentofu/configs/module_merge_body.go
Martin Atkins bb118c37a2 configs: Handle "dynamic" blocks as special during override merging
Previously we were treating "dynamic" blocks in configuration the same as
any other block type when merging config bodies, so that dynamic blocks
in the override would override any dynamic blocks present in the base,
without considering the dynamic block type.

It's more useful and intuitive for us to treat dynamic blocks as if they
are instances of their given block type for the purpose of overriding.
That means a foo block can be overridden by a dynamic "foo" block and
vice-versa, and dynamic blocks of different types do not interact at all
during overriding.

This requires us to recognize dynamic blocks and treat them specially
during decoding of a merged body. We leave them unexpanded here because
this package is not responsible for dynamic block expansion (that happens
in the sibling "lang" package) but we do decode them enough to recognize
their labels so we can treat them as if they were blocks of the labelled
type.
2019-04-16 06:58:45 -07:00

144 lines
4.6 KiB
Go

package configs
import (
"github.com/hashicorp/hcl2/hcl"
)
// MergeBodies creates a new HCL body that contains a combination of the
// given base and override bodies. Attributes and blocks defined in the
// override body take precedence over those of the same name defined in
// the base body.
//
// If any block of a particular type appears in "override" then it will
// replace _all_ of the blocks of the same type in "base" in the new
// body.
func MergeBodies(base, override hcl.Body) hcl.Body {
return mergeBody{
Base: base,
Override: override,
}
}
// mergeBody is a hcl.Body implementation that wraps a pair of other bodies
// and allows attributes and blocks within the override to take precedence
// over those defined in the base body.
//
// This is used to deal with dynamically-processed bodies in Module.mergeFile.
// It uses a shallow-only merging strategy where direct attributes defined
// in Override will override attributes of the same name in Base, while any
// blocks defined in Override will hide all blocks of the same type in Base.
//
// This cannot possibly "do the right thing" in all cases, because we don't
// have enough information about user intent. However, this behavior is intended
// to be reasonable for simple overriding use-cases.
type mergeBody struct {
Base hcl.Body
Override hcl.Body
}
var _ hcl.Body = mergeBody{}
func (b mergeBody) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) {
var diags hcl.Diagnostics
baseSchema := schemaWithDynamic(schema)
overrideSchema := schemaWithDynamic(schemaForOverrides(schema))
baseContent, _, cDiags := b.Base.PartialContent(baseSchema)
diags = append(diags, cDiags...)
overrideContent, _, cDiags := b.Override.PartialContent(overrideSchema)
diags = append(diags, cDiags...)
content := b.prepareContent(baseContent, overrideContent)
return content, diags
}
func (b mergeBody) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) {
var diags hcl.Diagnostics
baseSchema := schemaWithDynamic(schema)
overrideSchema := schemaWithDynamic(schemaForOverrides(schema))
baseContent, baseRemain, cDiags := b.Base.PartialContent(baseSchema)
diags = append(diags, cDiags...)
overrideContent, overrideRemain, cDiags := b.Override.PartialContent(overrideSchema)
diags = append(diags, cDiags...)
content := b.prepareContent(baseContent, overrideContent)
remain := MergeBodies(baseRemain, overrideRemain)
return content, remain, diags
}
func (b mergeBody) prepareContent(base *hcl.BodyContent, override *hcl.BodyContent) *hcl.BodyContent {
content := &hcl.BodyContent{
Attributes: make(hcl.Attributes),
}
// For attributes we just assign from each map in turn and let the override
// map clobber any matching entries from base.
for k, a := range base.Attributes {
content.Attributes[k] = a
}
for k, a := range override.Attributes {
content.Attributes[k] = a
}
// Things are a little more interesting for blocks because they arrive
// as a flat list. Our merging semantics call for us to suppress blocks
// from base if at least one block of the same type appears in override.
// We explicitly do not try to correlate and deeply merge nested blocks,
// since we don't have enough context here to infer user intent.
overriddenBlockTypes := make(map[string]bool)
for _, block := range override.Blocks {
if block.Type == "dynamic" {
overriddenBlockTypes[block.Labels[0]] = true
continue
}
overriddenBlockTypes[block.Type] = true
}
for _, block := range base.Blocks {
// We skip over dynamic blocks whose type label is an overridden type
// but note that below we do still leave them as dynamic blocks in
// the result because expanding the dynamic blocks that are left is
// done much later during the core graph walks, where we can safely
// evaluate the expressions.
if block.Type == "dynamic" && overriddenBlockTypes[block.Labels[0]] {
continue
}
if overriddenBlockTypes[block.Type] {
continue
}
content.Blocks = append(content.Blocks, block)
}
for _, block := range override.Blocks {
content.Blocks = append(content.Blocks, block)
}
return content
}
func (b mergeBody) JustAttributes() (hcl.Attributes, hcl.Diagnostics) {
var diags hcl.Diagnostics
ret := make(hcl.Attributes)
baseAttrs, aDiags := b.Base.JustAttributes()
diags = append(diags, aDiags...)
overrideAttrs, aDiags := b.Override.JustAttributes()
diags = append(diags, aDiags...)
for k, a := range baseAttrs {
ret[k] = a
}
for k, a := range overrideAttrs {
ret[k] = a
}
return ret, diags
}
func (b mergeBody) MissingItemRange() hcl.Range {
return b.Base.MissingItemRange()
}