diff --git a/configs/module_merge.go b/configs/module_merge.go index 8eb257a61..ce3bcd4b7 100644 --- a/configs/module_merge.go +++ b/configs/module_merge.go @@ -28,7 +28,7 @@ func (p *Provider) merge(op *Provider) hcl.Diagnostics { p.Version = op.Version } - p.Config = mergeBodies(p.Config, op.Config) + p.Config = MergeBodies(p.Config, op.Config) return diags } @@ -172,7 +172,7 @@ func (mc *ModuleCall) merge(omc *ModuleCall) hcl.Diagnostics { mc.Version = omc.Version } - mc.Config = mergeBodies(mc.Config, omc.Config) + mc.Config = MergeBodies(mc.Config, omc.Config) // We don't allow depends_on to be overridden because that is likely to // cause confusing misbehavior. @@ -218,7 +218,7 @@ func (r *ManagedResource) merge(or *ManagedResource) hcl.Diagnostics { r.Provisioners = or.Provisioners } - r.Config = mergeBodies(r.Config, or.Config) + r.Config = MergeBodies(r.Config, or.Config) // We don't allow depends_on to be overridden because that is likely to // cause confusing misbehavior. @@ -247,7 +247,7 @@ func (r *DataResource) merge(or *DataResource) hcl.Diagnostics { r.ProviderConfigRef = or.ProviderConfigRef } - r.Config = mergeBodies(r.Config, or.Config) + r.Config = MergeBodies(r.Config, or.Config) // We don't allow depends_on to be overridden because that is likely to // cause confusing misbehavior. diff --git a/configs/module_merge_body.go b/configs/module_merge_body.go index b1308e976..bb40e2683 100644 --- a/configs/module_merge_body.go +++ b/configs/module_merge_body.go @@ -4,7 +4,15 @@ import ( "github.com/hashicorp/hcl2/hcl" ) -func mergeBodies(base, override hcl.Body) hcl.Body { +// 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, @@ -55,7 +63,7 @@ func (b mergeBody) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl content := b.prepareContent(baseContent, overrideContent) - remain := mergeBodies(baseRemain, overrideRemain) + remain := MergeBodies(baseRemain, overrideRemain) return content, remain, diags } diff --git a/configs/synth_body.go b/configs/synth_body.go new file mode 100644 index 000000000..3ae1bff6a --- /dev/null +++ b/configs/synth_body.go @@ -0,0 +1,118 @@ +package configs + +import ( + "fmt" + + "github.com/hashicorp/hcl2/hcl" + "github.com/hashicorp/hcl2/hcl/hclsyntax" + "github.com/zclconf/go-cty/cty" +) + +// SynthBody produces a synthetic hcl.Body that behaves as if it had attributes +// corresponding to the elements given in the values map. +// +// This is useful in situations where, for example, values provided on the +// command line can override values given in configuration, using MergeBodies. +// +// The given filename is used in case any diagnostics are returned. Since +// the created body is synthetic, it is likely that this will not be a "real" +// filename. For example, if from a command line argument it could be +// a representation of that argument's name, such as "-var=...". +func SynthBody(filename string, values map[string]cty.Value) hcl.Body { + return synthBody{ + Filename: filename, + Values: values, + } +} + +type synthBody struct { + Filename string + Values map[string]cty.Value +} + +func (b synthBody) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) { + content, remain, diags := b.PartialContent(schema) + remainS := remain.(synthBody) + for name := range remainS.Values { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unsupported attribute", + Detail: fmt.Sprintf("An attribute named %q is not expected here.", name), + Subject: b.synthRange().Ptr(), + }) + } + return content, diags +} + +func (b synthBody) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) { + var diags hcl.Diagnostics + content := &hcl.BodyContent{ + Attributes: make(hcl.Attributes), + MissingItemRange: b.synthRange(), + } + + remainValues := make(map[string]cty.Value) + for attrName, val := range b.Values { + remainValues[attrName] = val + } + + for _, attrS := range schema.Attributes { + delete(remainValues, attrS.Name) + val, defined := b.Values[attrS.Name] + if !defined { + if attrS.Required { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing required attribute", + Detail: fmt.Sprintf("The attribute %q is required, but no definition was found.", attrS.Name), + Subject: b.synthRange().Ptr(), + }) + } + continue + } + content.Attributes[attrS.Name] = b.synthAttribute(attrS.Name, val) + } + + // We just ignore blocks altogether, because this body type never has + // nested blocks. + + remain := synthBody{ + Filename: b.Filename, + Values: remainValues, + } + + return content, remain, diags +} + +func (b synthBody) JustAttributes() (hcl.Attributes, hcl.Diagnostics) { + ret := make(hcl.Attributes) + for name, val := range b.Values { + ret[name] = b.synthAttribute(name, val) + } + return ret, nil +} + +func (b synthBody) MissingItemRange() hcl.Range { + return b.synthRange() +} + +func (b synthBody) synthAttribute(name string, val cty.Value) *hcl.Attribute { + rng := b.synthRange() + return &hcl.Attribute{ + Name: name, + Expr: &hclsyntax.LiteralValueExpr{ + Val: val, + SrcRange: rng, + }, + NameRange: rng, + Range: rng, + } +} + +func (b synthBody) synthRange() hcl.Range { + return hcl.Range{ + Filename: b.Filename, + Start: hcl.Pos{Line: 1, Column: 1}, + End: hcl.Pos{Line: 1, Column: 1}, + } +} diff --git a/configs/synth_body_test.go b/configs/synth_body_test.go new file mode 100644 index 000000000..692a39404 --- /dev/null +++ b/configs/synth_body_test.go @@ -0,0 +1,65 @@ +package configs + +import ( + "testing" + + "github.com/hashicorp/hcl2/hcl" + "github.com/zclconf/go-cty/cty" +) + +func TestSynthBodyContent(t *testing.T) { + tests := map[string]struct { + Values map[string]cty.Value + Schema *hcl.BodySchema + DiagCount int + }{ + "empty": { + Values: map[string]cty.Value{}, + Schema: &hcl.BodySchema{}, + DiagCount: 0, + }, + "missing required attribute": { + Values: map[string]cty.Value{}, + Schema: &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "nonexist", + Required: true, + }, + }, + }, + DiagCount: 1, // missing required attribute + }, + "missing optional attribute": { + Values: map[string]cty.Value{}, + Schema: &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "nonexist", + }, + }, + }, + DiagCount: 0, + }, + "extraneous attribute": { + Values: map[string]cty.Value{ + "foo": cty.StringVal("unwanted"), + }, + Schema: &hcl.BodySchema{}, + DiagCount: 1, // unsupported attribute + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + body := SynthBody("synth", test.Values) + _, diags := body.Content(test.Schema) + if got, want := len(diags), test.DiagCount; got != want { + t.Errorf("wrong number of diagnostics %d; want %d", got, want) + for _, diag := range diags { + t.Logf("- %s", diag) + } + } + }) + } +}