From 219c31f4ebf5dc9d7fc45ce2d11d7fdb81da7f0c Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Wed, 13 Aug 2025 16:06:34 +0200 Subject: [PATCH] action: move action trigger into nested struct this prepares the work on CLI / flag-driven invocations --- internal/command/jsonformat/diff.go | 6 +- internal/command/jsonformat/plan_test.go | 148 +++++++------- .../command/jsonplan/action_invocations.go | 64 ++++--- internal/command/views/hook_json_test.go | 12 +- internal/command/views/json/hook.go | 41 ++-- internal/plans/action_invocation.go | 64 +++++-- internal/plans/changes_src.go | 19 +- internal/plans/changes_sync.go | 8 +- internal/plans/planfile/tfplan.go | 109 ++++++----- internal/plans/planfile/tfplan_test.go | 44 +++-- internal/plans/planproto/planfile.pb.go | 180 +++++++++++++----- internal/plans/planproto/planfile.proto | 16 +- .../terraform/context_plan_actions_test.go | 102 ++++++---- internal/terraform/hook.go | 8 +- internal/terraform/node_action_apply.go | 25 +-- .../terraform/node_resource_plan_instance.go | 16 +- internal/terraform/transform_diff.go | 12 +- 17 files changed, 546 insertions(+), 328 deletions(-) diff --git a/internal/command/jsonformat/diff.go b/internal/command/jsonformat/diff.go index 1407f078eb..31662cb0fc 100644 --- a/internal/command/jsonformat/diff.go +++ b/internal/command/jsonformat/diff.go @@ -66,18 +66,18 @@ func precomputeDiffs(plan Plan, mode plans.Mode) diffs { after := []jsonplan.ActionInvocation{} for _, action := range plan.ActionInvocations { - if action.TriggeringResourceAddress != change.Address { + if action.LifecycleActionTrigger == nil || action.LifecycleActionTrigger.TriggeringResourceAddress != change.Address { continue } - switch action.TriggerEvent { + switch action.LifecycleActionTrigger.ActionTriggerEvent { case configs.BeforeCreate.String(), configs.BeforeUpdate.String(), configs.BeforeDestroy.String(): before = append(before, action) case configs.AfterCreate.String(), configs.AfterUpdate.String(), configs.AfterDestroy.String(): after = append(after, action) default: // The switch should be exhaustive. - panic(fmt.Sprintf("Unexpected triggering event when rendering action %s", action.TriggerEvent)) + panic(fmt.Sprintf("Unexpected triggering event when rendering action %s", action.LifecycleActionTrigger.ActionTriggerEvent)) } } diff --git a/internal/command/jsonformat/plan_test.go b/internal/command/jsonformat/plan_test.go index ce76d24037..e47b63c89f 100644 --- a/internal/command/jsonformat/plan_test.go +++ b/internal/command/jsonformat/plan_test.go @@ -8289,10 +8289,6 @@ func TestResourceChange_actions(t *testing.T) { }, } - ptr := func(i int) *int { - return &i - } - for name, tc := range map[string]struct { actionInvocations []jsonplan.ActionInvocation output string @@ -8300,14 +8296,16 @@ func TestResourceChange_actions(t *testing.T) { "before actions": { actionInvocations: []jsonplan.ActionInvocation{ { - Address: "action.test_unlinked.hello", - Type: "test_unlinked", - Name: "hello", - ProviderName: "registry.terraform.io/hashicorp/test", - ActionTriggerBlockIndex: ptr(0), - ActionsListIndex: ptr(0), - TriggeringResourceAddress: triggeringResourceAddr.String(), - TriggerEvent: "BeforeCreate", + Address: "action.test_unlinked.hello", + Type: "test_unlinked", + Name: "hello", + ProviderName: "registry.terraform.io/hashicorp/test", + LifecycleActionTrigger: &jsonplan.LifecycleActionTrigger{ + ActionTriggerBlockIndex: 0, + ActionsListIndex: 0, + TriggeringResourceAddress: triggeringResourceAddr.String(), + ActionTriggerEvent: "BeforeCreate", + }, ConfigValues: marshalConfigValues(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"), "disk": cty.ObjectVal(map[string]cty.Value{ @@ -8342,14 +8340,16 @@ func TestResourceChange_actions(t *testing.T) { "after actions": { actionInvocations: []jsonplan.ActionInvocation{ { - Address: "action.test_unlinked.hello", - Type: "test_unlinked", - Name: "hello", - ProviderName: "registry.terraform.io/hashicorp/test", - ActionTriggerBlockIndex: ptr(0), - ActionsListIndex: ptr(0), - TriggeringResourceAddress: triggeringResourceAddr.String(), - TriggerEvent: "AfterCreate", + Address: "action.test_unlinked.hello", + Type: "test_unlinked", + Name: "hello", + ProviderName: "registry.terraform.io/hashicorp/test", + LifecycleActionTrigger: &jsonplan.LifecycleActionTrigger{ + ActionTriggerBlockIndex: 0, + ActionsListIndex: 0, + TriggeringResourceAddress: triggeringResourceAddr.String(), + ActionTriggerEvent: "AfterCreate", + }, ConfigValues: marshalConfigValues(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"), "disk": cty.ObjectVal(map[string]cty.Value{ @@ -8384,66 +8384,76 @@ func TestResourceChange_actions(t *testing.T) { "before and after actions": { actionInvocations: []jsonplan.ActionInvocation{ { - Address: "action.test_unlinked.hello", - Type: "test_unlinked", - Name: "hello", - ProviderName: "registry.terraform.io/hashicorp/test", - ActionTriggerBlockIndex: ptr(0), - ActionsListIndex: ptr(0), - TriggeringResourceAddress: triggeringResourceAddr.String(), - TriggerEvent: "BeforeCreate", + Address: "action.test_unlinked.hello", + Type: "test_unlinked", + Name: "hello", + ProviderName: "registry.terraform.io/hashicorp/test", + LifecycleActionTrigger: &jsonplan.LifecycleActionTrigger{ + ActionTriggerBlockIndex: 0, + ActionsListIndex: 0, + TriggeringResourceAddress: triggeringResourceAddr.String(), + ActionTriggerEvent: "BeforeCreate", + }, ConfigValues: marshalConfigValues(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("first-block-and-action"), })), }, { - Address: "action.test_unlinked.hello", - Type: "test_unlinked", - Name: "hello", - ProviderName: "registry.terraform.io/hashicorp/test", - ActionTriggerBlockIndex: ptr(0), - ActionsListIndex: ptr(1), - TriggeringResourceAddress: triggeringResourceAddr.String(), - TriggerEvent: "BeforeCreate", + Address: "action.test_unlinked.hello", + Type: "test_unlinked", + Name: "hello", + ProviderName: "registry.terraform.io/hashicorp/test", + LifecycleActionTrigger: &jsonplan.LifecycleActionTrigger{ + ActionTriggerBlockIndex: 0, + ActionsListIndex: 1, + TriggeringResourceAddress: triggeringResourceAddr.String(), + ActionTriggerEvent: "BeforeCreate", + }, ConfigValues: marshalConfigValues(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("first-block-second-action"), })), }, { - Address: "action.test_unlinked.hello", - Type: "test_unlinked", - Name: "hello", - ProviderName: "registry.terraform.io/hashicorp/test", - ActionTriggerBlockIndex: ptr(1), - ActionsListIndex: ptr(0), - TriggeringResourceAddress: triggeringResourceAddr.String(), - TriggerEvent: "AfterCreate", + Address: "action.test_unlinked.hello", + Type: "test_unlinked", + Name: "hello", + ProviderName: "registry.terraform.io/hashicorp/test", + LifecycleActionTrigger: &jsonplan.LifecycleActionTrigger{ + ActionTriggerBlockIndex: 1, + ActionsListIndex: 0, + TriggeringResourceAddress: triggeringResourceAddr.String(), + ActionTriggerEvent: "AfterCreate", + }, ConfigValues: marshalConfigValues(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("second-block-first-action"), })), }, { - Address: "action.test_unlinked.hello", - Type: "test_unlinked", - Name: "hello", - ProviderName: "registry.terraform.io/hashicorp/test", - ActionTriggerBlockIndex: ptr(2), - ActionsListIndex: ptr(0), - TriggeringResourceAddress: triggeringResourceAddr.String(), - TriggerEvent: "AfterCreate", + Address: "action.test_unlinked.hello", + Type: "test_unlinked", + Name: "hello", + ProviderName: "registry.terraform.io/hashicorp/test", + LifecycleActionTrigger: &jsonplan.LifecycleActionTrigger{ + ActionTriggerBlockIndex: 2, + ActionsListIndex: 0, + TriggeringResourceAddress: triggeringResourceAddr.String(), + ActionTriggerEvent: "AfterCreate", + }, ConfigValues: marshalConfigValues(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("third-block-first-action"), })), }, { - Address: "action.test_unlinked.hello", - Type: "test_unlinked", - Name: "hello", - ProviderName: "registry.terraform.io/hashicorp/test", - ActionTriggerBlockIndex: ptr(3), - ActionsListIndex: ptr(0), - TriggeringResourceAddress: triggeringResourceAddr.String(), - TriggerEvent: "BeforeCreate", + Address: "action.test_unlinked.hello", + Type: "test_unlinked", + Name: "hello", + ProviderName: "registry.terraform.io/hashicorp/test", + LifecycleActionTrigger: &jsonplan.LifecycleActionTrigger{ + ActionTriggerBlockIndex: 3, + ActionsListIndex: 0, + TriggeringResourceAddress: triggeringResourceAddr.String(), + ActionTriggerEvent: "BeforeCreate", + }, ConfigValues: marshalConfigValues(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("fourth-block-first-action"), })), @@ -8488,14 +8498,16 @@ func TestResourceChange_actions(t *testing.T) { "no config value": { actionInvocations: []jsonplan.ActionInvocation{ { - Address: "action.test_unlinked.hello", - Type: "test_unlinked", - Name: "hello", - ProviderName: "registry.terraform.io/hashicorp/test", - ActionTriggerBlockIndex: ptr(0), - ActionsListIndex: ptr(0), - TriggeringResourceAddress: triggeringResourceAddr.String(), - TriggerEvent: "BeforeCreate", + Address: "action.test_unlinked.hello", + Type: "test_unlinked", + Name: "hello", + ProviderName: "registry.terraform.io/hashicorp/test", + LifecycleActionTrigger: &jsonplan.LifecycleActionTrigger{ + ActionTriggerBlockIndex: 0, + ActionsListIndex: 0, + TriggeringResourceAddress: triggeringResourceAddr.String(), + ActionTriggerEvent: "BeforeCreate", + }, }, }, output: ` # test_instance.example will be created diff --git a/internal/command/jsonplan/action_invocations.go b/internal/command/jsonplan/action_invocations.go index 7e4ac8db9f..bf481f48f0 100644 --- a/internal/command/jsonplan/action_invocations.go +++ b/internal/command/jsonplan/action_invocations.go @@ -31,39 +31,42 @@ type ActionInvocation struct { // offering "google_compute_instance". ProviderName string `json:"provider_name,omitempty"` - // These fields below are used for actions invoked during plan / apply, they are not applicable - // for terraform invoke. - - // ActionTriggerBlockIndex is the index of the action trigger block - ActionTriggerBlockIndex *int `json:"action_trigger_block_index,omitempty"` - // ActionsListIndex is the index of the action in the actions list - ActionsListIndex *int `json:"actions_list_index,omitempty"` - // TriggeringResourceAddress is the address of the resource that triggered the action + LifecycleActionTrigger *LifecycleActionTrigger `json:"lifecycle_action_trigger,omitempty"` + InvokeCmdActionTrigger *InvokeCmdActionTrigger `json:"invoke_cmd_action_trigger,omitempty"` +} + +type LifecycleActionTrigger struct { TriggeringResourceAddress string `json:"triggering_resource_address,omitempty"` - // TriggerEvent is the event that triggered the action - TriggerEvent string `json:"trigger_event,omitempty"` + ActionTriggerEvent string `json:"action_trigger_event,omitempty"` + ActionTriggerBlockIndex int `json:"action_trigger_block_index,omitempty"` + ActionsListIndex int `json:"actions_list_index,omitempty"` +} + +type InvokeCmdActionTrigger struct { + ActionTriggerEvent string `json:"action_trigger_event,omitempty"` } func ActionInvocationCompare(a, b ActionInvocation) int { - if a.TriggeringResourceAddress < b.TriggeringResourceAddress { - return -1 - } else if a.TriggeringResourceAddress > b.TriggeringResourceAddress { - return 1 - } + if a.LifecycleActionTrigger != nil && b.LifecycleActionTrigger != nil { + latA := *a.LifecycleActionTrigger + latB := *b.LifecycleActionTrigger - if a.ActionTriggerBlockIndex != nil && b.ActionTriggerBlockIndex != nil { - if *a.ActionTriggerBlockIndex < *b.ActionTriggerBlockIndex { + if latA.TriggeringResourceAddress < latB.TriggeringResourceAddress { return -1 - } else if *a.ActionTriggerBlockIndex > *b.ActionTriggerBlockIndex { + } else if latA.TriggeringResourceAddress > latB.TriggeringResourceAddress { return 1 } - } - if a.ActionsListIndex != nil && b.ActionsListIndex != nil { - if *a.ActionsListIndex < *b.ActionsListIndex { + if latA.ActionTriggerBlockIndex < latB.ActionTriggerBlockIndex { + return -1 + } else if latA.ActionTriggerBlockIndex > latB.ActionTriggerBlockIndex { + return 1 + } + + if latA.ActionsListIndex < latB.ActionsListIndex { return -1 - } else if *a.ActionsListIndex > *b.ActionsListIndex { + } else if latA.ActionsListIndex > latB.ActionsListIndex { return 1 } } @@ -111,13 +114,18 @@ func MarshalActionInvocations(actions []*plans.ActionInvocationInstanceSrc, sche Type: action.Addr.Action.Action.Type, Name: action.Addr.Action.Action.Name, ProviderName: action.ProviderAddr.Provider.String(), + } - // These fields are only used for non-CLI actions. We will need to find another format - // once we support terraform invoke. - ActionTriggerBlockIndex: &action.ActionTriggerBlockIndex, - ActionsListIndex: &action.ActionsListIndex, - TriggeringResourceAddress: action.TriggeringResourceAddr.String(), - TriggerEvent: action.TriggerEvent.String(), + switch at := action.ActionTrigger.(type) { + case plans.LifecycleActionTrigger: + ai.LifecycleActionTrigger = &LifecycleActionTrigger{ + TriggeringResourceAddress: at.TriggeringResourceAddr.String(), + ActionTriggerEvent: at.TriggerEvent().String(), + ActionTriggerBlockIndex: at.ActionTriggerBlockIndex, + ActionsListIndex: at.ActionsListIndex, + } + default: + return ret, fmt.Errorf("unsupported action trigger type: %T", at) } if actionDec.ConfigValue != cty.NilVal { diff --git a/internal/command/views/hook_json_test.go b/internal/command/views/hook_json_test.go index f25af0c4de..3da407cf3a 100644 --- a/internal/command/views/hook_json_test.go +++ b/internal/command/views/hook_json_test.go @@ -14,6 +14,7 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/terminal" "github.com/hashicorp/terraform/internal/terraform" @@ -32,10 +33,13 @@ func testJSONHookResourceID(addr addrs.AbsResourceInstance) terraform.HookResour func testJSONHookActionID(actionAddr addrs.AbsActionInstance, triggeringResourceAddr addrs.AbsResourceInstance, actionTriggerIndex int, actionsListIndex int) terraform.HookActionIdentity { return terraform.HookActionIdentity{ - Addr: actionAddr, - TriggeringResourceAddr: triggeringResourceAddr, - ActionTriggerBlockIndex: actionTriggerIndex, - ActionsListIndex: actionsListIndex, + Addr: actionAddr, + ActionTrigger: plans.LifecycleActionTrigger{ + TriggeringResourceAddr: triggeringResourceAddr, + ActionTriggerBlockIndex: actionTriggerIndex, + ActionsListIndex: actionsListIndex, + ActionTriggerEvent: configs.AfterCreate, + }, } } diff --git a/internal/command/views/json/hook.go b/internal/command/views/json/hook.go index cbb0a7a050..04eaf92fb7 100644 --- a/internal/command/views/json/hook.go +++ b/internal/command/views/json/hook.go @@ -373,10 +373,15 @@ func (h *actionStart) String() string { } func NewActionStart(id terraform.HookActionIdentity) Hook { + at, ok := id.ActionTrigger.(plans.LifecycleActionTrigger) + if !ok { + panic("invalid action trigger") + } + return &actionStart{ - TriggeringResource: newResourceAddr(id.TriggeringResourceAddr), - TriggerIndex: id.ActionTriggerBlockIndex, - ActionsIndex: id.ActionsListIndex, + TriggeringResource: newResourceAddr(at.TriggeringResourceAddr), + TriggerIndex: at.ActionTriggerBlockIndex, + ActionsIndex: at.ActionsListIndex, Action: newActionAddr(id.Addr), } } @@ -400,10 +405,14 @@ func (h *actionProgress) String() string { } func NewActionProgress(id terraform.HookActionIdentity, message string) Hook { + at, ok := id.ActionTrigger.(plans.LifecycleActionTrigger) + if !ok { + panic("invalid action trigger") + } return &actionProgress{ - TriggeringResource: newResourceAddr(id.TriggeringResourceAddr), - TriggerIndex: id.ActionTriggerBlockIndex, - ActionsIndex: id.ActionsListIndex, + TriggeringResource: newResourceAddr(at.TriggeringResourceAddr), + TriggerIndex: at.ActionTriggerBlockIndex, + ActionsIndex: at.ActionsListIndex, Action: newActionAddr(id.Addr), Message: message, } @@ -427,10 +436,14 @@ func (h *actionComplete) String() string { } func NewActionComplete(id terraform.HookActionIdentity) Hook { + at, ok := id.ActionTrigger.(plans.LifecycleActionTrigger) + if !ok { + panic("invalid action trigger") + } return &actionComplete{ - TriggeringResource: newResourceAddr(id.TriggeringResourceAddr), - TriggerIndex: id.ActionTriggerBlockIndex, - ActionsIndex: id.ActionsListIndex, + TriggeringResource: newResourceAddr(at.TriggeringResourceAddr), + TriggerIndex: at.ActionTriggerBlockIndex, + ActionsIndex: at.ActionsListIndex, Action: newActionAddr(id.Addr), } } @@ -454,10 +467,14 @@ func (h *actionErrored) String() string { } func NewActionErrored(id terraform.HookActionIdentity, err error) Hook { + at, ok := id.ActionTrigger.(plans.LifecycleActionTrigger) + if !ok { + panic("invalid action trigger") + } return &actionErrored{ - TriggeringResource: newResourceAddr(id.TriggeringResourceAddr), - TriggerIndex: id.ActionTriggerBlockIndex, - ActionsIndex: id.ActionsListIndex, + TriggeringResource: newResourceAddr(at.TriggeringResourceAddr), + TriggerIndex: at.ActionTriggerBlockIndex, + ActionsIndex: at.ActionsListIndex, Action: newActionAddr(id.Addr), Error: err.Error(), } diff --git a/internal/plans/action_invocation.go b/internal/plans/action_invocation.go index 9b98064e19..0757d13285 100644 --- a/internal/plans/action_invocation.go +++ b/internal/plans/action_invocation.go @@ -11,16 +11,9 @@ import ( ) type ActionInvocationInstance struct { - Addr addrs.AbsActionInstance - TriggeringResourceAddr addrs.AbsResourceInstance + Addr addrs.AbsActionInstance - // Information about the trigger - // The event that triggered this action invocation. - TriggerEvent configs.ActionTriggerEvent - // The index of the action_trigger block that triggered this invocation. - ActionTriggerBlockIndex int - // The index of the action in the evens list of the action_trigger block - ActionsListIndex int + ActionTrigger ActionTrigger // Provider is the address of the provider configuration that was used // to plan this action, and thus the configuration that must also be @@ -30,18 +23,59 @@ type ActionInvocationInstance struct { ConfigValue cty.Value } +type ActionTrigger interface { + actionTriggerSigil() + + TriggerEvent() configs.ActionTriggerEvent + + String() string + + Equals(to ActionTrigger) bool +} + +type LifecycleActionTrigger struct { + TriggeringResourceAddr addrs.AbsResourceInstance + // Information about the trigger + // The event that triggered this action invocation. + ActionTriggerEvent configs.ActionTriggerEvent + // The index of the action_trigger block that triggered this invocation. + ActionTriggerBlockIndex int + // The index of the action in the events list of the action_trigger block + ActionsListIndex int +} + +func (t LifecycleActionTrigger) TriggerEvent() configs.ActionTriggerEvent { + return t.ActionTriggerEvent +} + +func (t LifecycleActionTrigger) actionTriggerSigil() {} + +func (t LifecycleActionTrigger) String() string { + return t.TriggeringResourceAddr.String() +} + +func (t LifecycleActionTrigger) Equals(other ActionTrigger) bool { + o, ok := other.(LifecycleActionTrigger) + if !ok { + return false + } + + return t.TriggeringResourceAddr.Equal(o.TriggeringResourceAddr) && + t.ActionTriggerBlockIndex == o.ActionTriggerBlockIndex && + t.ActionsListIndex == o.ActionsListIndex +} + +var _ ActionTrigger = (*LifecycleActionTrigger)(nil) + // Encode produces a variant of the receiver that has its change values // serialized so it can be written to a plan file. Pass the implied type of the // corresponding resource type schema for correct operation. func (ai *ActionInvocationInstance) Encode(schema *providers.ActionSchema) (*ActionInvocationInstanceSrc, error) { ret := &ActionInvocationInstanceSrc{ - Addr: ai.Addr, - TriggeringResourceAddr: ai.TriggeringResourceAddr, - TriggerEvent: ai.TriggerEvent, - ActionTriggerBlockIndex: ai.ActionTriggerBlockIndex, - ActionsListIndex: ai.ActionsListIndex, - ProviderAddr: ai.ProviderAddr, + Addr: ai.Addr, + ActionTrigger: ai.ActionTrigger, + ProviderAddr: ai.ProviderAddr, } if ai.ConfigValue != cty.NilVal { diff --git a/internal/plans/changes_src.go b/internal/plans/changes_src.go index dcf97c45c2..089881fe09 100644 --- a/internal/plans/changes_src.go +++ b/internal/plans/changes_src.go @@ -9,7 +9,6 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/genconfig" "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/providers" @@ -566,11 +565,8 @@ func (c *ChangesSrc) AppendActionInvocationInstanceChange(action *ActionInvocati } type ActionInvocationInstanceSrc struct { - Addr addrs.AbsActionInstance - TriggeringResourceAddr addrs.AbsResourceInstance - TriggerEvent configs.ActionTriggerEvent - ActionTriggerBlockIndex int - ActionsListIndex int + Addr addrs.AbsActionInstance + ActionTrigger ActionTrigger ConfigValue DynamicValue @@ -590,13 +586,10 @@ func (acs *ActionInvocationInstanceSrc) Decode(schema *providers.ActionSchema) ( } ai := &ActionInvocationInstance{ - Addr: acs.Addr, - TriggeringResourceAddr: acs.TriggeringResourceAddr, - TriggerEvent: acs.TriggerEvent, - ActionTriggerBlockIndex: acs.ActionTriggerBlockIndex, - ActionsListIndex: acs.ActionsListIndex, - ProviderAddr: acs.ProviderAddr, - ConfigValue: config, + Addr: acs.Addr, + ActionTrigger: acs.ActionTrigger, + ProviderAddr: acs.ProviderAddr, + ConfigValue: config, } return ai, nil } diff --git a/internal/plans/changes_sync.go b/internal/plans/changes_sync.go index 18a2e9ff68..18d23dda1a 100644 --- a/internal/plans/changes_sync.go +++ b/internal/plans/changes_sync.go @@ -241,7 +241,7 @@ func (cs *ChangesSync) RemoveOutputChange(addr addrs.AbsOutputValue) { // GetActionInvocation gets an action invocation based on the action address, the triggering // resource address, the action trigger block index, and the action list index. -func (cs *ChangesSync) GetActionInvocation(addr addrs.AbsActionInstance, triggeringResourceAddr addrs.AbsResourceInstance, triggerBlockIndex, actionListIndex int) *ActionInvocationInstance { +func (cs *ChangesSync) GetActionInvocation(addr addrs.AbsActionInstance, actionTrigger ActionTrigger) *ActionInvocationInstance { if cs == nil { panic("GetActionInvocation on nil ChangesSync") } @@ -249,8 +249,10 @@ func (cs *ChangesSync) GetActionInvocation(addr addrs.AbsActionInstance, trigger defer cs.lock.Unlock() for _, a := range cs.changes.ActionInvocations { - if a.Addr.Equal(addr) && a.TriggeringResourceAddr.Equal(triggeringResourceAddr) && a.ActionTriggerBlockIndex == triggerBlockIndex && a.ActionsListIndex == actionListIndex { - return a + if a.Addr.Equal(addr) { + if a.ActionTrigger.Equals(actionTrigger) { + return a + } } } return nil diff --git a/internal/plans/planfile/tfplan.go b/internal/plans/planfile/tfplan.go index cf29be9311..142de53daa 100644 --- a/internal/plans/planfile/tfplan.go +++ b/internal/plans/planfile/tfplan.go @@ -1259,30 +1259,41 @@ func actionInvocationFromTfplan(rawAction *planproto.ActionInvocationInstance) ( } ret.Addr = actionAddr - ret.TriggeringResourceAddr, diags = addrs.ParseAbsResourceInstanceStr(rawAction.TriggeringResourceAddr) - if diags.HasErrors() { - return nil, fmt.Errorf("invalid resource instance address %q: %w", rawAction.TriggeringResourceAddr, diags.Err()) - } - - ret.ActionsListIndex = int(rawAction.ActionsListIndex) - ret.ActionTriggerBlockIndex = int(rawAction.ActionTriggerBlockIndex) - - switch rawAction.TriggerEvent { - case planproto.ActionTriggerEvent_BEFORE_CERATE: - ret.TriggerEvent = configs.BeforeCreate - case planproto.ActionTriggerEvent_AFTER_CREATE: - ret.TriggerEvent = configs.AfterCreate - case planproto.ActionTriggerEvent_BEFORE_UPDATE: - ret.TriggerEvent = configs.BeforeUpdate - case planproto.ActionTriggerEvent_AFTER_UPDATE: - ret.TriggerEvent = configs.AfterUpdate - case planproto.ActionTriggerEvent_BEFORE_DESTROY: - ret.TriggerEvent = configs.BeforeDestroy - case planproto.ActionTriggerEvent_AFTER_DESTROY: - ret.TriggerEvent = configs.AfterDestroy + switch at := rawAction.ActionTrigger.(type) { + case *planproto.ActionInvocationInstance_LifecycleActionTrigger: + triggeringResourceAddrs, diags := addrs.ParseAbsResourceInstanceStr(at.LifecycleActionTrigger.TriggeringResourceAddr) + if diags.HasErrors() { + return nil, fmt.Errorf("invalid resource instance address %q: %w", + at.LifecycleActionTrigger.TriggeringResourceAddr, diags.Err()) + } + + var ate configs.ActionTriggerEvent + switch at.LifecycleActionTrigger.TriggerEvent { + case planproto.ActionTriggerEvent_BEFORE_CERATE: + ate = configs.BeforeCreate + case planproto.ActionTriggerEvent_AFTER_CREATE: + ate = configs.AfterCreate + case planproto.ActionTriggerEvent_BEFORE_UPDATE: + ate = configs.BeforeUpdate + case planproto.ActionTriggerEvent_AFTER_UPDATE: + ate = configs.AfterUpdate + case planproto.ActionTriggerEvent_BEFORE_DESTROY: + ate = configs.BeforeDestroy + case planproto.ActionTriggerEvent_AFTER_DESTROY: + ate = configs.AfterDestroy + default: + return nil, fmt.Errorf("invalid action trigger event %s", at.LifecycleActionTrigger.TriggerEvent) + } + ret.ActionTrigger = plans.LifecycleActionTrigger{ + TriggeringResourceAddr: triggeringResourceAddrs, + ActionTriggerBlockIndex: int(at.LifecycleActionTrigger.ActionTriggerBlockIndex), + ActionsListIndex: int(at.LifecycleActionTrigger.ActionsListIndex), + ActionTriggerEvent: ate, + } default: - return nil, fmt.Errorf("invalid action trigger event %s", rawAction.TriggerEvent) + // This should be exhaustive + return nil, fmt.Errorf("unsupported action trigger type %t", rawAction.ActionTrigger) } providerAddr, diags := addrs.ParseAbsProviderConfigStr(rawAction.Provider) @@ -1307,29 +1318,39 @@ func actionInvocationToTfPlan(action *plans.ActionInvocationInstanceSrc) (*planp return nil, nil } - triggerEvent := planproto.ActionTriggerEvent_INVALID_EVENT - switch action.TriggerEvent { - case configs.BeforeCreate: - triggerEvent = planproto.ActionTriggerEvent_BEFORE_CERATE - case configs.AfterCreate: - triggerEvent = planproto.ActionTriggerEvent_AFTER_CREATE - case configs.BeforeUpdate: - triggerEvent = planproto.ActionTriggerEvent_BEFORE_UPDATE - case configs.AfterUpdate: - triggerEvent = planproto.ActionTriggerEvent_AFTER_UPDATE - case configs.BeforeDestroy: - triggerEvent = planproto.ActionTriggerEvent_BEFORE_DESTROY - case configs.AfterDestroy: - triggerEvent = planproto.ActionTriggerEvent_AFTER_DESTROY - } - ret := &planproto.ActionInvocationInstance{ - Addr: action.Addr.String(), - Provider: action.ProviderAddr.String(), - TriggeringResourceAddr: action.TriggeringResourceAddr.String(), - ActionsListIndex: int64(action.ActionsListIndex), - ActionTriggerBlockIndex: int64(action.ActionTriggerBlockIndex), - TriggerEvent: triggerEvent, + Addr: action.Addr.String(), + Provider: action.ProviderAddr.String(), + } + + switch at := action.ActionTrigger.(type) { + case plans.LifecycleActionTrigger: + triggerEvent := planproto.ActionTriggerEvent_INVALID_EVENT + switch at.ActionTriggerEvent { + case configs.BeforeCreate: + triggerEvent = planproto.ActionTriggerEvent_BEFORE_CERATE + case configs.AfterCreate: + triggerEvent = planproto.ActionTriggerEvent_AFTER_CREATE + case configs.BeforeUpdate: + triggerEvent = planproto.ActionTriggerEvent_BEFORE_UPDATE + case configs.AfterUpdate: + triggerEvent = planproto.ActionTriggerEvent_AFTER_UPDATE + case configs.BeforeDestroy: + triggerEvent = planproto.ActionTriggerEvent_BEFORE_DESTROY + case configs.AfterDestroy: + triggerEvent = planproto.ActionTriggerEvent_AFTER_DESTROY + } + ret.ActionTrigger = &planproto.ActionInvocationInstance_LifecycleActionTrigger{ + LifecycleActionTrigger: &planproto.LifecycleActionTrigger{ + TriggerEvent: triggerEvent, + TriggeringResourceAddr: at.TriggeringResourceAddr.String(), + ActionTriggerBlockIndex: int64(at.ActionTriggerBlockIndex), + ActionsListIndex: int64(at.ActionsListIndex), + }, + } + default: + // This should be exhaustive + return nil, fmt.Errorf("unsupported action trigger type: %T", at) } if action.ConfigValue != nil { diff --git a/internal/plans/planfile/tfplan_test.go b/internal/plans/planfile/tfplan_test.go index 10df8f5ff7..4998bb77fe 100644 --- a/internal/plans/planfile/tfplan_test.go +++ b/internal/plans/planfile/tfplan_test.go @@ -305,28 +305,32 @@ func examplePlanForTest(t *testing.T) *plans.Plan { }, ActionInvocations: []*plans.ActionInvocationInstanceSrc{ { - Addr: addrs.Action{Type: "example", Name: "foo"}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), - ProviderAddr: provider, - TriggerEvent: configs.BeforeCreate, - ActionTriggerBlockIndex: 2, - ActionsListIndex: 0, - TriggeringResourceAddr: addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "test_thing", - Name: "woot", - }.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance), + Addr: addrs.Action{Type: "example", Name: "foo"}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ProviderAddr: provider, + ActionTrigger: plans.LifecycleActionTrigger{ + ActionTriggerEvent: configs.BeforeCreate, + ActionTriggerBlockIndex: 2, + ActionsListIndex: 0, + TriggeringResourceAddr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_thing", + Name: "woot", + }.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance), + }, }, { - Addr: addrs.Action{Type: "example", Name: "bar"}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), - ProviderAddr: provider, - TriggerEvent: configs.BeforeCreate, - ActionTriggerBlockIndex: 2, - ActionsListIndex: 1, - TriggeringResourceAddr: addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "test_thing", - Name: "woot", - }.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance), + Addr: addrs.Action{Type: "example", Name: "bar"}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ProviderAddr: provider, + ActionTrigger: plans.LifecycleActionTrigger{ + ActionTriggerEvent: configs.BeforeCreate, + ActionTriggerBlockIndex: 2, + ActionsListIndex: 1, + TriggeringResourceAddr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_thing", + Name: "woot", + }.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance), + }, ConfigValue: mustNewDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("testing"), }), objTy), diff --git a/internal/plans/planproto/planfile.pb.go b/internal/plans/planproto/planfile.pb.go index a54b0f2df0..58a7c0b376 100644 --- a/internal/plans/planproto/planfile.pb.go +++ b/internal/plans/planproto/planfile.pb.go @@ -1652,15 +1652,15 @@ type ActionInvocationInstance struct { // provider is the address of the provider configuration that this change // was planned with, and thus the configuration that must be used to // apply it. - Provider string `protobuf:"bytes,2,opt,name=provider,proto3" json:"provider,omitempty"` - LinkedResources []*ResourceInstanceActionChange `protobuf:"bytes,3,rep,name=linked_resources,json=linkedResources,proto3" json:"linked_resources,omitempty"` - ConfigValue *DynamicValue `protobuf:"bytes,4,opt,name=config_value,json=configValue,proto3" json:"config_value,omitempty"` - TriggeringResourceAddr string `protobuf:"bytes,5,opt,name=triggering_resource_addr,json=triggeringResourceAddr,proto3" json:"triggering_resource_addr,omitempty"` - TriggerEvent ActionTriggerEvent `protobuf:"varint,6,opt,name=trigger_event,json=triggerEvent,proto3,enum=tfplan.ActionTriggerEvent" json:"trigger_event,omitempty"` - ActionTriggerBlockIndex int64 `protobuf:"varint,7,opt,name=action_trigger_block_index,json=actionTriggerBlockIndex,proto3" json:"action_trigger_block_index,omitempty"` - ActionsListIndex int64 `protobuf:"varint,8,opt,name=actions_list_index,json=actionsListIndex,proto3" json:"actions_list_index,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Provider string `protobuf:"bytes,2,opt,name=provider,proto3" json:"provider,omitempty"` + LinkedResources []*ResourceInstanceActionChange `protobuf:"bytes,3,rep,name=linked_resources,json=linkedResources,proto3" json:"linked_resources,omitempty"` + ConfigValue *DynamicValue `protobuf:"bytes,4,opt,name=config_value,json=configValue,proto3" json:"config_value,omitempty"` + // Types that are valid to be assigned to ActionTrigger: + // + // *ActionInvocationInstance_LifecycleActionTrigger + ActionTrigger isActionInvocationInstance_ActionTrigger `protobuf_oneof:"action_trigger"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ActionInvocationInstance) Reset() { @@ -1721,28 +1721,96 @@ func (x *ActionInvocationInstance) GetConfigValue() *DynamicValue { return nil } -func (x *ActionInvocationInstance) GetTriggeringResourceAddr() string { +func (x *ActionInvocationInstance) GetActionTrigger() isActionInvocationInstance_ActionTrigger { + if x != nil { + return x.ActionTrigger + } + return nil +} + +func (x *ActionInvocationInstance) GetLifecycleActionTrigger() *LifecycleActionTrigger { + if x != nil { + if x, ok := x.ActionTrigger.(*ActionInvocationInstance_LifecycleActionTrigger); ok { + return x.LifecycleActionTrigger + } + } + return nil +} + +type isActionInvocationInstance_ActionTrigger interface { + isActionInvocationInstance_ActionTrigger() +} + +type ActionInvocationInstance_LifecycleActionTrigger struct { + LifecycleActionTrigger *LifecycleActionTrigger `protobuf:"bytes,5,opt,name=lifecycle_action_trigger,json=lifecycleActionTrigger,proto3,oneof"` +} + +func (*ActionInvocationInstance_LifecycleActionTrigger) isActionInvocationInstance_ActionTrigger() {} + +// LifecycleActionTrigger contains details on the conditions that led to the +// triggering of an action. +type LifecycleActionTrigger struct { + state protoimpl.MessageState `protogen:"open.v1"` + TriggeringResourceAddr string `protobuf:"bytes,1,opt,name=triggering_resource_addr,json=triggeringResourceAddr,proto3" json:"triggering_resource_addr,omitempty"` + TriggerEvent ActionTriggerEvent `protobuf:"varint,2,opt,name=trigger_event,json=triggerEvent,proto3,enum=tfplan.ActionTriggerEvent" json:"trigger_event,omitempty"` + ActionTriggerBlockIndex int64 `protobuf:"varint,3,opt,name=action_trigger_block_index,json=actionTriggerBlockIndex,proto3" json:"action_trigger_block_index,omitempty"` + ActionsListIndex int64 `protobuf:"varint,4,opt,name=actions_list_index,json=actionsListIndex,proto3" json:"actions_list_index,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LifecycleActionTrigger) Reset() { + *x = LifecycleActionTrigger{} + mi := &file_planfile_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LifecycleActionTrigger) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LifecycleActionTrigger) ProtoMessage() {} + +func (x *LifecycleActionTrigger) ProtoReflect() protoreflect.Message { + mi := &file_planfile_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LifecycleActionTrigger.ProtoReflect.Descriptor instead. +func (*LifecycleActionTrigger) Descriptor() ([]byte, []int) { + return file_planfile_proto_rawDescGZIP(), []int{15} +} + +func (x *LifecycleActionTrigger) GetTriggeringResourceAddr() string { if x != nil { return x.TriggeringResourceAddr } return "" } -func (x *ActionInvocationInstance) GetTriggerEvent() ActionTriggerEvent { +func (x *LifecycleActionTrigger) GetTriggerEvent() ActionTriggerEvent { if x != nil { return x.TriggerEvent } return ActionTriggerEvent_INVALID_EVENT } -func (x *ActionInvocationInstance) GetActionTriggerBlockIndex() int64 { +func (x *LifecycleActionTrigger) GetActionTriggerBlockIndex() int64 { if x != nil { return x.ActionTriggerBlockIndex } return 0 } -func (x *ActionInvocationInstance) GetActionsListIndex() int64 { +func (x *LifecycleActionTrigger) GetActionsListIndex() int64 { if x != nil { return x.ActionsListIndex } @@ -1767,7 +1835,7 @@ type ResourceInstanceActionChange struct { func (x *ResourceInstanceActionChange) Reset() { *x = ResourceInstanceActionChange{} - mi := &file_planfile_proto_msgTypes[15] + mi := &file_planfile_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1779,7 +1847,7 @@ func (x *ResourceInstanceActionChange) String() string { func (*ResourceInstanceActionChange) ProtoMessage() {} func (x *ResourceInstanceActionChange) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[15] + mi := &file_planfile_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1792,7 +1860,7 @@ func (x *ResourceInstanceActionChange) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceInstanceActionChange.ProtoReflect.Descriptor instead. func (*ResourceInstanceActionChange) Descriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{15} + return file_planfile_proto_rawDescGZIP(), []int{16} } func (x *ResourceInstanceActionChange) GetAddr() string { @@ -1826,7 +1894,7 @@ type PlanResourceAttr struct { func (x *PlanResourceAttr) Reset() { *x = PlanResourceAttr{} - mi := &file_planfile_proto_msgTypes[17] + mi := &file_planfile_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1838,7 +1906,7 @@ func (x *PlanResourceAttr) String() string { func (*PlanResourceAttr) ProtoMessage() {} func (x *PlanResourceAttr) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[17] + mi := &file_planfile_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1879,7 +1947,7 @@ type CheckResults_ObjectResult struct { func (x *CheckResults_ObjectResult) Reset() { *x = CheckResults_ObjectResult{} - mi := &file_planfile_proto_msgTypes[18] + mi := &file_planfile_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1891,7 +1959,7 @@ func (x *CheckResults_ObjectResult) String() string { func (*CheckResults_ObjectResult) ProtoMessage() {} func (x *CheckResults_ObjectResult) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[18] + mi := &file_planfile_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1941,7 +2009,7 @@ type Path_Step struct { func (x *Path_Step) Reset() { *x = Path_Step{} - mi := &file_planfile_proto_msgTypes[19] + mi := &file_planfile_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1953,7 +2021,7 @@ func (x *Path_Step) String() string { func (*Path_Step) ProtoMessage() {} func (x *Path_Step) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[19] + mi := &file_planfile_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2129,16 +2197,19 @@ const file_planfile_proto_rawDesc = "" + "\aunknown\x18\x02 \x01(\bR\aunknown\x120\n" + "\bidentity\x18\x03 \x01(\v2\x14.tfplan.DynamicValueR\bidentity\":\n" + "\bDeferred\x12.\n" + - "\x06reason\x18\x01 \x01(\x0e2\x16.tfplan.DeferredReasonR\x06reason\"\xba\x03\n" + + "\x06reason\x18\x01 \x01(\x0e2\x16.tfplan.DeferredReasonR\x06reason\"\xc2\x02\n" + "\x18ActionInvocationInstance\x12\x12\n" + "\x04addr\x18\x01 \x01(\tR\x04addr\x12\x1a\n" + "\bprovider\x18\x02 \x01(\tR\bprovider\x12O\n" + "\x10linked_resources\x18\x03 \x03(\v2$.tfplan.ResourceInstanceActionChangeR\x0flinkedResources\x127\n" + - "\fconfig_value\x18\x04 \x01(\v2\x14.tfplan.DynamicValueR\vconfigValue\x128\n" + - "\x18triggering_resource_addr\x18\x05 \x01(\tR\x16triggeringResourceAddr\x12?\n" + - "\rtrigger_event\x18\x06 \x01(\x0e2\x1a.tfplan.ActionTriggerEventR\ftriggerEvent\x12;\n" + - "\x1aaction_trigger_block_index\x18\a \x01(\x03R\x17actionTriggerBlockIndex\x12,\n" + - "\x12actions_list_index\x18\b \x01(\x03R\x10actionsListIndex\"{\n" + + "\fconfig_value\x18\x04 \x01(\v2\x14.tfplan.DynamicValueR\vconfigValue\x12Z\n" + + "\x18lifecycle_action_trigger\x18\x05 \x01(\v2\x1e.tfplan.LifecycleActionTriggerH\x00R\x16lifecycleActionTriggerB\x10\n" + + "\x0eaction_trigger\"\xfe\x01\n" + + "\x16LifecycleActionTrigger\x128\n" + + "\x18triggering_resource_addr\x18\x01 \x01(\tR\x16triggeringResourceAddr\x12?\n" + + "\rtrigger_event\x18\x02 \x01(\x0e2\x1a.tfplan.ActionTriggerEventR\ftriggerEvent\x12;\n" + + "\x1aaction_trigger_block_index\x18\x03 \x01(\x03R\x17actionTriggerBlockIndex\x12,\n" + + "\x12actions_list_index\x18\x04 \x01(\x03R\x10actionsListIndex\"{\n" + "\x1cResourceInstanceActionChange\x12\x12\n" + "\x04addr\x18\x01 \x01(\tR\x04addr\x12\x1f\n" + "\vdeposed_key\x18\x02 \x01(\tR\n" + @@ -2209,7 +2280,7 @@ func file_planfile_proto_rawDescGZIP() []byte { } var file_planfile_proto_enumTypes = make([]protoimpl.EnumInfo, 7) -var file_planfile_proto_msgTypes = make([]protoimpl.MessageInfo, 20) +var file_planfile_proto_msgTypes = make([]protoimpl.MessageInfo, 21) var file_planfile_proto_goTypes = []any{ (Mode)(0), // 0: tfplan.Mode (Action)(0), // 1: tfplan.Action @@ -2233,15 +2304,16 @@ var file_planfile_proto_goTypes = []any{ (*Importing)(nil), // 19: tfplan.Importing (*Deferred)(nil), // 20: tfplan.Deferred (*ActionInvocationInstance)(nil), // 21: tfplan.ActionInvocationInstance - (*ResourceInstanceActionChange)(nil), // 22: tfplan.ResourceInstanceActionChange - nil, // 23: tfplan.Plan.VariablesEntry - (*PlanResourceAttr)(nil), // 24: tfplan.Plan.resource_attr - (*CheckResults_ObjectResult)(nil), // 25: tfplan.CheckResults.ObjectResult - (*Path_Step)(nil), // 26: tfplan.Path.Step + (*LifecycleActionTrigger)(nil), // 22: tfplan.LifecycleActionTrigger + (*ResourceInstanceActionChange)(nil), // 23: tfplan.ResourceInstanceActionChange + nil, // 24: tfplan.Plan.VariablesEntry + (*PlanResourceAttr)(nil), // 25: tfplan.Plan.resource_attr + (*CheckResults_ObjectResult)(nil), // 26: tfplan.CheckResults.ObjectResult + (*Path_Step)(nil), // 27: tfplan.Path.Step } var file_planfile_proto_depIdxs = []int32{ 0, // 0: tfplan.Plan.ui_mode:type_name -> tfplan.Mode - 23, // 1: tfplan.Plan.variables:type_name -> tfplan.Plan.VariablesEntry + 24, // 1: tfplan.Plan.variables:type_name -> tfplan.Plan.VariablesEntry 12, // 2: tfplan.Plan.resource_changes:type_name -> tfplan.ResourceInstanceChange 12, // 3: tfplan.Plan.resource_drift:type_name -> tfplan.ResourceInstanceChange 13, // 4: tfplan.Plan.deferred_changes:type_name -> tfplan.DeferredResourceInstanceChange @@ -2250,7 +2322,7 @@ var file_planfile_proto_depIdxs = []int32{ 21, // 7: tfplan.Plan.action_invocations:type_name -> tfplan.ActionInvocationInstance 8, // 8: tfplan.Plan.backend:type_name -> tfplan.Backend 9, // 9: tfplan.Plan.state_store:type_name -> tfplan.StateStore - 24, // 10: tfplan.Plan.relevant_attributes:type_name -> tfplan.Plan.resource_attr + 25, // 10: tfplan.Plan.relevant_attributes:type_name -> tfplan.Plan.resource_attr 16, // 11: tfplan.Plan.function_results:type_name -> tfplan.FunctionCallHash 17, // 12: tfplan.Backend.config:type_name -> tfplan.DynamicValue 17, // 13: tfplan.StateStore.config:type_name -> tfplan.DynamicValue @@ -2270,23 +2342,24 @@ var file_planfile_proto_depIdxs = []int32{ 11, // 27: tfplan.OutputChange.change:type_name -> tfplan.Change 6, // 28: tfplan.CheckResults.kind:type_name -> tfplan.CheckResults.ObjectKind 5, // 29: tfplan.CheckResults.status:type_name -> tfplan.CheckResults.Status - 25, // 30: tfplan.CheckResults.objects:type_name -> tfplan.CheckResults.ObjectResult - 26, // 31: tfplan.Path.steps:type_name -> tfplan.Path.Step + 26, // 30: tfplan.CheckResults.objects:type_name -> tfplan.CheckResults.ObjectResult + 27, // 31: tfplan.Path.steps:type_name -> tfplan.Path.Step 17, // 32: tfplan.Importing.identity:type_name -> tfplan.DynamicValue 3, // 33: tfplan.Deferred.reason:type_name -> tfplan.DeferredReason - 22, // 34: tfplan.ActionInvocationInstance.linked_resources:type_name -> tfplan.ResourceInstanceActionChange + 23, // 34: tfplan.ActionInvocationInstance.linked_resources:type_name -> tfplan.ResourceInstanceActionChange 17, // 35: tfplan.ActionInvocationInstance.config_value:type_name -> tfplan.DynamicValue - 4, // 36: tfplan.ActionInvocationInstance.trigger_event:type_name -> tfplan.ActionTriggerEvent - 11, // 37: tfplan.ResourceInstanceActionChange.change:type_name -> tfplan.Change - 17, // 38: tfplan.Plan.VariablesEntry.value:type_name -> tfplan.DynamicValue - 18, // 39: tfplan.Plan.resource_attr.attr:type_name -> tfplan.Path - 5, // 40: tfplan.CheckResults.ObjectResult.status:type_name -> tfplan.CheckResults.Status - 17, // 41: tfplan.Path.Step.element_key:type_name -> tfplan.DynamicValue - 42, // [42:42] is the sub-list for method output_type - 42, // [42:42] is the sub-list for method input_type - 42, // [42:42] is the sub-list for extension type_name - 42, // [42:42] is the sub-list for extension extendee - 0, // [0:42] is the sub-list for field type_name + 22, // 36: tfplan.ActionInvocationInstance.lifecycle_action_trigger:type_name -> tfplan.LifecycleActionTrigger + 4, // 37: tfplan.LifecycleActionTrigger.trigger_event:type_name -> tfplan.ActionTriggerEvent + 11, // 38: tfplan.ResourceInstanceActionChange.change:type_name -> tfplan.Change + 17, // 39: tfplan.Plan.VariablesEntry.value:type_name -> tfplan.DynamicValue + 18, // 40: tfplan.Plan.resource_attr.attr:type_name -> tfplan.Path + 5, // 41: tfplan.CheckResults.ObjectResult.status:type_name -> tfplan.CheckResults.Status + 17, // 42: tfplan.Path.Step.element_key:type_name -> tfplan.DynamicValue + 43, // [43:43] is the sub-list for method output_type + 43, // [43:43] is the sub-list for method input_type + 43, // [43:43] is the sub-list for extension type_name + 43, // [43:43] is the sub-list for extension extendee + 0, // [0:43] is the sub-list for field type_name } func init() { file_planfile_proto_init() } @@ -2294,7 +2367,10 @@ func file_planfile_proto_init() { if File_planfile_proto != nil { return } - file_planfile_proto_msgTypes[19].OneofWrappers = []any{ + file_planfile_proto_msgTypes[14].OneofWrappers = []any{ + (*ActionInvocationInstance_LifecycleActionTrigger)(nil), + } + file_planfile_proto_msgTypes[20].OneofWrappers = []any{ (*Path_Step_AttributeName)(nil), (*Path_Step_ElementKey)(nil), } @@ -2304,7 +2380,7 @@ func file_planfile_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_planfile_proto_rawDesc), len(file_planfile_proto_rawDesc)), NumEnums: 7, - NumMessages: 20, + NumMessages: 21, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/plans/planproto/planfile.proto b/internal/plans/planproto/planfile.proto index fd8b0cd460..c1576b434b 100644 --- a/internal/plans/planproto/planfile.proto +++ b/internal/plans/planproto/planfile.proto @@ -451,10 +451,18 @@ message ActionInvocationInstance { repeated ResourceInstanceActionChange linked_resources = 3; DynamicValue config_value = 4; - string triggering_resource_addr = 5; - ActionTriggerEvent trigger_event = 6; - int64 action_trigger_block_index = 7; - int64 actions_list_index = 8; + oneof action_trigger { + LifecycleActionTrigger lifecycle_action_trigger = 5; + } +} + +// LifecycleActionTrigger contains details on the conditions that led to the +// triggering of an action. +message LifecycleActionTrigger { + string triggering_resource_addr = 1; + ActionTriggerEvent trigger_event = 2; + int64 action_trigger_block_index = 3; + int64 actions_list_index = 4; } message ResourceInstanceActionChange { diff --git a/internal/terraform/context_plan_actions_test.go b/internal/terraform/context_plan_actions_test.go index 9a87a4b5af..6055ca6228 100644 --- a/internal/terraform/context_plan_actions_test.go +++ b/internal/terraform/context_plan_actions_test.go @@ -107,18 +107,23 @@ resource "test_object" "a" { t.Fatalf("expected action address to be 'action.test_unlinked.hello', got '%s'", action.Addr) } - if !action.TriggeringResourceAddr.Equal(mustResourceInstanceAddr("test_object.a")) { - t.Fatalf("expected action to have a triggering resource address 'test_object.a', got '%s'", action.TriggeringResourceAddr) + at, ok := action.ActionTrigger.(plans.LifecycleActionTrigger) + if !ok { + t.Fatalf("expected action trigger to be a LifecycleActionTrigger, got %T", action.ActionTrigger) } - if action.ActionTriggerBlockIndex != 0 { - t.Fatalf("expected action to have a triggering block index of 0, got %d", action.ActionTriggerBlockIndex) + if !at.TriggeringResourceAddr.Equal(mustResourceInstanceAddr("test_object.a")) { + t.Fatalf("expected action to have a triggering resource address 'test_object.a', got '%s'", at.TriggeringResourceAddr) } - if action.TriggerEvent != configs.BeforeCreate { - t.Fatalf("expected action to have a triggering event of 'before_create', got '%s'", action.TriggerEvent) + + if at.ActionTriggerBlockIndex != 0 { + t.Fatalf("expected action to have a triggering block index of 0, got %d", at.ActionTriggerBlockIndex) + } + if at.TriggerEvent() != configs.BeforeCreate { + t.Fatalf("expected action to have a triggering event of 'before_create', got '%s'", at.TriggerEvent()) } - if action.ActionsListIndex != 0 { - t.Fatalf("expected action to have a actions list index of 0, got %d", action.ActionsListIndex) + if at.ActionsListIndex != 0 { + t.Fatalf("expected action to have a actions list index of 0, got %d", at.ActionsListIndex) } if action.ProviderAddr.Provider != addrs.NewDefaultProvider("test") { @@ -902,18 +907,23 @@ resource "other_object" "a" { t.Fatalf("expected action address to be 'module.mod.action.test_unlinked.hello', got '%s'", action.Addr) } - if !action.TriggeringResourceAddr.Equal(mustResourceInstanceAddr("module.mod.other_object.a")) { - t.Fatalf("expected action to have triggering resource address 'module.mod.other_object.a', but it is %s", action.TriggeringResourceAddr) + at, ok := action.ActionTrigger.(plans.LifecycleActionTrigger) + if !ok { + t.Fatalf("expected action trigger to be a LifecycleActionTrigger, got %T", action.ActionTrigger) + } + + if !at.TriggeringResourceAddr.Equal(mustResourceInstanceAddr("module.mod.other_object.a")) { + t.Fatalf("expected action to have triggering resource address 'module.mod.other_object.a', but it is %s", at.TriggeringResourceAddr) } - if action.ActionTriggerBlockIndex != 0 { - t.Fatalf("expected action to have a triggering block index of 0, got %d", action.ActionTriggerBlockIndex) + if at.ActionTriggerBlockIndex != 0 { + t.Fatalf("expected action to have a triggering block index of 0, got %d", at.ActionTriggerBlockIndex) } - if action.TriggerEvent != configs.BeforeCreate { - t.Fatalf("expected action to have a triggering event of 'before_create', got '%s'", action.TriggerEvent) + if at.TriggerEvent() != configs.BeforeCreate { + t.Fatalf("expected action to have a triggering event of 'before_create', got '%s'", at.TriggerEvent()) } - if action.ActionsListIndex != 0 { - t.Fatalf("expected action to have a actions list index of 0, got %d", action.ActionsListIndex) + if at.ActionsListIndex != 0 { + t.Fatalf("expected action to have a actions list index of 0, got %d", at.ActionsListIndex) } if action.ProviderAddr.Provider != addrs.NewDefaultProvider("test") { @@ -951,7 +961,15 @@ resource "other_object" "a" { // We know we are run within two child modules, so we can just sort by the triggering resource address slices.SortFunc(p.Changes.ActionInvocations, func(a, b *plans.ActionInvocationInstanceSrc) int { - if a.TriggeringResourceAddr.String() < b.TriggeringResourceAddr.String() { + at, ok := a.ActionTrigger.(plans.LifecycleActionTrigger) + if !ok { + t.Fatalf("expected action trigger to be a LifecycleActionTrigger, got %T", a.ActionTrigger) + } + bt, ok := b.ActionTrigger.(plans.LifecycleActionTrigger) + if !ok { + t.Fatalf("expected action trigger to be a LifecycleActionTrigger, got %T", b.ActionTrigger) + } + if at.TriggeringResourceAddr.String() < bt.TriggeringResourceAddr.String() { return -1 } else { return 1 @@ -963,18 +981,20 @@ resource "other_object" "a" { t.Fatalf("expected action address to be 'module.mod[0].action.test_unlinked.hello', got '%s'", action.Addr) } - if !action.TriggeringResourceAddr.Equal(mustResourceInstanceAddr("module.mod[0].other_object.a")) { - t.Fatalf("expected action to have triggering resource address 'module.mod[0].other_object.a', but it is %s", action.TriggeringResourceAddr) + at := action.ActionTrigger.(plans.LifecycleActionTrigger) + + if !at.TriggeringResourceAddr.Equal(mustResourceInstanceAddr("module.mod[0].other_object.a")) { + t.Fatalf("expected action to have triggering resource address 'module.mod[0].other_object.a', but it is %s", at.TriggeringResourceAddr) } - if action.ActionTriggerBlockIndex != 0 { - t.Fatalf("expected action to have a triggering block index of 0, got %d", action.ActionTriggerBlockIndex) + if at.ActionTriggerBlockIndex != 0 { + t.Fatalf("expected action to have a triggering block index of 0, got %d", at.ActionTriggerBlockIndex) } - if action.TriggerEvent != configs.BeforeCreate { - t.Fatalf("expected action to have a triggering event of 'before_create', got '%s'", action.TriggerEvent) + if at.TriggerEvent() != configs.BeforeCreate { + t.Fatalf("expected action to have a triggering event of 'before_create', got '%s'", at.TriggerEvent()) } - if action.ActionsListIndex != 0 { - t.Fatalf("expected action to have a actions list index of 0, got %d", action.ActionsListIndex) + if at.ActionsListIndex != 0 { + t.Fatalf("expected action to have a actions list index of 0, got %d", at.ActionsListIndex) } if action.ProviderAddr.Provider != addrs.NewDefaultProvider("test") { @@ -986,8 +1006,10 @@ resource "other_object" "a" { t.Fatalf("expected action address to be 'module.mod[1].action.test_unlinked.hello', got '%s'", action2.Addr) } - if !action2.TriggeringResourceAddr.Equal(mustResourceInstanceAddr("module.mod[1].other_object.a")) { - t.Fatalf("expected action to have triggering resource address 'module.mod[1].other_object.a', but it is %s", action2.TriggeringResourceAddr) + a2t := action2.ActionTrigger.(plans.LifecycleActionTrigger) + + if !a2t.TriggeringResourceAddr.Equal(mustResourceInstanceAddr("module.mod[1].other_object.a")) { + t.Fatalf("expected action to have triggering resource address 'module.mod[1].other_object.a', but it is %s", a2t.TriggeringResourceAddr) } }, }, @@ -1028,8 +1050,13 @@ resource "other_object" "a" { t.Fatalf("expected action address to be 'module.mod.action.test_unlinked.hello', got '%s'", action.Addr) } - if !action.TriggeringResourceAddr.Equal(mustResourceInstanceAddr("module.mod.other_object.a")) { - t.Fatalf("expected action to have triggering resource address 'module.mod.other_object.a', but it is %s", action.TriggeringResourceAddr) + at, ok := action.ActionTrigger.(plans.LifecycleActionTrigger) + if !ok { + t.Fatalf("expected action trigger to be a lifecycle action trigger, got %T", action.ActionTrigger) + } + + if !at.TriggeringResourceAddr.Equal(mustResourceInstanceAddr("module.mod.other_object.a")) { + t.Fatalf("expected action to have triggering resource address 'module.mod.other_object.a', but it is %s", at.TriggeringResourceAddr) } if action.ProviderAddr.Module.String() != "module.mod" { @@ -1072,9 +1099,13 @@ resource "other_object" "a" { if action.Addr.String() != "action.ecosystem_unlinked.hello" { t.Fatalf("expected action address to be 'action.ecosystem_unlinked.hello', got '%s'", action.Addr) } + at, ok := action.ActionTrigger.(plans.LifecycleActionTrigger) + if !ok { + t.Fatalf("expected action trigger to be a LifecycleActionTrigger, got %T", action.ActionTrigger) + } - if !action.TriggeringResourceAddr.Equal(mustResourceInstanceAddr("other_object.a")) { - t.Fatalf("expected action to have triggering resource address 'other_object.a', but it is %s", action.TriggeringResourceAddr) + if !at.TriggeringResourceAddr.Equal(mustResourceInstanceAddr("other_object.a")) { + t.Fatalf("expected action to have triggering resource address 'other_object.a', but it is %s", at.TriggeringResourceAddr) } if action.ProviderAddr.Provider.Namespace != "danielmschmidt" { @@ -1114,8 +1145,13 @@ resource "other_object" "a" { t.Fatalf("expected action address to be 'action.test_unlinked.hello', got '%s'", action.Addr) } - if !action.TriggeringResourceAddr.Equal(mustResourceInstanceAddr("other_object.a")) { - t.Fatalf("expected action to have triggering resource address 'other_object.a', but it is %s", action.TriggeringResourceAddr) + at, ok := action.ActionTrigger.(plans.LifecycleActionTrigger) + if !ok { + t.Fatalf("expected action trigger to be a LifecycleActionTrigger, got %T", action.ActionTrigger) + } + + if !at.TriggeringResourceAddr.Equal(mustResourceInstanceAddr("other_object.a")) { + t.Fatalf("expected action to have triggering resource address 'other_object.a', but it is %s", at.TriggeringResourceAddr) } if action.ProviderAddr.Alias != "aliased" { diff --git a/internal/terraform/hook.go b/internal/terraform/hook.go index 1217372f5b..a7b8704900 100644 --- a/internal/terraform/hook.go +++ b/internal/terraform/hook.go @@ -38,15 +38,11 @@ type HookResourceIdentity struct { type HookActionIdentity struct { Addr addrs.AbsActionInstance - // If run as part of a plan / apply we also have the values below - // (if CLI triggered they are not applicable) - TriggeringResourceAddr addrs.AbsResourceInstance - ActionTriggerBlockIndex int - ActionsListIndex int + ActionTrigger plans.ActionTrigger } func (i *HookActionIdentity) String() string { - return i.Addr.String() + " (triggered by " + i.TriggeringResourceAddr.String() + ")" + return i.Addr.String() + " (triggered by " + i.ActionTrigger.String() + ")" } // Hook is the interface that must be implemented to hook into various diff --git a/internal/terraform/node_action_apply.go b/internal/terraform/node_action_apply.go index 75740616f1..96eee3ff07 100644 --- a/internal/terraform/node_action_apply.go +++ b/internal/terraform/node_action_apply.go @@ -57,13 +57,14 @@ func invokeActions(ctx EvalContext, actionInvocations []*plans.ActionInvocationI // This way we have the correct order of execution. orderedActionInvocations := make([]*plans.ActionInvocationInstance, 0, len(actionInvocations)) for _, invocation := range actionInvocations { - ai := ctx.Changes().GetActionInvocation(invocation.Addr, invocation.TriggeringResourceAddr, invocation.ActionTriggerBlockIndex, invocation.ActionsListIndex) + at := invocation.ActionTrigger.(plans.LifecycleActionTrigger) // We only support lifecycle actions for now. + ai := ctx.Changes().GetActionInvocation(invocation.Addr, at) if ai == nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, fmt.Sprintf("Failed to find action invocation instance %s in changes.", ai.Addr), - fmt.Sprintf("The action invocation instance %s was not found in the changes for %s.", ai.Addr, ai.TriggeringResourceAddr.String()), + fmt.Sprintf("The action invocation instance %s was not found in the changes for %s.", ai.Addr, at.TriggeringResourceAddr.String()), )) return finishedActionInvocations, diags } @@ -71,10 +72,12 @@ func invokeActions(ctx EvalContext, actionInvocations []*plans.ActionInvocationI orderedActionInvocations = append(orderedActionInvocations, ai) } sort.Slice(orderedActionInvocations, func(i, j int) bool { - if orderedActionInvocations[i].ActionTriggerBlockIndex == orderedActionInvocations[j].ActionTriggerBlockIndex { - return orderedActionInvocations[i].ActionsListIndex < orderedActionInvocations[j].ActionsListIndex + ati := orderedActionInvocations[i].ActionTrigger.(plans.LifecycleActionTrigger) + atj := orderedActionInvocations[i].ActionTrigger.(plans.LifecycleActionTrigger) + if ati.ActionTriggerBlockIndex == atj.ActionTriggerBlockIndex { + return ati.ActionsListIndex < atj.ActionsListIndex } - return orderedActionInvocations[i].ActionTriggerBlockIndex < orderedActionInvocations[j].ActionTriggerBlockIndex + return ati.ActionTriggerBlockIndex < atj.ActionTriggerBlockIndex }) // Now we ensure we have an expanded action instance for each action invocations. @@ -139,10 +142,8 @@ func invokeActions(ctx EvalContext, actionInvocations []*plans.ActionInvocationI } hookIdentity := HookActionIdentity{ - Addr: ai.Addr, - TriggeringResourceAddr: ai.TriggeringResourceAddr, - ActionTriggerBlockIndex: ai.ActionTriggerBlockIndex, - ActionsListIndex: ai.ActionsListIndex, + Addr: ai.Addr, + ActionTrigger: ai.ActionTrigger, } ctx.Hook(func(h Hook) (HookAction, error) { @@ -288,10 +289,10 @@ func areBeforeActionInvocations(actionInvocations []*plans.ActionInvocationInsta if len(actionInvocations) == 0 { panic("areBeforeActionInvocations called with empty actionInvocations") } - firstEvent := actionInvocations[0].TriggerEvent + firstEvent := actionInvocations[0].ActionTrigger.TriggerEvent() for _, ai := range actionInvocations { - if ai.TriggerEvent != firstEvent { - panic(fmt.Sprintf("areBeforeActionInvocations called with action invocations with different trigger events: %s != %s", firstEvent, ai.TriggerEvent)) + if ai.ActionTrigger.TriggerEvent() != firstEvent { + panic(fmt.Sprintf("areBeforeActionInvocations called with action invocations with different trigger events: %s != %s", firstEvent, ai.ActionTrigger.TriggerEvent())) } } diff --git a/internal/terraform/node_resource_plan_instance.go b/internal/terraform/node_resource_plan_instance.go index cff590642f..c0e5b6d804 100644 --- a/internal/terraform/node_resource_plan_instance.go +++ b/internal/terraform/node_resource_plan_instance.go @@ -647,13 +647,15 @@ func (n *NodePlannableResourceInstance) planActionTriggers(ctx EvalContext, chan } ctx.Changes().AppendActionInvocation(&plans.ActionInvocationInstance{ - Addr: absActionAddr, - ProviderAddr: actionInstance.ProviderAddr, - TriggeringResourceAddr: n.Addr, - TriggerEvent: *triggeringEvent, - ActionTriggerBlockIndex: i, - ActionsListIndex: j, - ConfigValue: actionInstance.ConfigValue, + Addr: absActionAddr, + ProviderAddr: actionInstance.ProviderAddr, + ConfigValue: actionInstance.ConfigValue, + ActionTrigger: plans.LifecycleActionTrigger{ + TriggeringResourceAddr: n.Addr, + ActionTriggerEvent: *triggeringEvent, + ActionTriggerBlockIndex: i, + ActionsListIndex: j, + }, }) } } diff --git a/internal/terraform/transform_diff.go b/internal/terraform/transform_diff.go index e696ba8d4d..e41dc3cc40 100644 --- a/internal/terraform/transform_diff.go +++ b/internal/terraform/transform_diff.go @@ -89,9 +89,13 @@ func (t *DiffTransformer) Transform(g *Graph) error { runBeforeNode := addrs.MakeMap[addrs.AbsResourceInstance, []*plans.ActionInvocationInstanceSrc]() runAfterNode := addrs.MakeMap[addrs.AbsResourceInstance, []*plans.ActionInvocationInstanceSrc]() for _, ai := range changes.ActionInvocations { + if _, ok := ai.ActionTrigger.(plans.LifecycleActionTrigger); !ok { + continue + } + ait := ai.ActionTrigger.(plans.LifecycleActionTrigger) var targetMap addrs.Map[addrs.AbsResourceInstance, []*plans.ActionInvocationInstanceSrc] - switch ai.TriggerEvent { + switch ait.ActionTriggerEvent { case configs.BeforeCreate, configs.BeforeUpdate, configs.BeforeDestroy: targetMap = runBeforeNode case configs.AfterCreate, configs.AfterUpdate, configs.AfterDestroy: @@ -101,11 +105,11 @@ func (t *DiffTransformer) Transform(g *Graph) error { } basis := []*plans.ActionInvocationInstanceSrc{} - if targetMap.Has(ai.TriggeringResourceAddr) { - basis = targetMap.Get(ai.TriggeringResourceAddr) + if targetMap.Has(ait.TriggeringResourceAddr) { + basis = targetMap.Get(ait.TriggeringResourceAddr) } - targetMap.Put(ai.TriggeringResourceAddr, append(basis, ai)) + targetMap.Put(ait.TriggeringResourceAddr, append(basis, ai)) } for _, rc := range changes.Resources {