You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
terraform/internal/command/jsonplan/action_invocations.go

236 lines
7.7 KiB

// 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.LifecycleActionTrigger:
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
}