diff --git a/internal/actions/actions.go b/internal/actions/actions.go index 27851a45d6..5073c1aebb 100644 --- a/internal/actions/actions.go +++ b/internal/actions/actions.go @@ -16,12 +16,14 @@ type Actions struct { // Must hold this lock when accessing all fields after this one. mu sync.Mutex - actionInstances addrs.Map[addrs.AbsActionInstance, ActionData] + actionInstances addrs.Map[addrs.AbsActionInstance, ActionData] + partialExpandedActions addrs.Map[addrs.PartialExpandedAction, ActionData] } func NewActions() *Actions { return &Actions{ - actionInstances: addrs.MakeMap[addrs.AbsActionInstance, ActionData](), + actionInstances: addrs.MakeMap[addrs.AbsActionInstance, ActionData](), + partialExpandedActions: addrs.MakeMap[addrs.PartialExpandedAction, ActionData](), } } @@ -70,3 +72,30 @@ func (a *Actions) GetActionInstanceKeys(addr addrs.AbsAction) []addrs.AbsActionI return result } + +func (a *Actions) AddPartialExpandedAction(addr addrs.PartialExpandedAction, configValue cty.Value, providerAddr addrs.AbsProviderConfig) { + a.mu.Lock() + defer a.mu.Unlock() + + if a.partialExpandedActions.Has(addr) { + panic("action instance already exists: " + addr.String()) + } + + a.partialExpandedActions.Put(addr, ActionData{ + ConfigValue: configValue, + ProviderAddr: providerAddr, + }) +} + +func (a *Actions) GetPartialExpandedAction(addr addrs.PartialExpandedAction) (*ActionData, bool) { + a.mu.Lock() + defer a.mu.Unlock() + + data, ok := a.partialExpandedActions.GetOk(addr) + + if !ok { + return nil, false + } + + return &data, true +} diff --git a/internal/addrs/partial_expanded.go b/internal/addrs/partial_expanded.go index 3ca3a5c982..673f226525 100644 --- a/internal/addrs/partial_expanded.go +++ b/internal/addrs/partial_expanded.go @@ -932,6 +932,10 @@ func (pea PartialExpandedAction) String() string { return pea.action.String() + "[*]" } +func (pea PartialExpandedAction) Equal(other PartialExpandedAction) bool { + return pea.module.MatchesPartial(other.module.expandedPrefix.PartialModule()) && pea.action.Equal(other.action) +} + func (pea PartialExpandedAction) UniqueKey() UniqueKey { // If this address is equivalent to an AbsAction address then we'll // return its instance key here so that function Equivalent will consider diff --git a/internal/command/jsonplan/action_invocations.go b/internal/command/jsonplan/action_invocations.go index 9f15db30ef..8445ff6a15 100644 --- a/internal/command/jsonplan/action_invocations.go +++ b/internal/command/jsonplan/action_invocations.go @@ -234,3 +234,103 @@ func MarshalDeferredActionInvocations(dais []*plans.DeferredActionInvocationSrc, } return deferredInvocations, nil } + +func MarshalDeferredPartialActionInvocations(dais []*plans.DeferredPartialExpandedActionInvocationSrc, schemas *terraform.Schemas) ([]DeferredActionInvocation, error) { + var deferredInvocations []DeferredActionInvocation + + // sortedActions := append([]*plans.DeferredActionInvocationSrc{}, dais...) + // sort.Slice(sortedActions, func(i, j int) bool { + // return sortedActions[i].ActionInvocationInstanceSrc.Less(sortedActions[j].ActionInvocationInstanceSrc) + // }) + + 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 +} diff --git a/internal/command/jsonplan/plan.go b/internal/command/jsonplan/plan.go index 62f9d8c641..637b0ed4ed 100644 --- a/internal/command/jsonplan/plan.go +++ b/internal/command/jsonplan/plan.go @@ -320,6 +320,14 @@ 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) diff --git a/internal/plans/action_invocation.go b/internal/plans/action_invocation.go index d28e5e6094..d50c71d09b 100644 --- a/internal/plans/action_invocation.go +++ b/internal/plans/action_invocation.go @@ -180,3 +180,167 @@ 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 +} diff --git a/internal/plans/deferring.go b/internal/plans/deferring.go index 2ead6dee54..ec6a38b86a 100644 --- a/internal/plans/deferring.go +++ b/internal/plans/deferring.go @@ -96,3 +96,53 @@ 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 +} diff --git a/internal/plans/deferring/deferred.go b/internal/plans/deferring/deferred.go index 9560fee015..42a2d30ad0 100644 --- a/internal/plans/deferring/deferred.go +++ b/internal/plans/deferring/deferred.go @@ -124,6 +124,8 @@ type Deferred struct { partialExpandedActionsDeferred addrs.Map[addrs.ConfigAction, addrs.Map[addrs.PartialExpandedAction, *plans.DeferredResourceInstanceChange]] + partialExpandedActionInvocationsDeferred []*plans.DeferredPartialExpandedActionInvocation + // partialExpandedModulesDeferred tracks all of the partial-expanded module // prefixes we were notified about. // @@ -155,6 +157,7 @@ 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, *plans.DeferredResourceInstanceChange]](), + partialExpandedActionInvocationsDeferred: []*plans.DeferredPartialExpandedActionInvocation{}, partialExpandedModulesDeferred: addrs.MakeSet[addrs.PartialExpandedModule](), } } @@ -196,6 +199,11 @@ 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. @@ -237,7 +245,8 @@ func (d *Deferred) HaveAnyDeferrals() bool { d.partialExpandedDataSourcesDeferred.Len() != 0 || d.partialExpandedEphemeralResourceDeferred.Len() != 0 || d.partialExpandedActionsDeferred.Len() != 0 || - len(d.partialExpandedModulesDeferred) != 0) + len(d.partialExpandedModulesDeferred) != 0) || + len(d.partialExpandedActionInvocationsDeferred) != 0 } // GetDeferredResourceInstanceValue returns the deferred value for the given @@ -323,6 +332,18 @@ func (d *Deferred) GetDeferredResourceInstances(addr addrs.AbsResource) map[addr return result } +func (d *Deferred) GetDeferredPartialExpandedResource(addr addrs.PartialExpandedResource) *plans.DeferredResourceInstanceChange { + d.mu.Lock() + defer d.mu.Unlock() + + item, ok := d.partialExpandedResourcesDeferred.GetOk(addr.ConfigResource()) + if !ok { + return nil + } + + return item.Get(addr) +} + // ShouldDeferResourceInstanceChanges returns true if the receiver knows some // reason why the resource instance with the given address should have its // planned action deferred for a future plan/apply round. @@ -708,6 +729,27 @@ 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 diff --git a/internal/plans/plan.go b/internal/plans/plan.go index 42a1ba4851..51db276764 100644 --- a/internal/plans/plan.go +++ b/internal/plans/plan.go @@ -66,13 +66,14 @@ type Plan struct { VariableMarks map[string][]cty.PathValueMarks ApplyTimeVariables collections.Set[string] - Changes *ChangesSrc - DriftedResources []*ResourceInstanceChangeSrc - DeferredResources []*DeferredResourceInstanceChangeSrc - DeferredActionInvocations []*DeferredActionInvocationSrc - TargetAddrs []addrs.Targetable - ActionTargetAddrs []addrs.Targetable - ForceReplaceAddrs []addrs.AbsResourceInstance + Changes *ChangesSrc + DriftedResources []*ResourceInstanceChangeSrc + DeferredResources []*DeferredResourceInstanceChangeSrc + DeferredActionInvocations []*DeferredActionInvocationSrc + DeferredPartialActionInvocations []*DeferredPartialExpandedActionInvocationSrc + TargetAddrs []addrs.Targetable + ActionTargetAddrs []addrs.Targetable + ForceReplaceAddrs []addrs.AbsResourceInstance Backend Backend StateStore StateStore diff --git a/internal/terraform/context_plan.go b/internal/terraform/context_plan.go index cf1ea9a196..f7cf1cc31d 100644 --- a/internal/terraform/context_plan.go +++ b/internal/terraform/context_plan.go @@ -885,6 +885,10 @@ 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". @@ -973,6 +977,26 @@ 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 { diff --git a/internal/terraform/node_action.go b/internal/terraform/node_action.go index bb7c02d1a9..9647e754bb 100644 --- a/internal/terraform/node_action.go +++ b/internal/terraform/node_action.go @@ -45,12 +45,13 @@ func (n *nodeExpandActionDeclaration) DynamicExpand(ctx EvalContext) (*Graph, tf pem := expander.UnknownModuleInstances(n.Addr.Module, false) for _, moduleAddr := range pem { - resourceAddr := moduleAddr.Action(n.Addr.Action) + actionAddr := moduleAddr.Action(n.Addr.Action) // And add a node to the graph for this action. g.Add(&NodeActionDeclarationPartialExpanded{ - addr: resourceAddr, + addr: actionAddr, config: n.Config, + Schema: n.Schema, resolvedProvider: n.ResolvedProvider, }) } diff --git a/internal/terraform/node_action_partialexp.go b/internal/terraform/node_action_partialexp.go index a70f8c69e9..f1abe3e20f 100644 --- a/internal/terraform/node_action_partialexp.go +++ b/internal/terraform/node_action_partialexp.go @@ -6,7 +6,10 @@ package terraform import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" ) // NodeActionDeclarationPartialExpanded is a graph node that stands in for @@ -21,6 +24,7 @@ import ( type NodeActionDeclarationPartialExpanded struct { addr addrs.PartialExpandedAction config configs.Action + Schema *providers.ActionSchema resolvedProvider addrs.AbsProviderConfig } @@ -53,6 +57,18 @@ func (n *NodeActionDeclarationPartialExpanded) ActionAddr() addrs.ConfigAction { // Execute implements GraphNodeExecutable. func (n *NodeActionDeclarationPartialExpanded) Execute(ctx EvalContext, op walkOperation) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics ctx.Deferrals().ReportActionExpansionDeferred(n.addr) + configVal := cty.NullVal(n.Schema.ConfigSchema.ImpliedType()) + if n.config.Config != nil { + var configDiags tfdiags.Diagnostics + configVal, _, configDiags = ctx.EvaluateBlock(n.config.Config, n.Schema.ConfigSchema.DeepCopy(), nil, instances.TotallyUnknownRepetitionData) + + diags = diags.Append(configDiags) + if diags.HasErrors() { + return diags + } + } + ctx.Actions().AddPartialExpandedAction(n.addr, configVal, n.resolvedProvider) return nil } diff --git a/internal/terraform/node_action_trigger_instance_plan.go b/internal/terraform/node_action_trigger_instance_plan.go index 195b6cd2eb..13640200b2 100644 --- a/internal/terraform/node_action_trigger_instance_plan.go +++ b/internal/terraform/node_action_trigger_instance_plan.go @@ -29,9 +29,8 @@ type nodeActionTriggerPlanInstance struct { } type lifecycleActionTriggerInstance struct { - resourceAddress addrs.AbsResourceInstance - events []configs.ActionTriggerEvent - //condition hcl.Expression + resourceAddress addrs.AbsResourceInstance + events []configs.ActionTriggerEvent actionTriggerBlockIndex int actionListIndex int invokingSubject *hcl.Range diff --git a/internal/terraform/node_action_trigger_partialexp.go b/internal/terraform/node_action_trigger_partialexp.go new file mode 100644 index 0000000000..c18d52efb7 --- /dev/null +++ b/internal/terraform/node_action_trigger_partialexp.go @@ -0,0 +1,129 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// NodeActionTriggerPartialExpanded is a graph node that stands in for +// an unbounded set of potential action trigger instances that we don't yet know. +// +// Its job is to check the configuration as much as we can with the information +// that's available (so we can raise an error early if something is clearly +// wrong across _all_ potential instances) and to record a placeholder value +// for use when evaluating other objects that refer to this resource. +// +// This is the partial-expanded equivalent of NodeActionTriggerInstance. +type NodeActionTriggerPartialExpanded struct { + addr addrs.PartialExpandedAction + config *configs.Action + resolvedProvider addrs.AbsProviderConfig + lifecycleActionTrigger *lifecycleActionTriggerPartialExpanded +} + +type lifecycleActionTriggerPartialExpanded struct { + resourceAddress addrs.PartialExpandedResource + events []configs.ActionTriggerEvent + actionTriggerBlockIndex int + actionListIndex int + invokingSubject *hcl.Range +} + +func (at *lifecycleActionTriggerPartialExpanded) Name() string { + return fmt.Sprintf("%s.lifecycle.action_trigger[%d].actions[%d]", at.resourceAddress.String(), at.actionTriggerBlockIndex, at.actionListIndex) +} + +var ( + _ graphNodeEvalContextScope = (*NodeActionTriggerPartialExpanded)(nil) + _ GraphNodeExecutable = (*NodeActionTriggerPartialExpanded)(nil) +) + +// Name implements [dag.NamedVertex]. +func (n *NodeActionTriggerPartialExpanded) Name() string { + return n.addr.String() +} + +// Path implements graphNodeEvalContextScope. +func (n *NodeActionTriggerPartialExpanded) Path() evalContextScope { + if moduleAddr, ok := n.addr.ModuleInstance(); ok { + return evalContextModuleInstance{Addr: moduleAddr} + } else if moduleAddr, ok := n.addr.PartialExpandedModule(); ok { + return evalContextPartialExpandedModule{Addr: moduleAddr} + } else { + // Should not get here: at least one of the two cases above + // should always be true for any valid addrs.PartialExpandedResource + panic("addrs.PartialExpandedResource has neither a partial-expanded or a fully-expanded module instance address") + } +} + +func (n *NodeActionTriggerPartialExpanded) ActionAddr() addrs.ConfigAction { + return n.addr.ConfigAction() +} + +// Execute implements GraphNodeExecutable. +func (n *NodeActionTriggerPartialExpanded) Execute(ctx EvalContext, op walkOperation) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + // We know that if the action is partially expanded, the triggering resource must also be partially expanded. + partialResourceChange := ctx.Deferrals().GetDeferredPartialExpandedResource(n.lifecycleActionTrigger.resourceAddress) + if partialResourceChange == nil { + panic("partialResource is nil") + } + + triggeringEvent, isTriggered := actionIsTriggeredByEvent(n.lifecycleActionTrigger.events, partialResourceChange.Change.Action) + if !isTriggered { + return nil + } + + actionInstance, ok := ctx.Actions().GetPartialExpandedAction(n.addr) + if !ok { + panic("action is nil") + } + + provider, _, err := getProvider(ctx, actionInstance.ProviderAddr) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to get provider", + Detail: fmt.Sprintf("Failed to get provider: %s", err), + Subject: n.lifecycleActionTrigger.invokingSubject, + }) + + return diags + } + + // We remove the marks for planning, we will record the sensitive values in the plans.ActionInvocationInstance + unmarkedConfig, _ := actionInstance.ConfigValue.UnmarkDeepWithPaths() + + resp := provider.PlanAction(providers.PlanActionRequest{ + ActionType: n.addr.ConfigAction().Action.Type, + ProposedActionData: unmarkedConfig, + ClientCapabilities: ctx.ClientCapabilities(), + }) + + if resp.Diagnostics.HasErrors() { + diags = diags.Append(resp.Diagnostics) + return diags + } + + ctx.Deferrals().ReportPartialActionInvocationDeferred(plans.PartialExpandedActionInvocationInstance{ + Addr: n.addr, + ProviderAddr: n.resolvedProvider, + ActionTrigger: plans.PartialLifecycleActionTrigger{ + TriggeringResourceAddr: n.lifecycleActionTrigger.resourceAddress, + ActionTriggerEvent: *triggeringEvent, + ActionTriggerBlockIndex: n.lifecycleActionTrigger.actionTriggerBlockIndex, + ActionsListIndex: n.lifecycleActionTrigger.actionListIndex, + }, + ConfigValue: actionInstance.ConfigValue, + }, providers.DeferredReasonInstanceCountUnknown) + return nil +} diff --git a/internal/terraform/node_action_trigger_plan.go b/internal/terraform/node_action_trigger_plan.go index f8f75e04d2..3f36f62bad 100644 --- a/internal/terraform/node_action_trigger_plan.go +++ b/internal/terraform/node_action_trigger_plan.go @@ -63,6 +63,37 @@ func (n *nodeActionTriggerPlanExpand) DynamicExpand(ctx EvalContext) (*Graph, tf } expander := ctx.InstanceExpander() + + // The possibility of partial-expanded modules and resources is guarded by a + // top-level option for the whole plan, so that we can preserve mainline + // behavior for the modules runtime. So, we currently branch off into an + // entirely-separate codepath in those situations, at the expense of + // duplicating some of the logic for behavior this method would normally + // handle. + if ctx.Deferrals().DeferralAllowed() { + pem := expander.UnknownModuleInstances(n.Addr.Module, false) + + for _, moduleAddr := range pem { + actionAddr := moduleAddr.Action(n.Addr.Action) + resourceAddr := moduleAddr.Resource(n.lifecycleActionTrigger.resourceAddress.Resource) + + // And add a node to the graph for this action. + g.Add(&NodeActionTriggerPartialExpanded{ + addr: actionAddr, + config: n.Config, + resolvedProvider: n.resolvedProvider, + lifecycleActionTrigger: &lifecycleActionTriggerPartialExpanded{ + resourceAddress: resourceAddr, + events: n.lifecycleActionTrigger.events, + actionTriggerBlockIndex: n.lifecycleActionTrigger.actionTriggerBlockIndex, + actionListIndex: n.lifecycleActionTrigger.actionListIndex, + invokingSubject: n.lifecycleActionTrigger.invokingSubject, + }, + }) + } + addRootNodeToGraph(&g) + } + // First we expand the module moduleInstances := expander.ExpandModule(n.lifecycleActionTrigger.resourceAddress.Module, false) for _, module := range moduleInstances {