Skip to content

Commit f960041

Browse files
committed
replace-triggered-by must reference a valid resource or resource attribute
1 parent 3bbd40b commit f960041

File tree

2 files changed

+83
-4
lines changed

2 files changed

+83
-4
lines changed

internal/terraform/context_plan2_test.go

+46
Original file line numberDiff line numberDiff line change
@@ -4399,6 +4399,52 @@ resource "test_object" "b" {
43994399
}
44004400
}
44014401

4402+
func TestContext2Plan_triggeredByInvalid(t *testing.T) {
4403+
m := testModuleInline(t, map[string]string{
4404+
"main.tf": `
4405+
resource "test_object" "a" {
4406+
count = 1
4407+
test_string = "new"
4408+
}
4409+
resource "test_object" "b" {
4410+
count = 1
4411+
test_string = test_object.a[count.index].test_string
4412+
lifecycle {
4413+
# reference to an invalid attribute "yikes" should cause an error
4414+
replace_triggered_by = [ test_object.a[count.index].yikes ]
4415+
}
4416+
}
4417+
`,
4418+
})
4419+
4420+
p := simpleMockProvider()
4421+
4422+
ctx := testContext2(t, &ContextOpts{
4423+
Providers: map[addrs.Provider]providers.Factory{
4424+
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
4425+
},
4426+
})
4427+
4428+
state := states.BuildState(func(s *states.SyncState) {
4429+
s.SetResourceInstanceCurrent(
4430+
mustResourceInstanceAddr("test_object.a[0]"),
4431+
&states.ResourceInstanceObjectSrc{
4432+
AttrsJSON: []byte(`{"test_string":"old"}`),
4433+
Status: states.ObjectReady,
4434+
},
4435+
mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
4436+
)
4437+
})
4438+
4439+
_, diags := ctx.Plan(m, state, &PlanOpts{
4440+
Mode: plans.NormalMode,
4441+
})
4442+
fmt.Println(diags.Err().Error())
4443+
if !diags.HasErrors() {
4444+
t.Fatalf("expected errors, got none")
4445+
}
4446+
}
4447+
44024448
func TestContext2Plan_dataSchemaChange(t *testing.T) {
44034449
// We can't decode the prior state when a data source upgrades the schema
44044450
// in an incompatible way. Since prior state for data sources is purely

internal/terraform/eval_context_builtin.go

+37-4
Original file line numberDiff line numberDiff line change
@@ -407,10 +407,7 @@ func (ctx *BuiltinEvalContext) EvaluateReplaceTriggeredBy(expr hcl.Expression, r
407407
return nil, false, diags
408408
}
409409

410-
path, _ := traversalToPath(ref.Remaining)
411-
attrBefore, _ := path.Apply(change.Before)
412-
attrAfter, _ := path.Apply(change.After)
413-
410+
attrBefore, attrAfter, diags := evalTriggeredByRefPath(ref, change)
414411
if attrBefore == cty.NilVal || attrAfter == cty.NilVal {
415412
replace := attrBefore != attrAfter
416413
return ref, replace, diags
@@ -421,6 +418,42 @@ func (ctx *BuiltinEvalContext) EvaluateReplaceTriggeredBy(expr hcl.Expression, r
421418
return ref, replace, diags
422419
}
423420

421+
// evalTriggeredByRefPath evaluates the attribute reference path in the context of the
422+
// resource change objects. It returns the before and after values of the attribute
423+
// and any diagnostics that occurred during evaluation.
424+
func evalTriggeredByRefPath(ref *addrs.Reference, change *plans.ResourceInstanceChange) (cty.Value, cty.Value, tfdiags.Diagnostics) {
425+
var diags tfdiags.Diagnostics
426+
path, key := traversalToPath(ref.Remaining)
427+
428+
applyPath := func(value cty.Value) (cty.Value, tfdiags.Diagnostics) {
429+
attr, err := path.Apply(value)
430+
if err != nil {
431+
return cty.NilVal, tfdiags.Diagnostics{}.Append(tfdiags.AttributeValue(
432+
tfdiags.Error,
433+
"Invalid attribute path",
434+
fmt.Sprintf(
435+
"The specified path %q could not be applied to the object specified in replace_triggered_by:\n"+
436+
"Path: %s\n"+
437+
"Error: %s\n"+
438+
"Please check your configuration and ensure the path is valid.",
439+
key, ref.DisplayString(), err.Error(),
440+
),
441+
path,
442+
))
443+
}
444+
return attr, nil
445+
}
446+
447+
// Apply the path to the "before" and "after" states
448+
attrBefore, beforeDiags := applyPath(change.Before)
449+
diags = diags.AppendWithoutDuplicates(beforeDiags...)
450+
451+
attrAfter, afterDiags := applyPath(change.After)
452+
diags = diags.AppendWithoutDuplicates(afterDiags...)
453+
454+
return attrBefore, attrAfter, diags
455+
}
456+
424457
func (ctx *BuiltinEvalContext) EvaluationScope(self addrs.Referenceable, source addrs.Referenceable, keyData InstanceKeyEvalData) *lang.Scope {
425458
switch scope := ctx.scope.(type) {
426459
case evalContextModuleInstance:

0 commit comments

Comments
 (0)