Skip to content

Commit

Permalink
stacks: track deferred changes in stackplan
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielMSchmidt committed May 7, 2024
1 parent 7052f28 commit a31bb9f
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 7 deletions.
4 changes: 2 additions & 2 deletions internal/plans/planfile/tfplan.go
Expand Up @@ -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
}
Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions internal/stacks/stackplan/component.go
Expand Up @@ -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"
Expand Down
81 changes: 78 additions & 3 deletions internal/stacks/stackplan/from_proto.go
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
76 changes: 76 additions & 0 deletions internal/stacks/stackplan/planned_change.go
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down
Expand Up @@ -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
Expand Down

0 comments on commit a31bb9f

Please sign in to comment.