diff --git a/internal/stacks/stackplan/planned_change.go b/internal/stacks/stackplan/planned_change.go index 7b205627a9..fb6ff36563 100644 --- a/internal/stacks/stackplan/planned_change.go +++ b/internal/stacks/stackplan/planned_change.go @@ -8,18 +8,19 @@ import ( "time" "github.com/hashicorp/go-version" - "github.com/hashicorp/terraform/internal/stacks/stackutils" "github.com/zclconf/go-cty/cty" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/anypb" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/plans/planfile" "github.com/hashicorp/terraform/internal/plans/planproto" "github.com/hashicorp/terraform/internal/rpcapi/terraform1" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackutils" "github.com/hashicorp/terraform/internal/stacks/tfstackdata1" "github.com/hashicorp/terraform/internal/states" ) @@ -96,6 +97,12 @@ type PlannedChangeComponentInstance struct { // are tracked in their own [PlannedChange] objects. Action plans.Action + // RequiredComponents is a set of the addresses of all of the components + // that provide infrastructure that this one's infrastructure will + // depend on. Any component named here must exist for the entire lifespan + // of this component instance. + RequiredComponents collections.Set[stackaddrs.AbsComponent] + // PlannedInputValues records our best approximation of the component's // topmost input values during the planning phase. This could contain // unknown values if one component is configured from results of another. @@ -104,6 +111,8 @@ type PlannedChangeComponentInstance struct { // with what's captured here. PlannedInputValues map[string]plans.DynamicValue + PlannedOutputValues map[string]cty.Value + // PlanTimestamp is the timestamp that would be returned from the // "plantimestamp" function in modules inside this component. We // must preserve this in the raw plan data to ensure that we can diff --git a/internal/stacks/stackruntime/internal/stackeval/component_instance.go b/internal/stacks/stackruntime/internal/stackeval/component_instance.go index ef4b4de441..4dd5b7df6b 100644 --- a/internal/stacks/stackruntime/internal/stackeval/component_instance.go +++ b/internal/stacks/stackruntime/internal/stackeval/component_instance.go @@ -979,16 +979,46 @@ func (c *ComponentInstance) PlanChanges(ctx context.Context) ([]stackplan.Planne corePlan, moreDiags := c.CheckModuleTreePlan(ctx) diags = diags.Append(moreDiags) if corePlan != nil { + existedBefore := false + if prevState := c.main.PlanPrevState(); prevState != nil { + existedBefore = prevState.HasComponentInstance(c.Addr()) + } + destroying := corePlan.UIMode == plans.DestroyMode + refreshOnly := corePlan.UIMode == plans.RefreshOnlyMode + + var action plans.Action + switch { + case destroying: + action = plans.Delete + case refreshOnly: + action = plans.Read + case existedBefore: + action = plans.Update + default: + action = plans.Create + } + + // FIXME: This is silly because we make ResultValue wrap the output + // values map up into an object and then just unwrap it again + // immediately. + var outputVals map[string]cty.Value + if resultVal := c.ResultValue(ctx, PlanPhase); resultVal.Type().IsObjectType() && resultVal.IsKnown() && !resultVal.IsNull() { + outputVals = make(map[string]cty.Value, resultVal.LengthInt()) + for it := resultVal.ElementIterator(); it.Next(); { + k, v := it.Element() + outputVals[k.AsString()] = v + } + } + // We must always at least announce that the component instance exists, // and that must come before any resource instance changes referring to it. changes = append(changes, &stackplan.PlannedChangeComponentInstance{ Addr: c.Addr(), - // FIXME: Once we actually have a prior state this should vary - // depending on whether the same component instance existed in - // the prior state. - Action: plans.Create, - PlannedInputValues: corePlan.VariableValues, + Action: action, + RequiredComponents: c.RequiredComponents(ctx), + PlannedInputValues: corePlan.VariableValues, + PlannedOutputValues: outputVals, // We must remember the plan timestamp so that the plantimestamp // function can return a consistent result during a later apply phase. diff --git a/internal/stacks/stackruntime/plan_test.go b/internal/stacks/stackruntime/plan_test.go index 1a542790b3..c5ffe77d9b 100644 --- a/internal/stacks/stackruntime/plan_test.go +++ b/internal/stacks/stackruntime/plan_test.go @@ -13,6 +13,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform/internal/addrs" terraformProvider "github.com/hashicorp/terraform/internal/builtin/providers/terraform" + "github.com/hashicorp/terraform/internal/collections" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" @@ -81,7 +82,11 @@ func TestPlanWithSingleResource(t *testing.T) { ), Action: plans.Create, PlannedInputValues: make(map[string]plans.DynamicValue), - PlanTimestamp: fakePlanTimestamp, + PlannedOutputValues: map[string]cty.Value{ + "input": cty.StringVal("hello"), + "output": cty.UnknownVal(cty.String), + }, + PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, @@ -171,7 +176,11 @@ func TestPlanWithSingleResource(t *testing.T) { }, } - if diff := cmp.Diff(wantChanges, gotChanges, ctydebug.CmpOptions); diff != "" { + cmpOptions := cmp.Options{ + ctydebug.CmpOptions, + collections.CmpOptions, + } + if diff := cmp.Diff(wantChanges, gotChanges, cmpOptions); diff != "" { t.Errorf("wrong changes\n%s", diff) } } diff --git a/internal/stacks/stackstate/state.go b/internal/stacks/stackstate/state.go index 53a0a7aaef..9b9293cce1 100644 --- a/internal/stacks/stackstate/state.go +++ b/internal/stacks/stackstate/state.go @@ -41,6 +41,10 @@ func NewState() *State { } } +func (s *State) HasComponentInstance(addr stackaddrs.AbsComponentInstance) bool { + return s.componentInstances.HasKey(addr) +} + // AllComponentInstances returns a set of addresses for all of the component // instances that are tracked in the state. //