track partial expanded action invocations through deferred action invocation

like we do it for resources just simpler because we don't have layered addrs maps
pull/37547/head
Daniel Schmidt 9 months ago
parent 68ad276d97
commit ad4371342f

@ -924,6 +924,16 @@ func (a *AbsAction) UnexpandedAction(action Action) PartialExpandedAction {
}
}
// UnknownActionInstance returns an [AbsActionInstance] that represents the
// same action as the receiver but with all instance keys replaced with a
// wildcard value.
func (per PartialExpandedAction) UnknownActionInstance() AbsActionInstance {
return AbsActionInstance{
Module: per.module.UnknownModuleInstance(),
Action: per.action.Instance(WildcardKey),
}
}
func (pea PartialExpandedAction) String() string {
moduleAddr := pea.module.String()
if len(moduleAddr) != 0 {

@ -234,98 +234,3 @@ func MarshalDeferredActionInvocations(dais []*plans.DeferredActionInvocationSrc,
}
return deferredInvocations, nil
}
func MarshalDeferredPartialActionInvocations(dais []*plans.DeferredPartialExpandedActionInvocationSrc, schemas *terraform.Schemas) ([]DeferredActionInvocation, error) {
var deferredInvocations []DeferredActionInvocation
for _, daiSrc := range dais {
ai, err := MarshalPartialActionInvocation(daiSrc.ActionInvocationInstanceSrc, schemas)
if err != nil {
return nil, err
}
dai := DeferredActionInvocation{
ActionInvocation: ai,
}
switch daiSrc.DeferredReason {
case providers.DeferredReasonInstanceCountUnknown:
dai.Reason = DeferredReasonInstanceCountUnknown
case providers.DeferredReasonResourceConfigUnknown:
dai.Reason = DeferredReasonResourceConfigUnknown
case providers.DeferredReasonProviderConfigUnknown:
dai.Reason = DeferredReasonProviderConfigUnknown
case providers.DeferredReasonAbsentPrereq:
dai.Reason = DeferredReasonAbsentPrereq
case providers.DeferredReasonDeferredPrereq:
dai.Reason = DeferredReasonDeferredPrereq
default:
// If we find a reason we don't know about, we'll just mark it as
// unknown. This is a bit of a safety net to ensure that we don't
// break if new reasons are introduced in future versions of the
// provider protocol.
dai.Reason = DeferredReasonUnknown
}
deferredInvocations = append(deferredInvocations, dai)
}
return deferredInvocations, nil
}
func MarshalPartialActionInvocation(action *plans.PartialExpandedActionInvocationInstanceSrc, schemas *terraform.Schemas) (ActionInvocation, error) {
ai := ActionInvocation{
Address: action.Addr.String(),
Type: action.Addr.ConfigAction().Action.Type,
Name: action.Addr.ConfigAction().Action.Name,
ProviderName: action.ProviderAddr.Provider.String(),
}
schema := schemas.ActionTypeConfig(
action.ProviderAddr.Provider,
action.Addr.ConfigAction().Action.Type,
)
if schema.ConfigSchema == nil {
return ai, fmt.Errorf("no schema found for %s (in provider %s)", action.Addr.ConfigAction().Action.Type, action.ProviderAddr.Provider)
}
actionDec, err := action.Decode(&schema)
if err != nil {
return ai, fmt.Errorf("failed to decode action %s: %w", action.Addr, err)
}
switch at := action.ActionTrigger.(type) {
case plans.PartialLifecycleActionTrigger:
ai.LifecycleActionTrigger = &LifecycleActionTrigger{
TriggeringResourceAddress: at.TriggeringResourceAddr.String(),
ActionTriggerEvent: at.TriggerEvent().String(),
ActionTriggerBlockIndex: at.ActionTriggerBlockIndex,
ActionsListIndex: at.ActionsListIndex,
}
default:
return ai, fmt.Errorf("unsupported action trigger type: %T", at)
}
if actionDec.ConfigValue != cty.NilVal {
_, pvms := actionDec.ConfigValue.UnmarkDeepWithPaths()
sensitivePaths, otherMarks := marks.PathsWithMark(pvms, marks.Sensitive)
ephemeralPaths, otherMarks := marks.PathsWithMark(otherMarks, marks.Ephemeral)
if len(ephemeralPaths) > 0 {
return ai, fmt.Errorf("action %s has ephemeral config values, which are not supported in action invocations", action.Addr)
}
if len(otherMarks) > 0 {
return ai, fmt.Errorf("action %s has config values with unsupported marks: %v", action.Addr, otherMarks)
}
configValue := actionDec.ConfigValue
if !configValue.IsWhollyKnown() {
configValue = omitUnknowns(actionDec.ConfigValue)
}
cs := jsonstate.SensitiveAsBool(marks.MarkPaths(configValue, marks.Sensitive, sensitivePaths))
configSensitive, err := ctyjson.Marshal(cs, cs.Type())
if err != nil {
return ai, err
}
ai.ConfigValues = marshalConfigValues(configValue)
ai.ConfigSensitive = configSensitive
}
return ai, nil
}

@ -320,14 +320,6 @@ func Marshal(
}
}
if p.DeferredPartialActionInvocations != nil {
deferredPartialActionInvocations, err := MarshalDeferredPartialActionInvocations(p.DeferredPartialActionInvocations, schemas)
if err != nil {
return nil, fmt.Errorf("error in marshaling deferred partial action invocations: %s", err)
}
output.DeferredActionInvocations = append(output.DeferredActionInvocations, deferredPartialActionInvocations...)
}
// output.OutputChanges
if output.OutputChanges, err = MarshalOutputChanges(p.Changes); err != nil {
return nil, fmt.Errorf("error in marshaling output changes: %s", err)

@ -180,167 +180,3 @@ func (ai *ActionInvocationInstance) DeepCopy() *ActionInvocationInstance {
ret := *ai
return &ret
}
// PartialActionTrigger is the equivalent of ActionTrigger but allows the
// triggering address to be only partially expanded. This is used during earlier
// phases of planning when (for example) count/for_each expansions are not yet
// fully resolved.
type PartialActionTrigger interface {
partialActionTriggerSigil()
TriggerEvent() configs.ActionTriggerEvent
String() string
Equals(other PartialActionTrigger) bool
}
// PartialLifecycleActionTrigger is the partial-expanded form of
// LifecycleActionTrigger. It differs only in that it stores a partial-expanded
// resource instance address for the triggering resource.
type PartialLifecycleActionTrigger struct {
TriggeringResourceAddr addrs.PartialExpandedResource
ActionTriggerEvent configs.ActionTriggerEvent
ActionTriggerBlockIndex int
ActionsListIndex int
}
func (t PartialLifecycleActionTrigger) partialActionTriggerSigil() {}
func (t PartialLifecycleActionTrigger) TriggerEvent() configs.ActionTriggerEvent {
return t.ActionTriggerEvent
}
func (t PartialLifecycleActionTrigger) String() string {
return t.TriggeringResourceAddr.String()
}
func (t PartialLifecycleActionTrigger) Equals(other PartialActionTrigger) bool {
o, ok := other.(*PartialLifecycleActionTrigger)
if !ok {
return false
}
pomt, tIsPartial := t.TriggeringResourceAddr.PartialExpandedModule()
pemo, oIsPartial := o.TriggeringResourceAddr.PartialExpandedModule()
if tIsPartial != oIsPartial {
return false
}
return pomt.MatchesPartial(pemo) && t.TriggeringResourceAddr.Resource().Equal(o.TriggeringResourceAddr.Resource()) &&
t.ActionTriggerEvent == o.ActionTriggerEvent &&
t.ActionTriggerBlockIndex == o.ActionTriggerBlockIndex &&
t.ActionsListIndex == o.ActionsListIndex
}
var _ PartialActionTrigger = (*PartialLifecycleActionTrigger)(nil)
// PartialExpandedActionInvocationInstance mirrors ActionInvocationInstance
// but keeps the action and/or trigger resource addresses in a
// partial-expanded form until all dynamic expansions (count, for_each, etc.)
// are resolved.
type PartialExpandedActionInvocationInstance struct {
Addr addrs.PartialExpandedAction
ActionTrigger PartialActionTrigger
ProviderAddr addrs.AbsProviderConfig
ConfigValue cty.Value
}
// DeepCopy creates a defensive copy of the partial-expanded invocation.
func (pii *PartialExpandedActionInvocationInstance) DeepCopy() *PartialExpandedActionInvocationInstance {
if pii == nil {
return pii
}
ret := *pii
return &ret
}
// Equals compares two partial-expanded invocation instances.
func (pii *PartialExpandedActionInvocationInstance) Equals(other *PartialExpandedActionInvocationInstance) bool {
if pii == nil || other == nil {
return pii == other
}
// We compare the (partial) action address and the trigger (which may also
// embed a partial address).
addrEqual := pii.Addr.Equal(other.Addr)
triggerEqual := false
if pii.ActionTrigger == nil && other.ActionTrigger == nil {
triggerEqual = true
} else if pii.ActionTrigger != nil && other.ActionTrigger != nil {
triggerEqual = pii.ActionTrigger.Equals(other.ActionTrigger)
}
return addrEqual && triggerEqual
}
type PartialExpandedActionInvocationInstanceSrc struct {
Addr addrs.PartialExpandedAction
ActionTrigger PartialActionTrigger
ProviderAddr addrs.AbsProviderConfig
ConfigValue DynamicValue
SensitiveConfigPaths []cty.Path
}
// Encode produces a variant of the receiver that has its config value
// serialized so it can be written to a plan file while action and trigger
// addresses are still in their partial-expanded form. Pass the implied type
// of the corresponding action schema for correct operation.
func (pii *PartialExpandedActionInvocationInstance) Encode(schema *providers.ActionSchema) (*PartialExpandedActionInvocationInstanceSrc, error) {
ret := &PartialExpandedActionInvocationInstanceSrc{
Addr: pii.Addr,
ActionTrigger: pii.ActionTrigger,
ProviderAddr: pii.ProviderAddr,
}
if pii.ConfigValue != cty.NilVal {
ty := cty.DynamicPseudoType
if schema != nil {
ty = schema.ConfigSchema.ImpliedType()
}
unmarkedConfigValue, pvms := pii.ConfigValue.UnmarkDeepWithPaths()
sensitivePaths, otherMarks := marks.PathsWithMark(pvms, marks.Sensitive)
if len(otherMarks) > 0 {
return nil, fmt.Errorf("%s: error serializing partial-expanded action invocation with unexpected marks on config value: %#v. This is a bug in Terraform.", tfdiags.FormatCtyPath(otherMarks[0].Path), otherMarks[0].Marks)
}
var err error
ret.ConfigValue, err = NewDynamicValue(unmarkedConfigValue, ty)
ret.SensitiveConfigPaths = sensitivePaths
if err != nil {
return nil, err
}
}
return ret, nil
}
// Decode produces an in-memory form of the serialized partial-expanded action
// invocation instance using the provided schema to infer the original config
// value type.
func (src *PartialExpandedActionInvocationInstanceSrc) Decode(schema *providers.ActionSchema) (*PartialExpandedActionInvocationInstance, error) {
ret := &PartialExpandedActionInvocationInstance{
Addr: src.Addr,
ActionTrigger: src.ActionTrigger,
ProviderAddr: src.ProviderAddr,
}
if src.ConfigValue != nil {
ty := cty.DynamicPseudoType
if schema != nil {
ty = schema.ConfigSchema.ImpliedType()
}
val, err := src.ConfigValue.Decode(ty)
if err != nil {
return nil, err
}
if len(src.SensitiveConfigPaths) > 0 {
val = marks.MarkPaths(val, marks.Sensitive, src.SensitiveConfigPaths)
}
ret.ConfigValue = val
}
return ret, nil
}

@ -96,53 +96,3 @@ func (dais *DeferredActionInvocationSrc) Decode(schema *providers.ActionSchema)
ActionInvocationInstance: instance,
}, nil
}
// DeferredPartialExpandedActionInvocation tracks information about an action
// invocation that has been deferred for some reason, where the underlying
// ActionInvocationInstance contains a partially expanded address (and
// LifecycleActionTrigger).
type DeferredPartialExpandedActionInvocation struct {
// DeferredReason is the reason why this action invocation was deferred.
DeferredReason providers.DeferredReason
// ActionInvocationInstance is the (partially expanded) instance of the action
// invocation that was deferred. Its Addr (and any embedded
// LifecycleActionTrigger addresses) are partial.
ActionInvocationInstance *PartialExpandedActionInvocationInstance
}
func (dai *DeferredPartialExpandedActionInvocation) Encode(schema *providers.ActionSchema) (*DeferredPartialExpandedActionInvocationSrc, error) {
src, err := dai.ActionInvocationInstance.Encode(schema)
if err != nil {
return nil, err
}
return &DeferredPartialExpandedActionInvocationSrc{
DeferredReason: dai.DeferredReason,
ActionInvocationInstanceSrc: src,
}, nil
}
// DeferredPartialExpandedActionInvocationSrc is the serialized form of
// DeferredPartialExpandedActionInvocation.
type DeferredPartialExpandedActionInvocationSrc struct {
// DeferredReason is the reason why this action invocation was deferred.
DeferredReason providers.DeferredReason
// ActionInvocationInstanceSrc is the (partially expanded) instance of the
// action invocation that was deferred. Its Addr (and any embedded
// LifecycleActionTrigger addresses) are partial.
ActionInvocationInstanceSrc *PartialExpandedActionInvocationInstanceSrc
}
func (dais *DeferredPartialExpandedActionInvocationSrc) Decode(schema *providers.ActionSchema) (*DeferredPartialExpandedActionInvocation, error) {
instance, err := dais.ActionInvocationInstanceSrc.Decode(schema)
if err != nil {
return nil, err
}
return &DeferredPartialExpandedActionInvocation{
DeferredReason: dais.DeferredReason,
ActionInvocationInstance: instance,
}, nil
}

@ -136,19 +136,6 @@ type Deferred struct {
// instance should be considered deferred.
partialExpandedActionsDeferred addrs.Map[addrs.ConfigAction, addrs.Map[addrs.PartialExpandedAction, providers.DeferredReason]]
// partialExpandedActionInvocationsDeferred tracks action invocation evaluations
// whose concrete action instance addresses could not yet be fully determined
// (for example because the action block contains for_each / count style
// expressions, or references values that depend on other deferred changes).
//
// Each element mirrors (in a more lightweight form) the information we
// retain for fully-expanded deferred action invocations, but is specific
// to the partially-expanded (wildcard / unknown) address space. Once the
// address space becomes concrete in a subsequent planning round the
// corresponding concrete action invocations will either be planned or
// recorded in actionInvocationDeferred instead.
partialExpandedActionInvocationsDeferred []*plans.DeferredPartialExpandedActionInvocation
// partialExpandedModulesDeferred tracks all of the partial-expanded module
// prefixes we were notified about.
//
@ -180,7 +167,6 @@ func NewDeferred(enabled bool) *Deferred {
partialExpandedDataSourcesDeferred: addrs.MakeMap[addrs.ConfigResource, addrs.Map[addrs.PartialExpandedResource, *plans.DeferredResourceInstanceChange]](),
partialExpandedEphemeralResourceDeferred: addrs.MakeMap[addrs.ConfigResource, addrs.Map[addrs.PartialExpandedResource, *plans.DeferredResourceInstanceChange]](),
partialExpandedActionsDeferred: addrs.MakeMap[addrs.ConfigAction, addrs.Map[addrs.PartialExpandedAction, providers.DeferredReason]](),
partialExpandedActionInvocationsDeferred: []*plans.DeferredPartialExpandedActionInvocation{},
partialExpandedModulesDeferred: addrs.MakeSet[addrs.PartialExpandedModule](),
}
}
@ -222,11 +208,6 @@ func (d *Deferred) GetDeferredActionInvocations() []*plans.DeferredActionInvocat
return d.actionInvocationDeferred
}
// GetDeferredPartialActionInvocations returns a list of all deferred partial action invocations.
func (d *Deferred) GetDeferredPartialActionInvocations() []*plans.DeferredPartialExpandedActionInvocation {
return d.partialExpandedActionInvocationsDeferred
}
// SetExternalDependencyDeferred modifies a freshly-constructed [Deferred]
// so that it will consider all resource instances as needing their actions
// deferred, even if there's no other reason to do that.
@ -268,8 +249,7 @@ func (d *Deferred) HaveAnyDeferrals() bool {
d.partialExpandedDataSourcesDeferred.Len() != 0 ||
d.partialExpandedEphemeralResourceDeferred.Len() != 0 ||
d.partialExpandedActionsDeferred.Len() != 0 ||
len(d.partialExpandedModulesDeferred) != 0 ||
len(d.partialExpandedActionInvocationsDeferred) != 0)
len(d.partialExpandedModulesDeferred) != 0)
}
// GetDeferredResourceInstanceValue returns the deferred value for the given
@ -749,27 +729,6 @@ func (d *Deferred) ReportActionDeferred(addr addrs.AbsActionInstance, reason pro
configMap.Put(addr, reason)
}
func (d *Deferred) ReportPartialActionInvocationDeferred(ai plans.PartialExpandedActionInvocationInstance, reason providers.DeferredReason) {
d.mu.Lock()
defer d.mu.Unlock()
// Check if the action invocation is already deferred
for _, deferred := range d.partialExpandedActionInvocationsDeferred {
if deferred.ActionInvocationInstance.Equals(&ai) {
// This indicates a bug in the caller, since our graph walk should
// ensure that we visit and evaluate each distinct action invocation
// only once.
panic(fmt.Sprintf("duplicate deferral report for action %s invoked by %s", ai.Addr.String(), ai.ActionTrigger.TriggerEvent().String()))
}
}
d.partialExpandedActionInvocationsDeferred = append(d.partialExpandedActionInvocationsDeferred, &plans.DeferredPartialExpandedActionInvocation{
ActionInvocationInstance: &ai,
DeferredReason: reason,
})
}
// ShouldDeferActionInvocation returns true if there is a reason to defer the action invocation instance
// We want to defer an action invocation if
// a) the resource was deferred

@ -66,14 +66,13 @@ type Plan struct {
VariableMarks map[string][]cty.PathValueMarks
ApplyTimeVariables collections.Set[string]
Changes *ChangesSrc
DriftedResources []*ResourceInstanceChangeSrc
DeferredResources []*DeferredResourceInstanceChangeSrc
DeferredActionInvocations []*DeferredActionInvocationSrc
DeferredPartialActionInvocations []*DeferredPartialExpandedActionInvocationSrc
TargetAddrs []addrs.Targetable
ActionTargetAddrs []addrs.Targetable
ForceReplaceAddrs []addrs.AbsResourceInstance
Changes *ChangesSrc
DriftedResources []*ResourceInstanceChangeSrc
DeferredResources []*DeferredResourceInstanceChangeSrc
DeferredActionInvocations []*DeferredActionInvocationSrc
TargetAddrs []addrs.Targetable
ActionTargetAddrs []addrs.Targetable
ForceReplaceAddrs []addrs.AbsResourceInstance
Backend Backend
StateStore StateStore

@ -885,10 +885,6 @@ func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, o
deferredActionInvocations, deferredActionInvocationsDiags := c.deferredActionInvocations(schemas, walker.Deferrals.GetDeferredActionInvocations())
diags = diags.Append(deferredActionInvocationsDiags)
plan.DeferredActionInvocations = deferredActionInvocations
deferredPartialActionInvocations, deferredPartialActionInvocationsDiags := c.deferredPartialActionInvocations(schemas, walker.Deferrals.GetDeferredPartialActionInvocations())
diags = diags.Append(deferredPartialActionInvocationsDiags)
plan.DeferredPartialActionInvocations = deferredPartialActionInvocations
}
// Our final rulings on whether the plan is "complete" and "applyable".
@ -977,26 +973,6 @@ func (c *Context) deferredActionInvocations(schemas *Schemas, deferrals []*plans
return deferredActionInvocations, diags
}
func (c *Context) deferredPartialActionInvocations(schemas *Schemas, deferrals []*plans.DeferredPartialExpandedActionInvocation) ([]*plans.DeferredPartialExpandedActionInvocationSrc, tfdiags.Diagnostics) {
var deferredPartialActionInvocations []*plans.DeferredPartialExpandedActionInvocationSrc
var diags tfdiags.Diagnostics
for _, deferral := range deferrals {
schema := schemas.ActionTypeConfig(deferral.ActionInvocationInstance.ProviderAddr.Provider, deferral.ActionInvocationInstance.Addr.ConfigAction().Action.Type)
deferralSrc, err := deferral.Encode(&schema)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to prepare deferred partial action invocation for plan",
fmt.Sprintf("The deferred partial action invocation %q could not be serialized to store in the plan: %s.", deferral.ActionInvocationInstance.Addr, err)))
continue
}
deferredPartialActionInvocations = append(deferredPartialActionInvocations, deferralSrc)
}
return deferredPartialActionInvocations, diags
}
func (c *Context) planGraph(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*Graph, walkOperation, tfdiags.Diagnostics) {
var externalProviderConfigs map[addrs.RootProviderConfig]providers.Interface
if opts != nil {

@ -2912,14 +2912,10 @@ resource "other_object" "a" {
t.Fatalf("expected 0 planned action invocations, got %d", got)
}
if got := len(p.DeferredActionInvocations); got != 0 {
t.Fatalf("expected 0 deferred action invocations, got %d", got)
}
if got := len(p.DeferredPartialActionInvocations); got != 1 {
if got := len(p.DeferredActionInvocations); got != 1 {
t.Fatalf("expected 1 deferred action invocations, got %d", got)
}
ac, err := p.DeferredPartialActionInvocations[0].Decode(&unlinkedActionSchema)
ac, err := p.DeferredActionInvocations[0].Decode(&unlinkedActionSchema)
if err != nil {
t.Fatalf("error decoding action invocation: %s", err)
}
@ -2990,15 +2986,12 @@ resource "other_object" "a" {
if len(p.Changes.ActionInvocations) != 0 {
t.Fatalf("expected 0 planned action invocations, got %d", len(p.Changes.ActionInvocations))
}
if got := len(p.DeferredActionInvocations); got != 0 {
t.Fatalf("expected 0 deferred action invocations, got %d", got)
}
if len(p.DeferredPartialActionInvocations) != 1 {
t.Fatalf("expected 1 deferred partial action invocations, got %d", len(p.DeferredPartialActionInvocations))
if len(p.DeferredActionInvocations) != 1 {
t.Fatalf("expected 1 deferred partial action invocations, got %d", len(p.DeferredActionInvocations))
}
ac, err := p.DeferredPartialActionInvocations[0].Decode(&unlinkedActionSchema)
ac, err := p.DeferredActionInvocations[0].Decode(&unlinkedActionSchema)
if err != nil {
t.Fatalf("error decoding action invocation: %s", err)
}

@ -114,11 +114,11 @@ func (n *NodeActionTriggerPartialExpanded) Execute(ctx EvalContext, op walkOpera
return diags
}
ctx.Deferrals().ReportPartialActionInvocationDeferred(plans.PartialExpandedActionInvocationInstance{
Addr: n.addr,
ctx.Deferrals().ReportActionInvocationDeferred(plans.ActionInvocationInstance{
Addr: n.addr.UnknownActionInstance(),
ProviderAddr: n.resolvedProvider,
ActionTrigger: plans.PartialLifecycleActionTrigger{
TriggeringResourceAddr: n.lifecycleActionTrigger.resourceAddress,
ActionTrigger: &plans.LifecycleActionTrigger{
TriggeringResourceAddr: n.lifecycleActionTrigger.resourceAddress.UnknownResourceInstance(),
ActionTriggerEvent: *triggeringEvent,
ActionTriggerBlockIndex: n.lifecycleActionTrigger.actionTriggerBlockIndex,
ActionsListIndex: n.lifecycleActionTrigger.actionListIndex,

Loading…
Cancel
Save