diff --git a/internal/stacks/stackruntime/apply_test.go b/internal/stacks/stackruntime/apply_test.go index 4233c6f8a0..4e3cb384cc 100644 --- a/internal/stacks/stackruntime/apply_test.go +++ b/internal/stacks/stackruntime/apply_test.go @@ -159,6 +159,54 @@ func TestApply(t *testing.T) { }, }, }, + "updating inputs and outputs (noop)": { + path: "component-input-output", + cycles: []TestCycle{ + { + planInputs: map[string]cty.Value{ + "value": cty.StringVal("foo"), + }, + }, + { + planInputs: map[string]cty.Value{ + "value": cty.StringVal("foo"), + }, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangeOutputValue{ + Addr: mustStackOutputValue("value"), + Action: plans.NoOp, + Before: cty.StringVal("foo"), + After: cty.StringVal("foo"), + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: mustStackInputVariable("value"), + Action: plans.NoOp, + Before: cty.StringVal("foo"), + After: cty.StringVal("foo"), + }, + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeOutputValue{ + Addr: mustStackOutputValue("value"), + Value: cty.StringVal("foo"), + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("value"), + Value: cty.StringVal("foo"), + }, + }, + }, + }, + }, "deleting inputs and outputs": { path: "component-input-output", state: stackstate.NewStateBuilder(). diff --git a/internal/stacks/stackruntime/internal/stackeval/input_variable.go b/internal/stacks/stackruntime/internal/stackeval/input_variable.go index 47ed2919ae..8865a4e128 100644 --- a/internal/stacks/stackruntime/internal/stackeval/input_variable.go +++ b/internal/stacks/stackruntime/internal/stackeval/input_variable.go @@ -245,13 +245,6 @@ func (v *InputVariable) PlanChanges(ctx context.Context) ([]stackplan.PlannedCha before, beforeEphemeral := v.main.PlanPrevState().RootInputVariable(v.Addr().Item) - action := plans.Create - if beforeEphemeral || before != cty.NilVal { - action = plans.Update - } else { - before = cty.NullVal(cty.DynamicPseudoType) - } - decl := v.Declaration(ctx) after := v.Value(ctx, PlanPhase) requiredOnApply := false @@ -261,6 +254,35 @@ func (v *InputVariable) PlanChanges(ctx context.Context) ([]stackplan.PlannedCha requiredOnApply = !after.IsNull() after = cty.NilVal } + + var action plans.Action + if beforeEphemeral { + // We can't tell the difference between an Update and NoOp change for + // an ephemeral input so we just always mark it as updated. + action = plans.Update + } else if before != cty.NilVal { + if decl.Ephemeral { + // if the new value is ephemeral, and the old value wasn't, then + // we'll set the operation to an update even if the actual hasn't + // changed + action = plans.Update + } else if result := before.Equals(after); result.IsKnown() && result.True() { + // The values are definitely equal, so NoOp change. + action = plans.NoOp + } else { + // If we don't know for sure that the values are equal, then we'll + // call this an update. + action = plans.Update + } + } else { + action = plans.Create + + // We think this is a brand new input variable so we'll also mark the + // before as being a null value (as opposed to NilVal which means it + // existed before but was ephemeral). + before = cty.NullVal(cty.DynamicPseudoType) + } + return []stackplan.PlannedChange{ &stackplan.PlannedChangeRootInputValue{ Addr: v.Addr().Item, diff --git a/internal/stacks/stackruntime/internal/stackeval/stack.go b/internal/stacks/stackruntime/internal/stackeval/stack.go index e1fe92dd0a..5de88ecf40 100644 --- a/internal/stacks/stackruntime/internal/stackeval/stack.go +++ b/internal/stacks/stackruntime/internal/stackeval/stack.go @@ -773,8 +773,13 @@ func (s *Stack) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfd action := plans.Create if actualBefore, exists := beforeVal[addr]; exists { - action = plans.Update before = actualBefore + + if result := before.Equals(after); result.IsKnown() && result.True() { + action = plans.NoOp + } else { + action = plans.Update + } } changes = append(changes, &stackplan.PlannedChangeOutputValue{