terraform/internal/command/jsonplan/action_invocations.go
Kristin Laemmert 70e074b8aa chore (actions): rename LifecycleActionTrigger -> ResourceActionTrigger in plan and proto
We'd previously removed all other references to "lifecycle" actions, which made this reference stand out. ResourceLifecycleActionTrigger is probably the most accurate name, but as this type just needs to be differentiated from InvokeActionTrigger I thought "resource" was enough (and I specifically wanted= to avoid lifecycle at all). I'm not super attached to the name, but I did think it would be clearer if we avoided Lifecycle as much as possible, since that's got some overlap with action subtypes.

In this instance, we call it a LifecycleActionTrigger because it's come from the resource's `lifecycle` block. This doesn't directly relate to the concept of LifecycleActions - even if we expand the design to have multiple action types (for example generic and lifecycle actions), both those actions types would use this same Trigger struct.
2026-02-23 15:17:45 -05:00

235 lines
7.7 KiB
Go

// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package jsonplan
import (
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
"github.com/hashicorp/terraform/internal/command/jsonstate"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/terraform"
)
type ActionInvocation struct {
// Address is the absolute action address
Address string `json:"address,omitempty"`
// Type is the type of the action
Type string `json:"type,omitempty"`
// Name is the name of the action
Name string `json:"name,omitempty"`
// ConfigValues is the JSON representation of the values in the config block of the action
ConfigValues json.RawMessage `json:"config_values,omitempty"`
ConfigSensitive json.RawMessage `json:"config_sensitive,omitempty"`
ConfigUnknown json.RawMessage `json:"config_unknown,omitempty"`
// ProviderName allows the property "type" to be interpreted unambiguously
// in the unusual situation where a provider offers a type whose
// name does not start with its own name, such as the "googlebeta" provider
// offering "google_compute_instance".
ProviderName string `json:"provider_name,omitempty"`
LifecycleActionTrigger *LifecycleActionTrigger `json:"lifecycle_action_trigger,omitempty"`
InvokeActionTrigger *InvokeActionTrigger `json:"invoke_action_trigger,omitempty"`
}
type LifecycleActionTrigger struct {
TriggeringResourceAddress string `json:"triggering_resource_address,omitempty"`
ActionTriggerEvent string `json:"action_trigger_event,omitempty"`
ActionTriggerBlockIndex int `json:"action_trigger_block_index"`
ActionsListIndex int `json:"actions_list_index"`
}
type InvokeActionTrigger struct{}
func ActionInvocationCompare(a, b ActionInvocation) int {
// invoke action triggers go first, then compare addresses between invoke
// action triggers.
if a.InvokeActionTrigger != nil {
if b.InvokeActionTrigger != nil {
return strings.Compare(a.Address, b.Address)
}
return -1
}
if b.InvokeActionTrigger != nil {
return 1
}
if a.LifecycleActionTrigger != nil && b.LifecycleActionTrigger != nil {
latA := *a.LifecycleActionTrigger
latB := *b.LifecycleActionTrigger
if latA.TriggeringResourceAddress < latB.TriggeringResourceAddress {
return -1
} else if latA.TriggeringResourceAddress > latB.TriggeringResourceAddress {
return 1
}
if latA.ActionTriggerBlockIndex < latB.ActionTriggerBlockIndex {
return -1
} else if latA.ActionTriggerBlockIndex > latB.ActionTriggerBlockIndex {
return 1
}
if latA.ActionsListIndex < latB.ActionsListIndex {
return -1
} else if latA.ActionsListIndex > latB.ActionsListIndex {
return 1
}
}
return 0
}
func MarshalActionInvocations(actions []*plans.ActionInvocationInstanceSrc, schemas *terraform.Schemas) ([]ActionInvocation, error) {
ret := make([]ActionInvocation, 0, len(actions))
for _, action := range actions {
ai, err := MarshalActionInvocation(action, schemas)
if err != nil {
return ret, fmt.Errorf("failed to decode action %s: %w", action.Addr, err)
}
ret = append(ret, ai)
}
return ret, nil
}
func MarshalActionInvocation(action *plans.ActionInvocationInstanceSrc, schemas *terraform.Schemas) (ActionInvocation, error) {
ai := ActionInvocation{
Address: action.Addr.String(),
Type: action.Addr.Action.Action.Type,
Name: action.Addr.Action.Action.Name,
ProviderName: action.ProviderAddr.Provider.String(),
}
schema := schemas.ActionTypeConfig(
action.ProviderAddr.Provider,
action.Addr.Action.Action.Type,
)
if schema.ConfigSchema == nil {
return ai, fmt.Errorf("no schema found for %s (in provider %s)", action.Addr.Action.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.ResourceActionTrigger:
ai.LifecycleActionTrigger = &LifecycleActionTrigger{
TriggeringResourceAddress: at.TriggeringResourceAddr.String(),
ActionTriggerEvent: at.TriggerEvent().String(),
ActionTriggerBlockIndex: at.ActionTriggerBlockIndex,
ActionsListIndex: at.ActionsListIndex,
}
case *plans.InvokeActionTrigger:
ai.InvokeActionTrigger = new(InvokeActionTrigger)
default:
return ai, fmt.Errorf("unsupported action trigger type: %T", at)
}
var config []byte
var sensitive []byte
var unknown []byte
if actionDec.ConfigValue != cty.NilVal {
unmarkedValue, 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)
}
unknownValue := unknownAsBool(unmarkedValue)
unknown, err = ctyjson.Marshal(unknownValue, unknownValue.Type())
if err != nil {
return ai, err
}
configValue := omitUnknowns(unmarkedValue)
config, err = ctyjson.Marshal(configValue, configValue.Type())
if err != nil {
return ai, err
}
sensitivePaths = append(sensitivePaths, schema.ConfigSchema.SensitivePaths(unmarkedValue, nil)...)
cs := jsonstate.SensitiveAsBool(marks.MarkPaths(unmarkedValue, marks.Sensitive, sensitivePaths))
sensitive, err = ctyjson.Marshal(cs, cs.Type())
if err != nil {
return ai, err
}
}
ai.ConfigValues = config
ai.ConfigSensitive = sensitive
ai.ConfigUnknown = unknown
return ai, nil
}
// DeferredActionInvocation is a description of an action invocation that has been
// deferred for some reason.
type DeferredActionInvocation struct {
// Reason is the reason why this action was deferred.
Reason string `json:"reason"`
// Change contains any information we have about the deferred change.
ActionInvocation ActionInvocation `json:"action_invocation"`
}
func MarshalDeferredActionInvocations(dais []*plans.DeferredActionInvocationSrc, 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 := MarshalActionInvocation(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
}