partially expand triggers as well

This commit is contained in:
Daniel Schmidt 2025-09-01 16:07:14 +02:00
parent b6a9990354
commit dffeeec81e
14 changed files with 613 additions and 15 deletions

View file

@ -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
}

View file

@ -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

View file

@ -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
}

View file

@ -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)

View file

@ -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
}

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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,
})
}

View file

@ -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
}

View file

@ -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

View file

@ -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
}

View file

@ -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 {