blob: 5207ec27079d67a82fafab4058f0eaa63affc207 [file]
// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package stacks
import (
"fmt"
"math/big"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// This file contains some hand-written type conversion helpers to complement
// the generated stubs.
// ChangeTypesForPlanAction returns the [ChangeType] sequence that corresponds
// to the given plan action, or an error if there is no known equivalent.
func ChangeTypesForPlanAction(action plans.Action) ([]ChangeType, error) {
switch action {
case plans.NoOp:
return []ChangeType{ChangeType_NOOP}, nil
case plans.Create:
return []ChangeType{ChangeType_CREATE}, nil
case plans.Read:
return []ChangeType{ChangeType_READ}, nil
case plans.Update:
return []ChangeType{ChangeType_UPDATE}, nil
case plans.Delete:
return []ChangeType{ChangeType_DELETE}, nil
case plans.DeleteThenCreate:
return []ChangeType{ChangeType_DELETE, ChangeType_CREATE}, nil
case plans.CreateThenDelete:
return []ChangeType{ChangeType_CREATE, ChangeType_DELETE}, nil
case plans.Forget:
return []ChangeType{ChangeType_FORGET}, nil
case plans.ForgetThenCreate:
return []ChangeType{ChangeType_FORGET, ChangeType_CREATE}, nil
case plans.CreateThenForget:
return []ChangeType{ChangeType_CREATE, ChangeType_FORGET}, nil
default:
return nil, fmt.Errorf("unsupported action %s", action)
}
}
// ActionTriggerEventForStackChangeProgress converts a [configs.ActionTriggerEvent]
// to its [StackChangeProgress_ActionTriggerEvent] protobuf equivalent.
func ActionTriggerEventForStackChangeProgress(event configs.ActionTriggerEvent) (StackChangeProgress_ActionTriggerEvent, error) {
switch event {
case configs.BeforeCreate:
return StackChangeProgress_BEFORE_CREATE, nil
case configs.AfterCreate:
return StackChangeProgress_AFTER_CREATE, nil
case configs.BeforeUpdate:
return StackChangeProgress_BEFORE_UPDATE, nil
case configs.AfterUpdate:
return StackChangeProgress_AFTER_UPDATE, nil
case configs.BeforeDestroy:
return StackChangeProgress_BEFORE_DESTROY, nil
case configs.AfterDestroy:
return StackChangeProgress_AFTER_DESTROY, nil
case configs.Invoke:
return StackChangeProgress_INVOKE, nil
default:
return StackChangeProgress_INVALID_EVENT, fmt.Errorf("unsupported trigger event %s", event)
}
}
// ActionTriggerEventForPlannedChange converts a [configs.ActionTriggerEvent]
// to its [PlannedChange_ActionTriggerEvent] protobuf equivalent.
func ActionTriggerEventForPlannedChange(event configs.ActionTriggerEvent) (PlannedChange_ActionTriggerEvent, error) {
switch event {
case configs.BeforeCreate:
return PlannedChange_BEFORE_CREATE, nil
case configs.AfterCreate:
return PlannedChange_AFTER_CREATE, nil
case configs.BeforeUpdate:
return PlannedChange_BEFORE_UPDATE, nil
case configs.AfterUpdate:
return PlannedChange_AFTER_UPDATE, nil
case configs.BeforeDestroy:
return PlannedChange_BEFORE_DESTROY, nil
case configs.AfterDestroy:
return PlannedChange_AFTER_DESTROY, nil
case configs.Invoke:
return PlannedChange_INVOKE, nil
default:
return PlannedChange_INVALID_EVENT, fmt.Errorf("unsupported trigger event %s", event)
}
}
// ToDynamicValue uses NewDynamicValue to construct a DynamicValue from the
// provider cty.Value. This function will strip out the sensitive paths and
// include them in the returned dynamic value. If from contains marks other
// than sensitive then this function will return an error.
func ToDynamicValue(from cty.Value, ty cty.Type) (*DynamicValue, error) {
// Separate out sensitive marks from the decoded value so we can re-serialize it
// with MessagePack. Sensitive paths get encoded separately in the final message.
unmarkedValue, markses := from.UnmarkDeepWithPaths()
sensitivePaths, otherMarkses := marks.PathsWithMark(markses, marks.Sensitive)
if len(otherMarkses) != 0 {
// Any other marks should've been dealt with by our caller before
// getting here, since we only know how to preserve the sensitive
// marking.
return nil, fmt.Errorf(
"%s: unhandled value marks %#v (this is a bug in Terraform)",
tfdiags.FormatCtyPath(otherMarkses[0].Path), otherMarkses[0].Marks,
)
}
encValue, err := plans.NewDynamicValue(unmarkedValue, ty)
if err != nil {
return nil, err
}
return NewDynamicValue(encValue, sensitivePaths), nil
}
// NewDynamicValue constructs a [DynamicValue] message object from a
// [plans.DynamicValue], which is Terraform Core's typical in-memory
// representation of an already-serialized dynamic value.
//
// The plans package represents the sensitive value mark as a separate field
// in [plans.ChangeSrc] rather than as part of the value itself, so callers must
// also provide a separate set of paths that are marked as sensitive.
func NewDynamicValue(from plans.DynamicValue, sensitivePaths []cty.Path) *DynamicValue {
// plans.DynamicValue is always MessagePack-serialized today, so we'll
// just write its bytes into the field for msgpack serialization
// unconditionally. If plans.DynamicValue grows to support different
// serialization formats in future we will need some additional logic here.
ret := &DynamicValue{
Msgpack: []byte(from),
}
if len(sensitivePaths) != 0 {
ret.Sensitive = make([]*AttributePath, 0, len(sensitivePaths))
for _, path := range sensitivePaths {
ret.Sensitive = append(ret.Sensitive, NewAttributePath(path))
}
}
return ret
}
// NewAttributePath constructs an [AttributePath] message object from
// a [cty.Path] value.
func NewAttributePath(from cty.Path) *AttributePath {
ret := &AttributePath{}
if len(from) == 0 {
return ret
}
ret.Steps = make([]*AttributePath_Step, len(from))
for i, step := range from {
switch step := step.(type) {
case cty.GetAttrStep:
ret.Steps[i] = &AttributePath_Step{
Selector: &AttributePath_Step_AttributeName{
AttributeName: step.Name,
},
}
case cty.IndexStep:
k := step.Key
// Although the key is cty.Value, in practice it should typically
// be constrained only to known and non-null strings and numbers.
// If we encounter anything else then we'll just abort and return
// a truncated path, since the only way other values should be
// able to appear is if we're traversing through a set, and we
// typically avoid doing that in callers by truncating the path
// at the same point anyway. (Note that marked values -- one of
// our main uses for AttributePath -- cannot exist inside
// sets anyway, so that case can't arise there.)
if k.IsNull() || !k.IsKnown() {
k = cty.DynamicVal // to force falling into the default case for the switch below
}
switch k.Type() {
case cty.String:
ret.Steps[i] = &AttributePath_Step{
Selector: &AttributePath_Step_ElementKeyString{
ElementKeyString: k.AsString(),
},
}
case cty.Number:
// We require an integer in int64 range. We might not get that
// in the unlikely event that this is a traversal through a
// cty.Set(cty.Number), since any number would be valid in
// principle for that case.
bf := k.AsBigFloat()
idx, acc := bf.Int64()
if acc != big.Exact {
ret.Steps = ret.Steps[:i]
return ret
}
ret.Steps[i] = &AttributePath_Step{
Selector: &AttributePath_Step_ElementKeyInt{
ElementKeyInt: idx,
},
}
default:
ret.Steps = ret.Steps[:i]
return ret
}
default:
// Should not get here because the above should be exhaustive for
// all cty.PathStep implementations.
panic(fmt.Sprintf("path has unsupported step type %T", step))
}
}
return ret
}
func NewResourceInstanceInStackAddr(addr stackaddrs.AbsResourceInstance) *ResourceInstanceInStackAddr {
return &ResourceInstanceInStackAddr{
ComponentInstanceAddr: addr.Component.String(),
ResourceInstanceAddr: addr.Item.String(),
}
}
func NewResourceInstanceObjectInStackAddr(addr stackaddrs.AbsResourceInstanceObject) *ResourceInstanceObjectInStackAddr {
return &ResourceInstanceObjectInStackAddr{
ComponentInstanceAddr: addr.Component.String(),
ResourceInstanceAddr: addr.Item.ResourceInstance.String(),
DeposedKey: addr.Item.DeposedKey.String(),
}
}
func NewActionInvocationInStackAddr(addr stackaddrs.AbsActionInvocationInstance) *ActionInvocationInstanceInStackAddr {
return &ActionInvocationInstanceInStackAddr{
ComponentInstanceAddr: addr.Component.String(),
ActionInvocationInstanceAddr: addr.Item.String(),
}
}