mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-12 09:01:58 -06:00
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:
parent
6eb3264d1a
commit
8b4c89bdaf
143
internal/terraform/evaluate_triggers.go
Normal file
143
internal/terraform/evaluate_triggers.go
Normal 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
|
||||
|
||||
}
|
94
internal/terraform/evaluate_triggers_test.go
Normal file
94
internal/terraform/evaluate_triggers_test.go
Normal 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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user