diff --git a/internal/stacks/stackplan/planned_change.go b/internal/stacks/stackplan/planned_change.go index d38bbb6892..3e80ba1e47 100644 --- a/internal/stacks/stackplan/planned_change.go +++ b/internal/stacks/stackplan/planned_change.go @@ -74,6 +74,12 @@ type PlannedChangeRootInputValue struct { // plan and apply, but a value set during planning can have a different // value during apply. RequiredOnApply bool + + // DeleteOnApply is true if this variable should be removed from the state + // on apply even if it was not actively removed from the configuration in + // a delete action. This is typically the case during a destroy only plan + // in which we want to update the state to remove everything. + DeleteOnApply bool } var _ PlannedChange = (*PlannedChangeRootInputValue)(nil) @@ -85,14 +91,18 @@ func (pc *PlannedChangeRootInputValue) PlannedChangeProto() (*stacks.PlannedChan return nil, err } - var raw anypb.Any - if pc.Action == plans.Delete { + var raws []*anypb.Any + if pc.Action == plans.Delete || pc.DeleteOnApply { + var raw anypb.Any if err := anypb.MarshalFrom(&raw, &tfstackdata1.DeletedRootInputVariable{ Name: pc.Addr.Name, }, proto.MarshalOptions{}); err != nil { return nil, fmt.Errorf("failed to encode raw state for %s: %w", pc.Addr, err) } - } else { + raws = append(raws, &raw) + } + + if pc.Action != plans.Delete { var ppdv *tfstackdata1.DynamicValue if pc.After != cty.NilVal { ppdv, err = tfstackdata1.DynamicValueToTFStackData1(pc.After, cty.DynamicPseudoType) @@ -100,6 +110,7 @@ func (pc *PlannedChangeRootInputValue) PlannedChangeProto() (*stacks.PlannedChan return nil, fmt.Errorf("failed to encode raw state for %s: %w", pc.Addr, err) } } + var raw anypb.Any if err := anypb.MarshalFrom(&raw, &tfstackdata1.PlanRootInputValue{ Name: pc.Addr.Name, Value: ppdv, @@ -107,6 +118,7 @@ func (pc *PlannedChangeRootInputValue) PlannedChangeProto() (*stacks.PlannedChan }, proto.MarshalOptions{}); err != nil { return nil, err } + raws = append(raws, &raw) } var before, after *stacks.DynamicValue @@ -124,7 +136,7 @@ func (pc *PlannedChangeRootInputValue) PlannedChangeProto() (*stacks.PlannedChan } return &stacks.PlannedChange{ - Raw: []*anypb.Any{&raw}, + Raw: raws, Descriptions: []*stacks.PlannedChange_ChangeDescription{ { Description: &stacks.PlannedChange_ChangeDescription_InputVariablePlanned{ diff --git a/internal/stacks/stackruntime/apply_destroy_test.go b/internal/stacks/stackruntime/apply_destroy_test.go index 907f3ed05a..ed39cd3c2e 100644 --- a/internal/stacks/stackruntime/apply_destroy_test.go +++ b/internal/stacks/stackruntime/apply_destroy_test.go @@ -42,6 +42,55 @@ func TestApplyDestroy(t *testing.T) { store *stacks_testing_provider.ResourceStore cycles []TestCycle }{ + "inputs-and-outputs": { + path: "component-input-output", + state: stackstate.NewStateBuilder(). + AddInput("value", cty.StringVal("foo")). + AddOutput("value", cty.StringVal("foo")). + Build(), + cycles: []TestCycle{ + { + planMode: plans.DestroyMode, + 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.Delete, + Before: cty.StringVal("foo"), + After: cty.NullVal(cty.String), + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: mustStackInputVariable("value"), + Action: plans.NoOp, + Before: cty.StringVal("foo"), + After: cty.StringVal("foo"), + DeleteOnApply: true, + }, + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeOutputValue{ + Addr: mustStackOutputValue("value"), + Value: cty.NilVal, // destroyed + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("value"), + Removed: true, // destroyed + }, + }, + }, + }, + }, "missing-resource": { path: path.Join("with-single-input", "valid"), description: "tests what happens when a resource is in state but not in the provider", @@ -86,14 +135,12 @@ func TestApplyDestroy(t *testing.T) { Schema: nil, }, &stackstate.AppliedChangeInputVariable{ - Addr: mustStackInputVariable("id"), - Value: cty.NullVal(cty.String), - // Removed: true, TODO: Enable this in a follow up PR. + Addr: mustStackInputVariable("id"), + Removed: true, }, &stackstate.AppliedChangeInputVariable{ - Addr: mustStackInputVariable("input"), - Value: cty.StringVal("hello"), - // Removed: true, TODO: Enable this in a follow up PR. + Addr: mustStackInputVariable("input"), + Removed: true, }, }, }, @@ -159,14 +206,12 @@ func TestApplyDestroy(t *testing.T) { NewStateSrc: nil, }, &stackstate.AppliedChangeInputVariable{ - Addr: mustStackInputVariable("id"), - Value: cty.StringVal("foo"), - // Removed: true, TODO: Enable this in a follow up PR. + Addr: mustStackInputVariable("id"), + Removed: true, }, &stackstate.AppliedChangeInputVariable{ - Addr: mustStackInputVariable("resource"), - Value: cty.StringVal("bar"), - // Removed: true, TODO: Enable this in a follow up PR. + Addr: mustStackInputVariable("resource"), + Removed: true, }, }, }, @@ -249,12 +294,10 @@ func TestApplyDestroy(t *testing.T) { &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("id"), Value: cty.StringVal("foo"), - // Removed: true, TODO: Enable this in a follow up PR. }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("resource"), Value: cty.StringVal("bar"), - // Removed: true, TODO: Enable this in a follow up PR. }, }, }, @@ -287,14 +330,12 @@ func TestApplyDestroy(t *testing.T) { NewStateSrc: nil, // deleted }, &stackstate.AppliedChangeInputVariable{ - Addr: mustStackInputVariable("id"), - Value: cty.StringVal("foo"), - // Removed: true, TODO: Enable this in a follow up PR. + Addr: mustStackInputVariable("id"), + Removed: true, }, &stackstate.AppliedChangeInputVariable{ - Addr: mustStackInputVariable("resource"), - Value: cty.StringVal("bar"), - // Removed: true, TODO: Enable this in a follow up PR. + Addr: mustStackInputVariable("resource"), + Removed: true, }, }, }, @@ -452,14 +493,12 @@ func TestApplyDestroy(t *testing.T) { Schema: stacks_testing_provider.FailedResourceSchema, }, &stackstate.AppliedChangeInputVariable{ - Addr: mustStackInputVariable("fail_apply"), - Value: cty.False, - // Removed: true, TODO: Enable this in a follow up PR. + Addr: mustStackInputVariable("fail_apply"), + Removed: true, }, &stackstate.AppliedChangeInputVariable{ - Addr: mustStackInputVariable("fail_plan"), - Value: cty.False, - // Removed: true, TODO: Enable this in a follow up PR. + Addr: mustStackInputVariable("fail_plan"), + Removed: true, }, }, wantAppliedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { diff --git a/internal/stacks/stackruntime/internal/stackeval/input_variable.go b/internal/stacks/stackruntime/internal/stackeval/input_variable.go index 8865a4e128..1eae32f44d 100644 --- a/internal/stacks/stackruntime/internal/stackeval/input_variable.go +++ b/internal/stacks/stackruntime/internal/stackeval/input_variable.go @@ -243,6 +243,8 @@ func (v *InputVariable) PlanChanges(ctx context.Context) ([]stackplan.PlannedCha return nil, diags } + destroy := v.main.PlanningOpts().PlanningMode == plans.DestroyMode + before, beforeEphemeral := v.main.PlanPrevState().RootInputVariable(v.Addr().Item) decl := v.Declaration(ctx) @@ -290,6 +292,7 @@ func (v *InputVariable) PlanChanges(ctx context.Context) ([]stackplan.PlannedCha Before: before, After: after, RequiredOnApply: requiredOnApply, + DeleteOnApply: destroy, }, }, diags } @@ -331,6 +334,15 @@ func (v *InputVariable) CheckApply(ctx context.Context) ([]stackstate.AppliedCha return nil, diags } + if v.main.PlanBeingApplied().DeletedInputVariables.Has(v.Addr().Item) { + // If the plan being applied has this variable as being deleted, then + // we won't handle it here. This is usually the case during a destroy + // only plan in which we wanted to both capture the value for an input + // as we still need it, while also noting that everything is being + // destroyed. + return nil, diags + } + decl := v.Declaration(ctx) value := v.Value(ctx, ApplyPhase) if decl.Ephemeral { diff --git a/internal/stacks/stackruntime/internal/stackeval/stack.go b/internal/stacks/stackruntime/internal/stackeval/stack.go index 5de88ecf40..7363099ef7 100644 --- a/internal/stacks/stackruntime/internal/stackeval/stack.go +++ b/internal/stacks/stackruntime/internal/stackeval/stack.go @@ -761,33 +761,59 @@ func (s *Stack) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfd // correctly implemented. panic(fmt.Sprintf("invalid result from Stack.ResultValue: %#v", afterVal)) } - beforeVal := s.main.PlanPrevState().RootOutputValues() var changes []stackplan.PlannedChange - for it := afterVal.ElementIterator(); it.Next(); { - k, after := it.Element() - - addr := stackaddrs.OutputValue{Name: k.AsString()} - before := cty.NullVal(cty.DynamicPseudoType) - action := plans.Create - - if actualBefore, exists := beforeVal[addr]; exists { - before = actualBefore - - if result := before.Equals(after); result.IsKnown() && result.True() { - action = plans.NoOp - } else { - action = plans.Update + if s.main.PlanningOpts().PlanningMode == plans.DestroyMode { + // For a destroy plan, we'll actually cheat a little bit and swap out + // the values for null and destroy actions. We do this here because + // for most stacks and outputs we might have components that rely on + // the output being calculated based on the prior state rather than + // returning null. So, we leave the internals to compute the value + // in a helpful way and then just blanket say that all outputs will be + // destroyed during the plan. + for name, attr := range afterVal.Type().AttributeTypes() { + addr := stackaddrs.OutputValue{Name: name} + if before, exists := beforeVal[addr]; exists { + + // If the before doesn't exist, then we'll emit nothing for this + // change as it doesn't already exist in state so doesn't need + // to be destroyed. + changes = append(changes, &stackplan.PlannedChangeOutputValue{ + Addr: stackaddrs.OutputValue{Name: name}, + Action: plans.Delete, + Before: before, + // We can set the right type here, as do have the + // configuration. + After: cty.NullVal(attr), + }) } } + } else { + for it := afterVal.ElementIterator(); it.Next(); { + k, after := it.Element() + + addr := stackaddrs.OutputValue{Name: k.AsString()} + before := cty.NullVal(cty.DynamicPseudoType) + action := plans.Create + + if actualBefore, exists := beforeVal[addr]; exists { + before = actualBefore + + if result := before.Equals(after); result.IsKnown() && result.True() { + action = plans.NoOp + } else { + action = plans.Update + } + } - changes = append(changes, &stackplan.PlannedChangeOutputValue{ - Addr: addr, - Action: action, - Before: before, - After: after, - }) + changes = append(changes, &stackplan.PlannedChangeOutputValue{ + Addr: addr, + Action: action, + Before: before, + After: after, + }) + } } for addr, before := range beforeVal { @@ -843,16 +869,27 @@ func (s *Stack) CheckApply(ctx context.Context) ([]stackstate.AppliedChange, tfd panic(fmt.Sprintf("invalid result from Stack.ResultValue: %#v", resultVal)) } + deletedOutputValues := s.main.PlanBeingApplied().DeletedOutputValues + var changes []stackstate.AppliedChange for it := resultVal.ElementIterator(); it.Next(); { k, v := it.Element() + + addr := stackaddrs.OutputValue{Name: k.AsString()} + if deletedOutputValues.Has(addr) { + // Then we are deleting this output value even though it is in the + // configuration for some reason (probably because this is a + // delete plan and we're deleting everything). So, we won't process + // it here and only below. + continue + } changes = append(changes, &stackstate.AppliedChangeOutputValue{ - Addr: stackaddrs.OutputValue{Name: k.AsString()}, + Addr: addr, Value: v, }) } - for _, value := range s.main.PlanBeingApplied().DeletedOutputValues.Elems() { + for _, value := range deletedOutputValues.Elems() { // elements that are being deleted will explicitly not show up in our // result value changes = append(changes, &stackstate.AppliedChangeOutputValue{