| // 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(), |
| } |
| } |