terraform/internal/plans/changes_src.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

645 lines
20 KiB
Go
Raw Permalink Normal View History

// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package plans
import (
"fmt"
Handle marks a little more consistently In the very first implementation of "sensitive values" we were unfortunately not disciplined about separating the idea of "marked value" from the idea of "sensitive value" (where the latter is a subset of the former). The first implementation just assumed that any marking whatsoever meant "sensitive". We later improved that by adding the marks package and the marks.Sensitive value to standardize on the representation of "sensitive value" as being a value marked with _that specific mark_. However, we did not perform a thorough review of all of the mark-handling codepaths to make sure they all agreed on that definition. In particular, the state and plan models were both designed as if they supported arbitrary marks but then in practice marks other than marks.Sensitive would be handled in various inconsistent ways: dropped entirely, or interpreted as if marks.Sensitive, and possibly do so inconsistently when a value is used only in memory vs. round-tripped through a wire/file format. The goal of this commit is to resolve those oddities so that there are now two possible situations: - General mark handling: some codepaths genuinely handle marks generically, by transporting them from input value to output value in a way consistent with how cty itself deals with marks. This is the ideal case because it means we can add new marks in future and assume these codepaths will handle them correctly without any further modifications. - Sensitive-only mark preservation: the codepaths that interact with our wire protocols and file formats typically have only specialized support for sensitive values in particular, and lack support for any other marks. Those codepaths are now subject to a new rule where they must return an error if asked to deal with any other mark, so that if we introduce new marks in future we'll be forced either to define how we'll avoid those markings reaching the file/wire formats or extend the file/wire formats to support the new marks. Some new helper functions in package marks are intended to standardize how we deal with the "sensitive values only" situations, in the hope that this will make it easier to keep things consistent as the codebase evolves in future. In practice the modules runtime only ever uses marks.Sensitive as a mark today, so all of these checks are effectively covering "should never happen" cases. The only other mark Terraform uses is an implementation detail of "terraform console" and does not interact with any of the codepaths that only support sensitive values in particular.
2024-04-16 20:20:33 -04:00
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/genconfig"
Handle marks a little more consistently In the very first implementation of "sensitive values" we were unfortunately not disciplined about separating the idea of "marked value" from the idea of "sensitive value" (where the latter is a subset of the former). The first implementation just assumed that any marking whatsoever meant "sensitive". We later improved that by adding the marks package and the marks.Sensitive value to standardize on the representation of "sensitive value" as being a value marked with _that specific mark_. However, we did not perform a thorough review of all of the mark-handling codepaths to make sure they all agreed on that definition. In particular, the state and plan models were both designed as if they supported arbitrary marks but then in practice marks other than marks.Sensitive would be handled in various inconsistent ways: dropped entirely, or interpreted as if marks.Sensitive, and possibly do so inconsistently when a value is used only in memory vs. round-tripped through a wire/file format. The goal of this commit is to resolve those oddities so that there are now two possible situations: - General mark handling: some codepaths genuinely handle marks generically, by transporting them from input value to output value in a way consistent with how cty itself deals with marks. This is the ideal case because it means we can add new marks in future and assume these codepaths will handle them correctly without any further modifications. - Sensitive-only mark preservation: the codepaths that interact with our wire protocols and file formats typically have only specialized support for sensitive values in particular, and lack support for any other marks. Those codepaths are now subject to a new rule where they must return an error if asked to deal with any other mark, so that if we introduce new marks in future we'll be forced either to define how we'll avoid those markings reaching the file/wire formats or extend the file/wire formats to support the new marks. Some new helper functions in package marks are intended to standardize how we deal with the "sensitive values only" situations, in the hope that this will make it easier to keep things consistent as the codebase evolves in future. In practice the modules runtime only ever uses marks.Sensitive as a mark today, so all of these checks are effectively covering "should never happen" cases. The only other mark Terraform uses is an implementation detail of "terraform console" and does not interact with any of the codepaths that only support sensitive values in particular.
2024-04-16 20:20:33 -04:00
"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))
}
2025-03-04 10:33:43 -05:00
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
}
2024-08-12 14:28:26 -04:00
// 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
2025-08-27 16:26:28 -04:00
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
Handle marks a little more consistently In the very first implementation of "sensitive values" we were unfortunately not disciplined about separating the idea of "marked value" from the idea of "sensitive value" (where the latter is a subset of the former). The first implementation just assumed that any marking whatsoever meant "sensitive". We later improved that by adding the marks package and the marks.Sensitive value to standardize on the representation of "sensitive value" as being a value marked with _that specific mark_. However, we did not perform a thorough review of all of the mark-handling codepaths to make sure they all agreed on that definition. In particular, the state and plan models were both designed as if they supported arbitrary marks but then in practice marks other than marks.Sensitive would be handled in various inconsistent ways: dropped entirely, or interpreted as if marks.Sensitive, and possibly do so inconsistently when a value is used only in memory vs. round-tripped through a wire/file format. The goal of this commit is to resolve those oddities so that there are now two possible situations: - General mark handling: some codepaths genuinely handle marks generically, by transporting them from input value to output value in a way consistent with how cty itself deals with marks. This is the ideal case because it means we can add new marks in future and assume these codepaths will handle them correctly without any further modifications. - Sensitive-only mark preservation: the codepaths that interact with our wire protocols and file formats typically have only specialized support for sensitive values in particular, and lack support for any other marks. Those codepaths are now subject to a new rule where they must return an error if asked to deal with any other mark, so that if we introduce new marks in future we'll be forced either to define how we'll avoid those markings reaching the file/wire formats or extend the file/wire formats to support the new marks. Some new helper functions in package marks are intended to standardize how we deal with the "sensitive values only" situations, in the hope that this will make it easier to keep things consistent as the codebase evolves in future. In practice the modules runtime only ever uses marks.Sensitive as a mark today, so all of these checks are effectively covering "should never happen" cases. The only other mark Terraform uses is an implementation detail of "terraform console" and does not interact with any of the codepaths that only support sensitive values in particular.
2024-04-16 20:20:33 -04:00
// 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,
Handle marks a little more consistently In the very first implementation of "sensitive values" we were unfortunately not disciplined about separating the idea of "marked value" from the idea of "sensitive value" (where the latter is a subset of the former). The first implementation just assumed that any marking whatsoever meant "sensitive". We later improved that by adding the marks package and the marks.Sensitive value to standardize on the representation of "sensitive value" as being a value marked with _that specific mark_. However, we did not perform a thorough review of all of the mark-handling codepaths to make sure they all agreed on that definition. In particular, the state and plan models were both designed as if they supported arbitrary marks but then in practice marks other than marks.Sensitive would be handled in various inconsistent ways: dropped entirely, or interpreted as if marks.Sensitive, and possibly do so inconsistently when a value is used only in memory vs. round-tripped through a wire/file format. The goal of this commit is to resolve those oddities so that there are now two possible situations: - General mark handling: some codepaths genuinely handle marks generically, by transporting them from input value to output value in a way consistent with how cty itself deals with marks. This is the ideal case because it means we can add new marks in future and assume these codepaths will handle them correctly without any further modifications. - Sensitive-only mark preservation: the codepaths that interact with our wire protocols and file formats typically have only specialized support for sensitive values in particular, and lack support for any other marks. Those codepaths are now subject to a new rule where they must return an error if asked to deal with any other mark, so that if we introduce new marks in future we'll be forced either to define how we'll avoid those markings reaching the file/wire formats or extend the file/wire formats to support the new marks. Some new helper functions in package marks are intended to standardize how we deal with the "sensitive values only" situations, in the hope that this will make it easier to keep things consistent as the codebase evolves in future. In practice the modules runtime only ever uses marks.Sensitive as a mark today, so all of these checks are effectively covering "should never happen" cases. The only other mark Terraform uses is an implementation detail of "terraform console" and does not interact with any of the codepaths that only support sensitive values in particular.
2024-04-16 20:20:33 -04:00
Before: marks.MarkPaths(before, marks.Sensitive, cs.BeforeSensitivePaths),
BeforeIdentity: beforeIdentity,
Handle marks a little more consistently In the very first implementation of "sensitive values" we were unfortunately not disciplined about separating the idea of "marked value" from the idea of "sensitive value" (where the latter is a subset of the former). The first implementation just assumed that any marking whatsoever meant "sensitive". We later improved that by adding the marks package and the marks.Sensitive value to standardize on the representation of "sensitive value" as being a value marked with _that specific mark_. However, we did not perform a thorough review of all of the mark-handling codepaths to make sure they all agreed on that definition. In particular, the state and plan models were both designed as if they supported arbitrary marks but then in practice marks other than marks.Sensitive would be handled in various inconsistent ways: dropped entirely, or interpreted as if marks.Sensitive, and possibly do so inconsistently when a value is used only in memory vs. round-tripped through a wire/file format. The goal of this commit is to resolve those oddities so that there are now two possible situations: - General mark handling: some codepaths genuinely handle marks generically, by transporting them from input value to output value in a way consistent with how cty itself deals with marks. This is the ideal case because it means we can add new marks in future and assume these codepaths will handle them correctly without any further modifications. - Sensitive-only mark preservation: the codepaths that interact with our wire protocols and file formats typically have only specialized support for sensitive values in particular, and lack support for any other marks. Those codepaths are now subject to a new rule where they must return an error if asked to deal with any other mark, so that if we introduce new marks in future we'll be forced either to define how we'll avoid those markings reaching the file/wire formats or extend the file/wire formats to support the new marks. Some new helper functions in package marks are intended to standardize how we deal with the "sensitive values only" situations, in the hope that this will make it easier to keep things consistent as the codebase evolves in future. In practice the modules runtime only ever uses marks.Sensitive as a mark today, so all of these checks are effectively covering "should never happen" cases. The only other mark Terraform uses is an implementation detail of "terraform console" and does not interact with any of the codepaths that only support sensitive values in particular.
2024-04-16 20:20:33 -04:00
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
}
2025-08-15 10:08:16 -04:00
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)
2025-08-15 10:08:16 -04:00
var laterInvocations []*ActionInvocationInstanceSrc
for _, invocation := range actionInvocations {
if lat, ok := invocation.ActionTrigger.(*ResourceActionTrigger); ok {
if sameTriggerEvent(lat, needleLat) && triggersLater(lat, needleLat) {
2025-08-15 10:08:16 -04:00
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)
}