evaluate replace_triggered_by expressions

Evaluate the expressions stored in replace_triggered_by into the
*addrs.Reference needed to lookup changes in the plan.
This commit is contained in:
James Bardin 2022-04-08 15:24:40 -04:00
parent 6eb3264d1a
commit 8b4c89bdaf
2 changed files with 237 additions and 0 deletions

View File

@ -0,0 +1,143 @@
package terraform
import (
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/instances"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
func evalReplaceTriggeredByExpr(expr hcl.Expression, keyData instances.RepetitionData) (*addrs.Reference, tfdiags.Diagnostics) {
var ref *addrs.Reference
var diags tfdiags.Diagnostics
traversal, diags := triggersExprToTraversal(expr, keyData)
if diags.HasErrors() {
return nil, diags
}
// We now have a static traversal, so we can just turn it into an addrs.Reference.
ref, ds := addrs.ParseRef(traversal)
diags = diags.Append(ds)
return ref, diags
}
// trggersExprToTraversal takes an hcl expression limited to the syntax allowed
// in replace_triggered_by, and converts it to a static traversal. The
// RepetitionData contains the data necessary to evaluate the only allowed
// variables in the expression, count.index and each.key.
func triggersExprToTraversal(expr hcl.Expression, keyData instances.RepetitionData) (hcl.Traversal, tfdiags.Diagnostics) {
var trav hcl.Traversal
var diags tfdiags.Diagnostics
switch e := expr.(type) {
case *hclsyntax.RelativeTraversalExpr:
t, d := triggersExprToTraversal(e.Source, keyData)
diags = diags.Append(d)
trav = append(trav, t...)
trav = append(trav, e.Traversal...)
case *hclsyntax.ScopeTraversalExpr:
// a static reference, we can just append the traversal
trav = append(trav, e.Traversal...)
case *hclsyntax.IndexExpr:
// Get the collection from the index expression
t, d := triggersExprToTraversal(e.Collection, keyData)
diags = diags.Append(d)
if diags.HasErrors() {
return nil, diags
}
trav = append(trav, t...)
// The index key is the only place where we could have variables that
// reference count and each, so we need to parse those independently.
idx, hclDiags := parseIndexKeyExpr(e.Key, keyData)
diags = diags.Append(hclDiags)
trav = append(trav, idx)
default:
// Something unexpected got through config validation. We're not sure
// what it is, but we'll point it out in the diagnostics for the user
// to fix.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid replace_triggered_by expression",
Detail: "Unexpected expression found in replace_triggered_by.",
Subject: e.Range().Ptr(),
})
}
return trav, diags
}
// parseIndexKeyExpr takes an hcl.Expression and parses it as an index key, while
// evaluating any references to count.index or each.key.
func parseIndexKeyExpr(expr hcl.Expression, keyData instances.RepetitionData) (hcl.TraverseIndex, hcl.Diagnostics) {
idx := hcl.TraverseIndex{
SrcRange: expr.Range(),
}
trav, diags := hcl.RelTraversalForExpr(expr)
if diags.HasErrors() {
return idx, diags
}
keyParts := []string{}
for _, t := range trav {
attr, ok := t.(hcl.TraverseAttr)
if !ok {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid index expression",
Detail: "Only constant values, count.index or each.key are allowed in index expressions.",
Subject: expr.Range().Ptr(),
})
return idx, diags
}
keyParts = append(keyParts, attr.Name)
}
switch strings.Join(keyParts, ".") {
case "count.index":
if keyData.CountIndex == cty.NilVal {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Reference to "count" in non-counted context`,
Detail: `The "count" object can only be used in "resource" blocks when the "count" argument is set.`,
Subject: expr.Range().Ptr(),
})
}
idx.Key = keyData.CountIndex
case "each.key":
if keyData.EachKey == cty.NilVal {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Reference to "each" in context without for_each`,
Detail: `The "each" object can be used only in "resource" blocks when the "for_each" argument is set.`,
Subject: expr.Range().Ptr(),
})
}
idx.Key = keyData.EachKey
default:
// Something may have slipped through validation, probably from a json
// configuration.
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid index expression",
Detail: "Only constant values, count.index or each.key are allowed in index expressions.",
Subject: expr.Range().Ptr(),
})
}
return idx, diags
}

View File

@ -0,0 +1,94 @@
package terraform
import (
"testing"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/instances"
"github.com/zclconf/go-cty/cty"
)
func TestEvalReplaceTriggeredBy(t *testing.T) {
tests := map[string]struct {
// Raw config expression from within replace_triggered_by list.
// If this does not contains any count or each references, it should
// directly parse into the same *addrs.Reference.
expr string
// If the expression contains count or each, then we need to add
// repetition data, and the static string to parse into the desired
// *addrs.Reference
repData instances.RepetitionData
reference string
}{
"single resource": {
expr: "test_resource.a",
},
"resource instance attr": {
expr: "test_resource.a.attr",
},
"resource instance index attr": {
expr: "test_resource.a[0].attr",
},
"resource instance count": {
expr: "test_resource.a[count.index]",
repData: instances.RepetitionData{
CountIndex: cty.NumberIntVal(0),
},
reference: "test_resource.a[0]",
},
"resource instance for_each": {
expr: "test_resource.a[each.key].attr",
repData: instances.RepetitionData{
EachKey: cty.StringVal("k"),
},
reference: `test_resource.a["k"].attr`,
},
"resource instance for_each map attr": {
expr: "test_resource.a[each.key].attr[each.key]",
repData: instances.RepetitionData{
EachKey: cty.StringVal("k"),
},
reference: `test_resource.a["k"].attr["k"]`,
},
}
for name, tc := range tests {
pos := hcl.Pos{Line: 1, Column: 1}
t.Run(name, func(t *testing.T) {
expr, hclDiags := hclsyntax.ParseExpression([]byte(tc.expr), "", pos)
if hclDiags.HasErrors() {
t.Fatal(hclDiags)
}
got, diags := evalReplaceTriggeredByExpr(expr, tc.repData)
if diags.HasErrors() {
t.Fatal(diags.Err())
}
want := tc.reference
if want == "" {
want = tc.expr
}
// create the desired reference
traversal, travDiags := hclsyntax.ParseTraversalAbs([]byte(want), "", pos)
if travDiags.HasErrors() {
t.Fatal(travDiags)
}
ref, diags := addrs.ParseRef(traversal)
if diags.HasErrors() {
t.Fatal(diags.Err())
}
if got.DisplayString() != ref.DisplayString() {
t.Fatalf("expected %q: got %q", ref.DisplayString(), got.DisplayString())
}
})
}
}