diff --git a/internal/plans/planfile/tfplan.go b/internal/plans/planfile/tfplan.go index 290cd10baf89..20f7c6f90e94 100644 --- a/internal/plans/planfile/tfplan.go +++ b/internal/plans/planfile/tfplan.go @@ -450,7 +450,7 @@ func deferredChangeFromTfplan(dc *planproto.DeferredResourceInstanceChange) (*pl return nil, err } - reason, err := deferredReasonFromProto(dc.Deferred.Reason) + reason, err := DeferredReasonFromProto(dc.Deferred.Reason) if err != nil { return nil, err } @@ -461,7 +461,7 @@ func deferredChangeFromTfplan(dc *planproto.DeferredResourceInstanceChange) (*pl }, nil } -func deferredReasonFromProto(reason planproto.DeferredReason) (providers.DeferredReason, error) { +func DeferredReasonFromProto(reason planproto.DeferredReason) (providers.DeferredReason, error) { switch reason { case planproto.DeferredReason_INSTANCE_COUNT_UNKNOWN: return providers.DeferredReasonInstanceCountUnknown, nil diff --git a/internal/stacks/stackplan/component.go b/internal/stacks/stackplan/component.go index 9d413fd521ee..aa5003e713c3 100644 --- a/internal/stacks/stackplan/component.go +++ b/internal/stacks/stackplan/component.go @@ -46,8 +46,9 @@ type Component struct { // will handle any apply-time actions for that object. ResourceInstanceProviderConfig addrs.Map[addrs.AbsResourceInstanceObject, addrs.AbsProviderConfig] - // TODO: Something for deferred resource instance changes, once we have - // such a concept. + // DeferredResourceInstanceChanges is a set of resource instance objects + // that have changes that are deferred to a later plan and apply cycle. + DeferredResourceInstanceChanges addrs.Map[addrs.AbsResourceInstanceObject, *plans.DeferredResourceInstanceChangeSrc] // PlanTimestamp is the time Terraform Core recorded as the single "plan // timestamp", which is used only for the result of the "plantimestamp" diff --git a/internal/stacks/stackplan/from_proto.go b/internal/stacks/stackplan/from_proto.go index fbdb789b35fb..4b95ebe4930b 100644 --- a/internal/stacks/stackplan/from_proto.go +++ b/internal/stacks/stackplan/from_proto.go @@ -15,6 +15,7 @@ import ( "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/providers" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" "github.com/hashicorp/terraform/internal/stacks/stackstate" "github.com/hashicorp/terraform/internal/stacks/tfstackdata1" @@ -122,9 +123,10 @@ func LoadFromProto(msgs []*anypb.Any) (*Plan, error) { PlannedOutputValues: outputVals, PlannedChecks: checkResults, - ResourceInstancePlanned: addrs.MakeMap[addrs.AbsResourceInstanceObject, *plans.ResourceInstanceChangeSrc](), - ResourceInstancePriorState: addrs.MakeMap[addrs.AbsResourceInstanceObject, *states.ResourceInstanceObjectSrc](), - ResourceInstanceProviderConfig: addrs.MakeMap[addrs.AbsResourceInstanceObject, addrs.AbsProviderConfig](), + ResourceInstancePlanned: addrs.MakeMap[addrs.AbsResourceInstanceObject, *plans.ResourceInstanceChangeSrc](), + ResourceInstancePriorState: addrs.MakeMap[addrs.AbsResourceInstanceObject, *states.ResourceInstanceObjectSrc](), + ResourceInstanceProviderConfig: addrs.MakeMap[addrs.AbsResourceInstanceObject, addrs.AbsProviderConfig](), + DeferredResourceInstanceChanges: addrs.MakeMap[addrs.AbsResourceInstanceObject, *plans.DeferredResourceInstanceChangeSrc](), }) } c := ret.Components.Get(addr) @@ -204,6 +206,79 @@ func LoadFromProto(msgs []*anypb.Any) (*Plan, error) { c.ResourceInstancePriorState.Put(fullAddr, nil) } + case *tfstackdata1.PlanDeferredResourceInstanceChange: + if msg.Deferred == nil { + return nil, fmt.Errorf("missing deferred from PlanDeferredResourceInstanceChange") + } + + cAddr, diags := stackaddrs.ParseAbsComponentInstanceStr(msg.Change.ComponentInstanceAddr) + if diags.HasErrors() { + return nil, fmt.Errorf("invalid component instance address syntax in %q", msg.Change.ComponentInstanceAddr) + } + riAddr, diags := addrs.ParseAbsResourceInstanceStr(msg.Change.ResourceInstanceAddr) + if diags.HasErrors() { + return nil, fmt.Errorf("invalid resource instance address syntax in %q", msg.Change.ResourceInstanceAddr) + } + providerConfigAddr, diags := addrs.ParseAbsProviderConfigStr(msg.Change.ProviderConfigAddr) + if diags.HasErrors() { + return nil, fmt.Errorf("invalid provider configuration address syntax in %q", msg.Change.ProviderConfigAddr) + } + + var deposedKey addrs.DeposedKey + if msg.Change.DeposedKey != "" { + deposedKey, err = addrs.ParseDeposedKey(msg.Change.DeposedKey) + if err != nil { + return nil, fmt.Errorf("invalid deposed key syntax in %q", msg.Change.DeposedKey) + } + } + fullAddr := addrs.AbsResourceInstanceObject{ + ResourceInstance: riAddr, + DeposedKey: deposedKey, + } + + c, ok := ret.Components.GetOk(cAddr) + if !ok { + return nil, fmt.Errorf("deferred resource instance for unannounced component instance %s", cAddr) + } + + riPlan, err := planfile.ResourceChangeFromProto(msg.Change.Change) + if err != nil { + return nil, fmt.Errorf("invalid resource instance change: %w", err) + } + // We currently have some redundant information in the nested + // "change" object due to having reused some protobuf message + // types from the traditional Terraform CLI planproto format. + // We'll make sure the redundant information is consistent + // here because otherwise they're likely to cause + // difficult-to-debug problems downstream. + if !riPlan.Addr.Equal(fullAddr.ResourceInstance) && riPlan.DeposedKey == fullAddr.DeposedKey { + return nil, fmt.Errorf("planned change has inconsistent address to its containing object") + } + if !riPlan.ProviderAddr.Equal(providerConfigAddr) { + return nil, fmt.Errorf("planned change has inconsistent provider configuration address to its containing object") + } + + var deferredReason providers.DeferredReason + switch msg.Deferred.Reason { + case tfstackdata1.PlanDeferredResourceInstanceChange_Deferred_INSTANCE_COUNT_UNKNOWN: + deferredReason = providers.DeferredReasonInstanceCountUnknown + case tfstackdata1.PlanDeferredResourceInstanceChange_Deferred_RESOURCE_CONFIG_UNKNOWN: + deferredReason = providers.DeferredReasonResourceConfigUnknown + case tfstackdata1.PlanDeferredResourceInstanceChange_Deferred_PROVIDER_CONFIG_UNKNOWN: + deferredReason = providers.DeferredReasonProviderConfigUnknown + case tfstackdata1.PlanDeferredResourceInstanceChange_Deferred_ABSENT_PREREQ: + deferredReason = providers.DeferredReasonAbsentPrereq + case tfstackdata1.PlanDeferredResourceInstanceChange_Deferred_DEFERRED_PREREQ: + deferredReason = providers.DeferredReasonDeferredPrereq + default: + deferredReason = providers.DeferredReasonInvalid + } + + c.DeferredResourceInstanceChanges.Put(fullAddr, &plans.DeferredResourceInstanceChangeSrc{ + ChangeSrc: riPlan, + DeferredReason: deferredReason, + }) + default: // Should not get here, because a stack plan can only be loaded by // the same version of Terraform that created it, and the above diff --git a/internal/stacks/stackplan/planned_change.go b/internal/stacks/stackplan/planned_change.go index d2967761dc3e..c4e061da61d8 100644 --- a/internal/stacks/stackplan/planned_change.go +++ b/internal/stacks/stackplan/planned_change.go @@ -18,6 +18,7 @@ import ( "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/providers" "github.com/hashicorp/terraform/internal/rpcapi/terraform1" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" "github.com/hashicorp/terraform/internal/stacks/stackutils" @@ -388,6 +389,81 @@ func (pc *PlannedChangeResourceInstancePlanned) PlannedChangeProto() (*terraform Descriptions: descs, }, nil } + +// PlannedChangeDeferredResourceInstancePlanned announces that an action that Terraform +// is proposing to take if this plan is applied is being deferred. +type PlannedChangeDeferredResourceInstancePlanned struct { + // ResourceInstancePlanned is the planned change that is being deferred. + ResourceInstancePlanned PlannedChangeResourceInstancePlanned + + // DeferredReason is the reason why the change is being deferred. + DeferredReason providers.DeferredReason +} + +var _ PlannedChange = (*PlannedChangeDeferredResourceInstancePlanned)(nil) + +// PlannedChangeProto implements PlannedChange. +func (dpc *PlannedChangeDeferredResourceInstancePlanned) PlannedChangeProto() (*terraform1.PlannedChange, error) { + change, err := dpc.ResourceInstancePlanned.PlanResourceInstanceChangePlannedProto() + if err != nil { + return nil, err + } + + var deferred tfstackdata1.PlanDeferredResourceInstanceChange_Deferred + switch dpc.DeferredReason { + case providers.DeferredReasonInstanceCountUnknown: + deferred.Reason = tfstackdata1.PlanDeferredResourceInstanceChange_Deferred_INSTANCE_COUNT_UNKNOWN + case providers.DeferredReasonResourceConfigUnknown: + deferred.Reason = tfstackdata1.PlanDeferredResourceInstanceChange_Deferred_RESOURCE_CONFIG_UNKNOWN + case providers.DeferredReasonProviderConfigUnknown: + deferred.Reason = tfstackdata1.PlanDeferredResourceInstanceChange_Deferred_PROVIDER_CONFIG_UNKNOWN + case providers.DeferredReasonAbsentPrereq: + deferred.Reason = tfstackdata1.PlanDeferredResourceInstanceChange_Deferred_ABSENT_PREREQ + case providers.DeferredReasonDeferredPrereq: + deferred.Reason = tfstackdata1.PlanDeferredResourceInstanceChange_Deferred_DEFERRED_PREREQ + default: + deferred.Reason = tfstackdata1.PlanDeferredResourceInstanceChange_Deferred_INVALID + } + + var raw anypb.Any + err = anypb.MarshalFrom(&raw, &tfstackdata1.PlanDeferredResourceInstanceChange{ + Change: change, + Deferred: &deferred, + }, proto.MarshalOptions{}) + if err != nil { + return nil, err + } + ricd, err := dpc.ResourceInstancePlanned.ChangeDesciption() + if err != nil { + return nil, err + } + + var deferred2 terraform1.Deferred + switch dpc.DeferredReason { + case providers.DeferredReasonInstanceCountUnknown: + deferred2.Reason = terraform1.Deferred_INSTANCE_COUNT_UNKNOWN + case providers.DeferredReasonResourceConfigUnknown: + deferred2.Reason = terraform1.Deferred_RESOURCE_CONFIG_UNKNOWN + case providers.DeferredReasonProviderConfigUnknown: + deferred2.Reason = terraform1.Deferred_PROVIDER_CONFIG_UNKNOWN + case providers.DeferredReasonAbsentPrereq: + deferred2.Reason = terraform1.Deferred_ABSENT_PREREQ + case providers.DeferredReasonDeferredPrereq: + deferred2.Reason = terraform1.Deferred_DEFERRED_PREREQ + default: + deferred2.Reason = terraform1.Deferred_INVALID + } + + var descs []*terraform1.PlannedChange_ChangeDescription + descs = append(descs, &terraform1.PlannedChange_ChangeDescription{ + Description: &terraform1.PlannedChange_ChangeDescription_ResourceInstanceDeferred{ + ResourceInstanceDeferred: &terraform1.PlannedChange_ResourceInstanceDeferred{ + ResourceInstance: ricd.GetResourceInstancePlanned(), + Deferred: &deferred2, + }, + }, + }) + return &terraform1.PlannedChange{ Raw: []*anypb.Any{&raw}, Descriptions: descs, diff --git a/internal/stacks/stackruntime/internal/stackeval/component_instance.go b/internal/stacks/stackruntime/internal/stackeval/component_instance.go index b4eba207ba56..6267dbeaa499 100644 --- a/internal/stacks/stackruntime/internal/stackeval/component_instance.go +++ b/internal/stacks/stackruntime/internal/stackeval/component_instance.go @@ -1344,6 +1344,52 @@ func (c *ComponentInstance) PlanChanges(ctx context.Context) ([]stackplan.Planne seenObjects.Add(addr) } } + + // We need to keep track of the deferred changes as well + for _, dr := range corePlan.DeferredResources { + rsrcChange := dr.ChangeSrc + objAddr := addrs.AbsResourceInstanceObject{ + ResourceInstance: rsrcChange.Addr, + DeposedKey: rsrcChange.DeposedKey, + } + var priorStateSrc *states.ResourceInstanceObjectSrc + if corePlan.PriorState != nil { + priorStateSrc = corePlan.PriorState.ResourceInstanceObjectSrc(objAddr) + } + + schema, err := c.resourceTypeSchema( + ctx, + rsrcChange.ProviderAddr.Provider, + rsrcChange.Addr.Resource.Resource.Mode, + rsrcChange.Addr.Resource.Resource.Type, + ) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Can't fetch provider schema to save plan", + fmt.Sprintf( + "Failed to retrieve the schema for %s from provider %s: %s. This is a bug in Terraform.", + rsrcChange.Addr, rsrcChange.ProviderAddr.Provider, err, + ), + )) + continue + } + + plannedChangeResourceInstance := stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: c.Addr(), + Item: objAddr, + }, + ChangeSrc: rsrcChange, + Schema: schema, + PriorStateSrc: priorStateSrc, + ProviderConfigAddr: rsrcChange.ProviderAddr, + } + changes = append(changes, &stackplan.PlannedChangeDeferredResourceInstancePlanned{ + DeferredReason: dr.DeferredReason, + ResourceInstancePlanned: plannedChangeResourceInstance, + }) + } } return changes, diags