allow path and terraform in self-block eval

We can insert the terraform and path values into EvalSelfBlock, since
these are static and always known during evaluation.
This commit is contained in:
James Bardin 2020-11-02 13:55:33 -05:00
parent a413fa7425
commit 7c4d00c04c
2 changed files with 176 additions and 2 deletions

View File

@ -72,8 +72,13 @@ func (s *Scope) EvalBlock(body hcl.Body, schema *configschema.Block) (cty.Value,
// EvalSelfBlock evaluates the given body only within the scope of the provided
// object and instance key data. References to the object must use self, and the
// key data will only contain count.index or each.key.
// key data will only contain count.index or each.key. The static values for
// terraform and path will also be available in this context.
func (s *Scope) EvalSelfBlock(body hcl.Body, self cty.Value, schema *configschema.Block, keyData instances.RepetitionData) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
spec := schema.DecoderSpec()
vals := make(map[string]cty.Value)
vals["self"] = self
@ -88,12 +93,55 @@ func (s *Scope) EvalSelfBlock(body hcl.Body, self cty.Value, schema *configschem
})
}
refs, refDiags := References(hcldec.Variables(body, spec))
diags = diags.Append(refDiags)
terraformAttrs := map[string]cty.Value{}
pathAttrs := map[string]cty.Value{}
// We could always load the static values for Path and Terraform values,
// but we want to parse the references so that we can get source ranges for
// user diagnostics.
for _, ref := range refs {
// we already loaded the self value
if ref.Subject == addrs.Self {
continue
}
switch subj := ref.Subject.(type) {
case addrs.PathAttr:
val, valDiags := normalizeRefValue(s.Data.GetPathAttr(subj, ref.SourceRange))
diags = diags.Append(valDiags)
pathAttrs[subj.Name] = val
case addrs.TerraformAttr:
val, valDiags := normalizeRefValue(s.Data.GetTerraformAttr(subj, ref.SourceRange))
diags = diags.Append(valDiags)
terraformAttrs[subj.Name] = val
case addrs.CountAttr, addrs.ForEachAttr:
// each and count have already been handled.
default:
// This should have been caught in validation, but point the user
// to the correct location in case something slipped through.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Invalid reference`,
Detail: fmt.Sprintf("The reference to %q is not valid in this context", ref.Subject),
Subject: ref.SourceRange.ToHCL().Ptr(),
})
}
}
vals["path"] = cty.ObjectVal(pathAttrs)
vals["terraform"] = cty.ObjectVal(terraformAttrs)
ctx := &hcl.EvalContext{
Variables: vals,
Functions: s.Functions(),
}
var diags tfdiags.Diagnostics
val, decDiags := hcldec.Decode(body, schema.DecoderSpec(), ctx)
diags = diags.Append(decDiags)
return val, diags

View File

@ -7,6 +7,7 @@ import (
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/instances"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
@ -642,3 +643,128 @@ func formattedJSONValue(val cty.Value) string {
json.Indent(&buf, j, "", " ")
return buf.String()
}
func TestScopeEvalSelfBlock(t *testing.T) {
data := &dataForTests{
PathAttrs: map[string]cty.Value{
"module": cty.StringVal("foo/bar"),
"cwd": cty.StringVal("/home/foo/bar"),
"root": cty.StringVal("/home/foo"),
},
TerraformAttrs: map[string]cty.Value{
"workspace": cty.StringVal("default"),
},
}
schema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"attr": {
Type: cty.String,
},
"num": {
Type: cty.Number,
},
},
}
tests := []struct {
Config string
Self cty.Value
KeyData instances.RepetitionData
Want map[string]cty.Value
}{
{
Config: `attr = self.foo`,
Self: cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("bar"),
}),
KeyData: instances.RepetitionData{
CountIndex: cty.NumberIntVal(0),
},
Want: map[string]cty.Value{
"attr": cty.StringVal("bar"),
"num": cty.NullVal(cty.Number),
},
},
{
Config: `num = count.index`,
KeyData: instances.RepetitionData{
CountIndex: cty.NumberIntVal(0),
},
Want: map[string]cty.Value{
"attr": cty.NullVal(cty.String),
"num": cty.NumberIntVal(0),
},
},
{
Config: `attr = each.key`,
KeyData: instances.RepetitionData{
EachKey: cty.StringVal("a"),
},
Want: map[string]cty.Value{
"attr": cty.StringVal("a"),
"num": cty.NullVal(cty.Number),
},
},
{
Config: `attr = path.cwd`,
Want: map[string]cty.Value{
"attr": cty.StringVal("/home/foo/bar"),
"num": cty.NullVal(cty.Number),
},
},
{
Config: `attr = path.module`,
Want: map[string]cty.Value{
"attr": cty.StringVal("foo/bar"),
"num": cty.NullVal(cty.Number),
},
},
{
Config: `attr = path.root`,
Want: map[string]cty.Value{
"attr": cty.StringVal("/home/foo"),
"num": cty.NullVal(cty.Number),
},
},
{
Config: `attr = terraform.workspace`,
Want: map[string]cty.Value{
"attr": cty.StringVal("default"),
"num": cty.NullVal(cty.Number),
},
},
}
for _, test := range tests {
t.Run(test.Config, func(t *testing.T) {
file, parseDiags := hclsyntax.ParseConfig([]byte(test.Config), "", hcl.Pos{Line: 1, Column: 1})
if len(parseDiags) != 0 {
t.Errorf("unexpected diagnostics during parse")
for _, diag := range parseDiags {
t.Errorf("- %s", diag)
}
return
}
body := file.Body
scope := &Scope{
Data: data,
}
gotVal, ctxDiags := scope.EvalSelfBlock(body, test.Self, schema, test.KeyData)
if ctxDiags.HasErrors() {
t.Fatal(ctxDiags.Err())
}
wantVal := cty.ObjectVal(test.Want)
if !gotVal.RawEquals(wantVal) {
t.Errorf(
"wrong result\nexpr: %s\ngot: %#v\nwant: %#v",
test.Config, gotVal, wantVal,
)
}
})
}
}