Skip to content

Commit c1886a2

Browse files
core: Don't re-register checkable outputs during the apply step
Once again we're caught out by sharing the same output value node type between the plan phase and the apply phase. To allow for some slight variation between plan and apply without drastic refactoring here we just add a new flag to nodeExpandOutput which is true only during the planning phase. This then allows us to register the checkable objects only during the planning phase and not incorrectly re-register them during the apply phase. It's incorrect to re-register during apply because we carry over the planned checkable objects from the plan phase into the apply phase so we can guarantee that the final state will have all of the same checkable objects that the plan did. This avoids a panic during the apply phase from the incorrect duplicate registration.
1 parent 8c98e1f commit c1886a2

File tree

4 files changed

+127
-2
lines changed

4 files changed

+127
-2
lines changed

internal/terraform/context_apply2_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ import (
1414
"github.com/zclconf/go-cty/cty"
1515

1616
"github.com/hashicorp/terraform/internal/addrs"
17+
"github.com/hashicorp/terraform/internal/checks"
1718
"github.com/hashicorp/terraform/internal/configs/configschema"
1819
"github.com/hashicorp/terraform/internal/lang/marks"
1920
"github.com/hashicorp/terraform/internal/plans"
2021
"github.com/hashicorp/terraform/internal/providers"
2122
"github.com/hashicorp/terraform/internal/states"
23+
"github.com/hashicorp/terraform/internal/tfdiags"
2224
)
2325

2426
// Test that the PreApply hook is called with the correct deposed key
@@ -917,6 +919,106 @@ resource "test_resource" "c" {
917919
})
918920
}
919921

922+
func TestContext2Apply_outputValuePrecondition(t *testing.T) {
923+
m := testModuleInline(t, map[string]string{
924+
"main.tf": `
925+
variable "input" {
926+
type = string
927+
}
928+
929+
module "child" {
930+
source = "./child"
931+
932+
input = var.input
933+
}
934+
935+
output "result" {
936+
value = module.child.result
937+
}
938+
`,
939+
"child/main.tf": `
940+
variable "input" {
941+
type = string
942+
}
943+
944+
output "result" {
945+
value = var.input
946+
947+
precondition {
948+
condition = var.input != ""
949+
error_message = "Input must not be empty."
950+
}
951+
}
952+
`,
953+
})
954+
955+
t.Run("pass", func(t *testing.T) {
956+
ctx := testContext2(t, &ContextOpts{})
957+
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
958+
Mode: plans.NormalMode,
959+
SetVariables: InputValues{
960+
"input": &InputValue{
961+
Value: cty.StringVal("beep"),
962+
SourceType: ValueFromCLIArg,
963+
},
964+
},
965+
})
966+
assertNoDiagnostics(t, diags)
967+
968+
addr := addrs.OutputValue{Name: "result"}.Absolute(addrs.RootModuleInstance.Child("child", addrs.NoKey))
969+
result := plan.Checks.GetObjectResult(addr)
970+
if result == nil {
971+
t.Fatalf("no check result for %s in the plan", addr)
972+
}
973+
if got, want := result.Status, checks.StatusPass; got != want {
974+
t.Fatalf("wrong check status during planning\ngot: %s\nwant: %s", got, want)
975+
}
976+
977+
state, diags := ctx.Apply(plan, m)
978+
assertNoDiagnostics(t, diags)
979+
result = state.CheckResults.GetObjectResult(addr)
980+
if result == nil {
981+
t.Fatalf("no check result for %s in the final state", addr)
982+
}
983+
if got, want := result.Status, checks.StatusPass; got != want {
984+
t.Errorf("wrong check status after apply\ngot: %s\nwant: %s", got, want)
985+
}
986+
})
987+
988+
t.Run("fail", func(t *testing.T) {
989+
// NOTE: This test actually catches a failure during planning and so
990+
// cannot proceed to apply, so it's really more of a plan test
991+
// than an apply test but better to keep all of these
992+
// thematically-related test cases together.
993+
ctx := testContext2(t, &ContextOpts{})
994+
_, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
995+
Mode: plans.NormalMode,
996+
SetVariables: InputValues{
997+
"input": &InputValue{
998+
Value: cty.StringVal(""),
999+
SourceType: ValueFromCLIArg,
1000+
},
1001+
},
1002+
})
1003+
if !diags.HasErrors() {
1004+
t.Fatalf("succeeded; want error")
1005+
}
1006+
1007+
const wantSummary = "Module output value precondition failed"
1008+
found := false
1009+
for _, diag := range diags {
1010+
if diag.Severity() == tfdiags.Error && diag.Description().Summary == wantSummary {
1011+
found = true
1012+
break
1013+
}
1014+
}
1015+
1016+
if !found {
1017+
t.Fatalf("missing expected error\nwant summary: %s\ngot: %s", wantSummary, diags.Err().Error())
1018+
}
1019+
})
1020+
}
1021+
9201022
func TestContext2Apply_resourceConditionApplyTimeFail(t *testing.T) {
9211023
// This tests the less common situation where a condition fails due to
9221024
// a change in a resource other than the one the condition is attached to,

internal/terraform/graph_builder_plan.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer {
113113
Config: b.Config,
114114
RefreshOnly: b.skipPlanChanges,
115115
removeRootOutputs: b.Operation == walkPlanDestroy,
116+
Planning: b.Operation == walkPlan || b.Operation == walkPlanDestroy,
116117
},
117118

118119
// Add orphan resources

internal/terraform/node_output.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ type nodeExpandOutput struct {
2525
Config *configs.Output
2626
Destroy bool
2727
RefreshOnly bool
28+
29+
// Planning is set to true when this node is in a graph that was produced
30+
// by the plan graph builder, as opposed to the apply graph builder.
31+
// This quirk is just because we share the same node type between both
32+
// phases but in practice there are a few small differences in the actions
33+
// we need to take between plan and apply. See method DynamicExpand for
34+
// details.
35+
Planning bool
2836
}
2937

3038
var (
@@ -59,9 +67,18 @@ func (n *nodeExpandOutput) DynamicExpand(ctx EvalContext) (*Graph, error) {
5967
// wants to know the addresses of the checkable objects so that it can
6068
// treat them as unknown status if we encounter an error before actually
6169
// visiting the checks.
70+
//
71+
// We must do this only during planning, because the apply phase will start
72+
// with all of the same checkable objects that were registered during the
73+
// planning phase. Consumers of our JSON plan and state formats expect
74+
// that the set of checkable objects will be consistent between the plan
75+
// and any state snapshots created during apply, and that only the statuses
76+
// of those objects will have changed.
6277
var checkableAddrs addrs.Set[addrs.Checkable]
63-
if checkState := ctx.Checks(); checkState.ConfigHasChecks(n.Addr.InModule(n.Module)) {
64-
checkableAddrs = addrs.MakeSet[addrs.Checkable]()
78+
if n.Planning {
79+
if checkState := ctx.Checks(); checkState.ConfigHasChecks(n.Addr.InModule(n.Module)) {
80+
checkableAddrs = addrs.MakeSet[addrs.Checkable]()
81+
}
6582
}
6683

6784
var g Graph

internal/terraform/transform_output.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ type OutputTransformer struct {
2626
// Refresh-only mode means that any failing output preconditions are
2727
// reported as warnings rather than errors
2828
RefreshOnly bool
29+
30+
// Planning must be set to true only when we're building a planning graph.
31+
// It must be set to false whenever we're building an apply graph.
32+
Planning bool
2933
}
3034

3135
func (t *OutputTransformer) Transform(g *Graph) error {
@@ -89,6 +93,7 @@ func (t *OutputTransformer) transform(g *Graph, c *configs.Config) error {
8993
Config: o,
9094
Destroy: t.removeRootOutputs,
9195
RefreshOnly: t.RefreshOnly,
96+
Planning: t.Planning,
9297
}
9398
}
9499

0 commit comments

Comments
 (0)