From 7c4d00c04cd89f947d7bfbd8d5093d589b5f4208 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Mon, 2 Nov 2020 13:55:33 -0500 Subject: [PATCH] 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. --- lang/eval.go | 52 ++++++++++++++++++- lang/eval_test.go | 126 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 2 deletions(-) diff --git a/lang/eval.go b/lang/eval.go index f3369069da..c8a22e50cb 100644 --- a/lang/eval.go +++ b/lang/eval.go @@ -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 diff --git a/lang/eval_test.go b/lang/eval_test.go index f9ee894d61..0ddcef193f 100644 --- a/lang/eval_test.go +++ b/lang/eval_test.go @@ -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, + ) + } + }) + } +}