configs: Implementation of mergeBody

mergeBody is a hcl.Body implementation that deals with our override file
merging behavior for the portions of the configuration that are not
processed until full eval time.

Mimicking the behavior of our old config merge implementation from the
"config" package, the rules here are:

- Attributes in the override body hide attributes of the same name in
  the base body.
- Any block in the override body hides all blocks with the same type name
  that appear in the base body.

This is tested by a new test for the overriding of module arguments, which
asserts the correct behavior of the merged body as part of its work.
This commit is contained in:
Martin Atkins 2018-02-06 18:30:12 -08:00
parent 7c8efe103e
commit cc38e91612
4 changed files with 158 additions and 3 deletions

View File

@ -31,15 +31,89 @@ type mergeBody struct {
var _ hcl.Body = mergeBody{}
func (b mergeBody) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) {
panic("mergeBody.Content not yet implemented")
var diags hcl.Diagnostics
oSchema := schemaForOverrides(schema)
baseContent, cDiags := b.Base.Content(schema)
diags = append(diags, cDiags...)
overrideContent, cDiags := b.Override.Content(oSchema)
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) {
panic("mergeBody.Content not yet implemented")
var diags hcl.Diagnostics
oSchema := schemaForOverrides(schema)
baseContent, baseRemain, cDiags := b.Base.PartialContent(schema)
diags = append(diags, cDiags...)
overrideContent, overrideRemain, cDiags := b.Override.PartialContent(oSchema)
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 {
overriddenBlockTypes[block.Type] = true
}
for _, block := range base.Blocks {
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) {
panic("mergeBody.JustAttributes not yet implemented")
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 {

View File

@ -3,6 +3,7 @@ package configs
import (
"testing"
"github.com/hashicorp/hcl2/gohcl"
"github.com/hashicorp/hcl2/hcl"
"github.com/zclconf/go-cty/cty"
)
@ -59,3 +60,77 @@ func TestModuleOverrideVariable(t *testing.T) {
}
assertResultDeepEqual(t, got, want)
}
func TestModuleOverrideModule(t *testing.T) {
mod, diags := testModuleFromDir("test-fixtures/valid-modules/override-module")
assertNoDiagnostics(t, diags)
if mod == nil {
t.Fatalf("module is nil")
}
if _, exists := mod.ModuleCalls["example"]; !exists {
t.Fatalf("no module 'example'")
}
if len(mod.ModuleCalls) != 1 {
t.Fatalf("wrong number of module calls in result %d; want 1", len(mod.ModuleCalls))
}
got := mod.ModuleCalls["example"]
want := &ModuleCall{
Name: "example",
SourceAddr: "./example2-a_override",
SourceAddrRange: hcl.Range{
Filename: "test-fixtures/valid-modules/override-module/a_override.tf",
Start: hcl.Pos{
Line: 3,
Column: 12,
Byte: 31,
},
End: hcl.Pos{
Line: 3,
Column: 35,
Byte: 54,
},
},
SourceSet: true,
DeclRange: hcl.Range{
Filename: "test-fixtures/valid-modules/override-module/primary.tf",
Start: hcl.Pos{
Line: 2,
Column: 1,
Byte: 1,
},
End: hcl.Pos{
Line: 2,
Column: 17,
Byte: 17,
},
},
}
// We're going to extract and nil out our hcl.Body here because DeepEqual
// is not a useful way to assert on that.
gotConfig := got.Config
got.Config = nil
assertResultDeepEqual(t, got, want)
type content struct {
Kept *string `hcl:"kept"`
Foo *string `hcl:"foo"`
New *string `hcl:"new"`
Newer *string `hcl:"newer"`
}
var gotArgs content
diags = gohcl.DecodeBody(gotConfig, nil, &gotArgs)
assertNoDiagnostics(t, diags)
wantArgs := content{
Kept: stringPtr("primary kept"),
Foo: stringPtr("a_override foo"),
New: stringPtr("b_override new"),
Newer: stringPtr("b_override newer"),
}
assertResultDeepEqual(t, gotArgs, wantArgs)
}

View File

@ -93,3 +93,7 @@ func assertResultDeepEqual(t *testing.T, got, want interface{}) bool {
}
return false
}
func stringPtr(s string) *string {
return &s
}

View File

@ -1,5 +1,7 @@
module "example" {
source = "./example2-a_override"
foo = "a_override foo"
new = "a_override new"
}