terraform/internal/plans/changes_src.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

644 lines
20 KiB
Go

// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package plans
import (
"fmt"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/genconfig"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/schemarepo"
"github.com/hashicorp/terraform/internal/states"
)
// ChangesSrc describes various actions that Terraform will attempt to take if
// the corresponding plan is applied.
//
// A Changes object can be rendered into a visual diff (by the caller, using
// code in another package) for display to the user.
type ChangesSrc struct {
// Resources tracks planned changes to resource instance objects.
Resources []*ResourceInstanceChangeSrc
Queries []*QueryInstanceSrc
// ActionInvocations tracks planned action invocations, which may have
// embedded resource instance changes.
ActionInvocations []*ActionInvocationInstanceSrc
// Outputs tracks planned changes output values.
//
// Note that although an in-memory plan contains planned changes for
// outputs throughout the configuration, a plan serialized
// to disk retains only the root outputs because they are
// externally-visible, while other outputs are implementation details and
// can be easily re-calculated during the apply phase. Therefore only root
// module outputs will survive a round-trip through a plan file.
Outputs []*OutputChangeSrc
}
func NewChangesSrc() *ChangesSrc {
return &ChangesSrc{}
}
func (c *ChangesSrc) Empty() bool {
for _, res := range c.Resources {
if res.Action != NoOp || res.Moved() {
return false
}
if res.Importing != nil {
return false
}
}
for _, out := range c.Outputs {
if out.Addr.Module.IsRoot() && out.Action != NoOp {
return false
}
}
if len(c.ActionInvocations) > 0 {
// action invocations can be applied
return false
}
return true
}
// ResourceInstance returns the planned change for the current object of the
// resource instance of the given address, if any. Returns nil if no change is
// planned.
func (c *ChangesSrc) ResourceInstance(addr addrs.AbsResourceInstance) *ResourceInstanceChangeSrc {
for _, rc := range c.Resources {
if rc.Addr.Equal(addr) && rc.DeposedKey == states.NotDeposed {
return rc
}
}
return nil
}
// ResourceInstanceDeposed returns the plan change of a deposed object of
// the resource instance of the given address, if any. Returns nil if no change
// is planned.
func (c *ChangesSrc) ResourceInstanceDeposed(addr addrs.AbsResourceInstance, key states.DeposedKey) *ResourceInstanceChangeSrc {
for _, rc := range c.Resources {
if rc.Addr.Equal(addr) && rc.DeposedKey == key {
return rc
}
}
return nil
}
// OutputValue returns the planned change for the output value with the
//
// given address, if any. Returns nil if no change is planned.
func (c *ChangesSrc) OutputValue(addr addrs.AbsOutputValue) *OutputChangeSrc {
for _, oc := range c.Outputs {
if oc.Addr.Equal(addr) {
return oc
}
}
return nil
}
// Decode decodes all the stored resource and output changes into a new *Changes value.
func (c *ChangesSrc) Decode(schemas *schemarepo.Schemas) (*Changes, error) {
changes := NewChanges()
for _, rcs := range c.Resources {
p, ok := schemas.Providers[rcs.ProviderAddr.Provider]
if !ok {
return nil, fmt.Errorf("ChangesSrc.Decode: missing provider %s for %s", rcs.ProviderAddr, rcs.Addr)
}
var schema providers.Schema
switch rcs.Addr.Resource.Resource.Mode {
case addrs.ManagedResourceMode:
schema = p.ResourceTypes[rcs.Addr.Resource.Resource.Type]
case addrs.DataResourceMode:
schema = p.DataSources[rcs.Addr.Resource.Resource.Type]
default:
panic(fmt.Sprintf("unexpected resource mode %s", rcs.Addr.Resource.Resource.Mode))
}
if schema.Body == nil {
return nil, fmt.Errorf("ChangesSrc.Decode: missing schema for %s", rcs.Addr)
}
rc, err := rcs.Decode(schema)
if err != nil {
return nil, err
}
rc.Before = marks.MarkPaths(rc.Before, marks.Sensitive, rcs.BeforeSensitivePaths)
rc.After = marks.MarkPaths(rc.After, marks.Sensitive, rcs.AfterSensitivePaths)
changes.Resources = append(changes.Resources, rc)
}
for _, qis := range c.Queries {
p, ok := schemas.Providers[qis.ProviderAddr.Provider]
if !ok {
return nil, fmt.Errorf("ChangesSrc.Decode: missing provider %s for %s", qis.ProviderAddr, qis.Addr)
}
schema := p.ListResourceTypes[qis.Addr.Resource.Resource.Type]
if schema.Body == nil {
return nil, fmt.Errorf("ChangesSrc.Decode: missing schema for %s", qis.Addr)
}
query, err := qis.Decode(schema)
if err != nil {
return nil, err
}
changes.Queries = append(changes.Queries, query)
}
for _, ais := range c.ActionInvocations {
p, ok := schemas.Providers[ais.ProviderAddr.Provider]
if !ok {
return nil, fmt.Errorf("ChangesSrc.Decode: missing provider %s for %s", ais.ProviderAddr, ais.Addr)
}
schema, ok := p.Actions[ais.Addr.Action.Action.Type]
if !ok {
return nil, fmt.Errorf("ChangesSrc.Decode: missing schema for %s", ais.Addr.Action.Action.Type)
}
action, err := ais.Decode(&schema)
if err != nil {
return nil, err
}
changes.ActionInvocations = append(changes.ActionInvocations, action)
}
for _, ocs := range c.Outputs {
oc, err := ocs.Decode()
if err != nil {
return nil, err
}
changes.Outputs = append(changes.Outputs, oc)
}
return changes, nil
}
// AppendResourceInstanceChange records the given resource instance change in
// the set of planned resource changes.
func (c *ChangesSrc) AppendResourceInstanceChange(change *ResourceInstanceChangeSrc) {
if c == nil {
panic("AppendResourceInstanceChange on nil ChangesSync")
}
s := change.DeepCopy()
c.Resources = append(c.Resources, s)
}
type QueryInstanceSrc struct {
Addr addrs.AbsResourceInstance
ProviderAddr addrs.AbsProviderConfig
Results DynamicValue
Generated genconfig.ImportGroup
}
func (qis *QueryInstanceSrc) Decode(schema providers.Schema) (*QueryInstance, error) {
query, err := qis.Results.Decode(schema.Body.ImpliedType())
if err != nil {
return nil, err
}
return &QueryInstance{
Addr: qis.Addr,
Results: QueryResults{
Value: query,
Generated: qis.Generated,
},
ProviderAddr: qis.ProviderAddr,
}, nil
}
// ResourceInstanceChangeSrc is a not-yet-decoded ResourceInstanceChange.
// Pass the associated resource type's schema type to method Decode to
// obtain a ResourceInstanceChange.
type ResourceInstanceChangeSrc struct {
// Addr is the absolute address of the resource instance that the change
// will apply to.
//
// THIS IS NOT A SUFFICIENT UNIQUE IDENTIFIER! It doesn't consider the
// fact that multiple objects for the same resource instance might be
// present in the same plan; use the ObjectAddr method instead if you
// need a unique address for a particular change.
Addr addrs.AbsResourceInstance
// DeposedKey is the identifier for a deposed object associated with the
// given instance, or states.NotDeposed if this change applies to the
// current object.
//
// A Replace change for a resource with create_before_destroy set will
// create a new DeposedKey temporarily during replacement. In that case,
// DeposedKey in the plan is always states.NotDeposed, representing that
// the current object is being replaced with the deposed.
DeposedKey states.DeposedKey
// PrevRunAddr is the absolute address that this resource instance had at
// the conclusion of a previous run.
//
// This will typically be the same as Addr, but can be different if the
// previous resource instance was subject to a "moved" block that we
// handled in the process of creating this plan.
//
// For the initial creation of a resource instance there isn't really any
// meaningful "previous run address", but PrevRunAddr will still be set
// equal to Addr in that case in order to simplify logic elsewhere which
// aims to detect and react to the movement of instances between addresses.
PrevRunAddr addrs.AbsResourceInstance
// Provider is the address of the provider configuration that was used
// to plan this change, and thus the configuration that must also be
// used to apply it.
ProviderAddr addrs.AbsProviderConfig
// ChangeSrc is an embedded description of the not-yet-decoded change.
ChangeSrc
// ActionReason is an optional extra indication of why we chose the
// action recorded in Change.Action for this particular resource instance.
//
// This is an approximate mechanism only for the purpose of explaining the
// plan to end-users in the UI and is not to be used for any
// decision-making during the apply step; if apply behavior needs to vary
// depending on the "action reason" then the information for that decision
// must be recorded more precisely elsewhere for that purpose.
//
// See the field of the same name in ResourceInstanceChange for more
// details.
ActionReason ResourceInstanceChangeActionReason
// RequiredReplace is a set of paths that caused the change action to be
// Replace rather than Update. Always nil if the change action is not
// Replace.
RequiredReplace cty.PathSet
// Private allows a provider to stash any extra data that is opaque to
// Terraform that relates to this change. Terraform will save this
// byte-for-byte and return it to the provider in the apply call.
Private []byte
}
func (rcs *ResourceInstanceChangeSrc) ObjectAddr() addrs.AbsResourceInstanceObject {
return addrs.AbsResourceInstanceObject{
ResourceInstance: rcs.Addr,
DeposedKey: rcs.DeposedKey,
}
}
// Decode unmarshals the raw representation of the instance object being
// changed. Pass the implied type of the corresponding resource type schema
// for correct operation.
func (rcs *ResourceInstanceChangeSrc) Decode(schema providers.Schema) (*ResourceInstanceChange, error) {
change, err := rcs.ChangeSrc.Decode(&schema)
if err != nil {
return nil, err
}
prevRunAddr := rcs.PrevRunAddr
if prevRunAddr.Resource.Resource.Type == "" {
// Suggests an old caller that hasn't been properly updated to
// populate this yet.
prevRunAddr = rcs.Addr
}
return &ResourceInstanceChange{
Addr: rcs.Addr,
PrevRunAddr: prevRunAddr,
DeposedKey: rcs.DeposedKey,
ProviderAddr: rcs.ProviderAddr,
Change: *change,
ActionReason: rcs.ActionReason,
RequiredReplace: rcs.RequiredReplace,
Private: rcs.Private,
}, nil
}
// DeepCopy creates a copy of the receiver where any pointers to nested mutable
// values are also copied, thus ensuring that future mutations of the receiver
// will not affect the copy.
//
// Some types used within a resource change are immutable by convention even
// though the Go language allows them to be mutated, such as the types from
// the addrs package. These are _not_ copied by this method, under the
// assumption that callers will behave themselves.
func (rcs *ResourceInstanceChangeSrc) DeepCopy() *ResourceInstanceChangeSrc {
if rcs == nil {
return nil
}
ret := *rcs
ret.RequiredReplace = cty.NewPathSet(ret.RequiredReplace.List()...)
if len(ret.Private) != 0 {
private := make([]byte, len(ret.Private))
copy(private, ret.Private)
ret.Private = private
}
ret.ChangeSrc.Before = ret.ChangeSrc.Before.Copy()
ret.ChangeSrc.After = ret.ChangeSrc.After.Copy()
return &ret
}
func (rcs *ResourceInstanceChangeSrc) Moved() bool {
return !rcs.Addr.Equal(rcs.PrevRunAddr)
}
// OutputChangeSrc describes a change to an output value.
type OutputChangeSrc struct {
// Addr is the absolute address of the output value that the change
// will apply to.
Addr addrs.AbsOutputValue
// ChangeSrc is an embedded description of the not-yet-decoded change.
//
// For output value changes, the type constraint for the DynamicValue
// instances is always cty.DynamicPseudoType.
ChangeSrc
// Sensitive, if true, indicates that either the old or new value in the
// change is sensitive and so a rendered version of the plan in the UI
// should elide the actual values while still indicating the action of the
// change.
Sensitive bool
}
// Decode unmarshals the raw representation of the output value being
// changed.
func (ocs *OutputChangeSrc) Decode() (*OutputChange, error) {
change, err := ocs.ChangeSrc.Decode(nil)
if err != nil {
return nil, err
}
return &OutputChange{
Addr: ocs.Addr,
Change: *change,
Sensitive: ocs.Sensitive,
}, nil
}
// DeepCopy creates a copy of the receiver where any pointers to nested mutable
// values are also copied, thus ensuring that future mutations of the receiver
// will not affect the copy.
//
// Some types used within a resource change are immutable by convention even
// though the Go language allows them to be mutated, such as the types from
// the addrs package. These are _not_ copied by this method, under the
// assumption that callers will behave themselves.
func (ocs *OutputChangeSrc) DeepCopy() *OutputChangeSrc {
if ocs == nil {
return nil
}
ret := *ocs
ret.ChangeSrc.Before = ret.ChangeSrc.Before.Copy()
ret.ChangeSrc.After = ret.ChangeSrc.After.Copy()
return &ret
}
// ImportingSrc is the part of a ChangeSrc that describes the embedded import
// action.
//
// The fields in here are subject to change, so downstream consumers should be
// prepared for backwards compatibility in case the contents changes.
type ImportingSrc struct {
// ID is the original ID of the imported resource.
ID string
// Identity is the original identity of the imported resource.
Identity DynamicValue
// Unknown is true if the ID was unknown when we tried to import it. This
// should only be true if the overall change is embedded within a deferred
// action.
Unknown bool
}
// Decode unmarshals the raw representation of the importing action.
func (is *ImportingSrc) Decode(identityType cty.Type) *Importing {
if is == nil {
return nil
}
if is.Unknown {
if is.Identity == nil {
return &Importing{
Target: cty.UnknownVal(cty.String),
}
}
return &Importing{
Target: cty.UnknownVal(cty.EmptyObject),
}
}
if is.Identity == nil {
return &Importing{
Target: cty.StringVal(is.ID),
}
}
target, err := is.Identity.Decode(identityType)
if err != nil {
return &Importing{
Target: target,
}
}
return nil
}
// ChangeSrc is a not-yet-decoded Change.
type ChangeSrc struct {
// Action defines what kind of change is being made.
Action Action
// Before and After correspond to the fields of the same name in Change,
// but have not yet been decoded from the serialized value used for
// storage.
Before, After DynamicValue
// BeforeIdentity and AfterIdentity correspond to the fields of the same name in Change,
// but have not yet been decoded from the serialized value used for
// storage.
BeforeIdentity, AfterIdentity DynamicValue
// BeforeSensitivePaths and AfterSensitivePaths are the paths for any
// values in Before or After (respectively) that are considered to be
// sensitive. The sensitive marks are removed from the in-memory values
// to enable encoding (marked values cannot be marshalled), and so we
// store the sensitive paths to allow re-marking later when we decode
// the serialized change.
BeforeSensitivePaths, AfterSensitivePaths []cty.Path
// Importing is present if the resource is being imported as part of this
// change.
//
// Use the simple presence of this field to detect if a ChangeSrc is to be
// imported, the contents of this structure may be modified going forward.
Importing *ImportingSrc
// GeneratedConfig contains any HCL config generated for this resource
// during planning, as a string. If GeneratedConfig is populated, Importing
// should be true. However, not all Importing changes contain generated
// config.
GeneratedConfig string
}
// Decode unmarshals the raw representations of the before and after values
// to produce a Change object. Pass the type constraint that the result must
// conform to.
//
// Where a ChangeSrc is embedded in some other struct, it's generally better
// to call the corresponding Decode method of that struct rather than working
// directly with its embedded Change.
func (cs *ChangeSrc) Decode(schema *providers.Schema) (*Change, error) {
var err error
ty := cty.DynamicPseudoType
identityType := cty.DynamicPseudoType
if schema != nil {
ty = schema.Body.ImpliedType()
identityType = schema.Identity.ImpliedType()
}
before := cty.NullVal(ty)
beforeIdentity := cty.NullVal(identityType)
after := cty.NullVal(ty)
afterIdentity := cty.NullVal(identityType)
if len(cs.Before) > 0 {
before, err = cs.Before.Decode(ty)
if err != nil {
return nil, fmt.Errorf("error decoding 'before' value: %s", err)
}
}
if len(cs.BeforeIdentity) > 0 {
beforeIdentity, err = cs.BeforeIdentity.Decode(identityType)
if err != nil {
return nil, fmt.Errorf("error decoding 'beforeIdentity' value: %s", err)
}
}
if len(cs.After) > 0 {
after, err = cs.After.Decode(ty)
if err != nil {
return nil, fmt.Errorf("error decoding 'after' value: %s", err)
}
}
if len(cs.AfterIdentity) > 0 {
afterIdentity, err = cs.AfterIdentity.Decode(identityType)
if err != nil {
return nil, fmt.Errorf("error decoding 'afterIdentity' value: %s", err)
}
}
return &Change{
Action: cs.Action,
Before: marks.MarkPaths(before, marks.Sensitive, cs.BeforeSensitivePaths),
BeforeIdentity: beforeIdentity,
After: marks.MarkPaths(after, marks.Sensitive, cs.AfterSensitivePaths),
AfterIdentity: afterIdentity,
Importing: cs.Importing.Decode(identityType),
GeneratedConfig: cs.GeneratedConfig,
}, nil
}
// AppendActionInvocationInstanceChange records the given resource instance change in
// the set of planned resource changes.
func (c *ChangesSrc) AppendActionInvocationInstanceChange(action *ActionInvocationInstanceSrc) {
if c == nil {
panic("AppendActionInvocationInstanceChange on nil ChangesSync")
}
a := action.DeepCopy()
c.ActionInvocations = append(c.ActionInvocations, a)
}
type ActionInvocationInstanceSrc struct {
Addr addrs.AbsActionInstance
ActionTrigger ActionTrigger
ConfigValue DynamicValue
SensitiveConfigPaths []cty.Path
ProviderAddr addrs.AbsProviderConfig
}
// Decode unmarshals the raw representation of actions.
func (acs *ActionInvocationInstanceSrc) Decode(schema *providers.ActionSchema) (*ActionInvocationInstance, error) {
ty := cty.DynamicPseudoType
if schema != nil {
ty = schema.ConfigSchema.ImpliedType()
}
config, err := acs.ConfigValue.Decode(ty)
if err != nil {
return nil, fmt.Errorf("error decoding 'config' value: %s", err)
}
markedConfigValue := marks.MarkPaths(config, marks.Sensitive, acs.SensitiveConfigPaths)
ai := &ActionInvocationInstance{
Addr: acs.Addr,
ActionTrigger: acs.ActionTrigger,
ProviderAddr: acs.ProviderAddr,
ConfigValue: markedConfigValue,
}
return ai, nil
}
func (acs *ActionInvocationInstanceSrc) DeepCopy() *ActionInvocationInstanceSrc {
if acs == nil {
return acs
}
ret := *acs
ret.ConfigValue = ret.ConfigValue.Copy()
return &ret
}
func (acs *ActionInvocationInstanceSrc) Less(other *ActionInvocationInstanceSrc) bool {
if acs.ActionTrigger.Equals(other.ActionTrigger) {
return acs.Addr.Less(other.Addr)
}
return acs.ActionTrigger.Less(other.ActionTrigger)
}
// FilterLaterActionInvocations returns the list of action invocations that
// should be triggered after this one. This function assumes the supplied list
// of action invocations has already been filtered to invocations against the
// same resource as the current invocation.
func (acs *ActionInvocationInstanceSrc) FilterLaterActionInvocations(actionInvocations []*ActionInvocationInstanceSrc) []*ActionInvocationInstanceSrc {
needleLat := acs.ActionTrigger.(*ResourceActionTrigger)
var laterInvocations []*ActionInvocationInstanceSrc
for _, invocation := range actionInvocations {
if lat, ok := invocation.ActionTrigger.(*ResourceActionTrigger); ok {
if sameTriggerEvent(lat, needleLat) && triggersLater(lat, needleLat) {
laterInvocations = append(laterInvocations, invocation)
}
}
}
return laterInvocations
}
func sameTriggerEvent(one, two *ResourceActionTrigger) bool {
return one.ActionTriggerEvent == two.ActionTriggerEvent
}
func triggersLater(one, two *ResourceActionTrigger) bool {
return one.ActionTriggerBlockIndex > two.ActionTriggerBlockIndex || (one.ActionTriggerBlockIndex == two.ActionTriggerBlockIndex && one.ActionsListIndex > two.ActionsListIndex)
}