| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package stackstate | |
| import ( | |
| "fmt" | |
| "log" | |
| "sync" | |
| "github.com/zclconf/go-cty/cty" | |
| "google.golang.org/protobuf/proto" | |
| "google.golang.org/protobuf/reflect/protoreflect" | |
| "google.golang.org/protobuf/types/known/anypb" | |
| "google.golang.org/protobuf/types/known/emptypb" | |
| "github.com/hashicorp/terraform/internal/addrs" | |
| "github.com/hashicorp/terraform/internal/stacks/stackaddrs" | |
| "github.com/hashicorp/terraform/internal/stacks/stackstate/statekeys" | |
| "github.com/hashicorp/terraform/internal/stacks/tfstackdata1" | |
| ) | |
| // A helper for loading prior state snapshots in a streaming manner. | |
| type Loader struct { | |
| ret *State | |
| mu sync.Mutex | |
| } | |
| // Constructs a new [Loader], with an initial empty state. | |
| func NewLoader() *Loader { | |
| ret := NewState() | |
| ret.inputRaw = make(map[string]*anypb.Any) | |
| return &Loader{ | |
| ret: ret, | |
| } | |
| } | |
| // AddRaw adds a single raw state object to the state being loaded. | |
| func (l *Loader) AddRaw(rawKey string, rawMsg *anypb.Any) error { | |
| l.mu.Lock() | |
| defer l.mu.Unlock() | |
| if l.ret == nil { | |
| return fmt.Errorf("loader has been consumed") | |
| } | |
| if _, exists := l.ret.inputRaw[rawKey]; exists { | |
| // This suggests a client bug because the recipient of state events | |
| // from ApplyStackChanges is supposed to keep only the latest | |
| // object associated with each distinct key. | |
| return fmt.Errorf("duplicate raw state object key %q", rawKey) | |
| } | |
| l.ret.inputRaw[rawKey] = rawMsg | |
| key, err := statekeys.Parse(rawKey) | |
| if err != nil { | |
| // "invalid" here means that it was either not syntactically | |
| // valid at all or was a recognized type but with the wrong | |
| // syntax for that type. | |
| // An unrecognized key type is NOT invalid; we handle that below. | |
| return fmt.Errorf("invalid tracking key %q in state: %w", rawKey, err) | |
| } | |
| if !statekeys.RecognizedType(key) { | |
| err = handleUnrecognizedKey(key, l.ret) | |
| if err != nil { | |
| return err | |
| } | |
| return nil | |
| } | |
| if rawMsg == nil { | |
| // This suggests a state mutation bug where a deleted object was | |
| // written as a map entry without a value, as opposed to deleting | |
| // the value. We tolerate this here just because otherwise it | |
| // would be harder to recover once a state has been mutated | |
| // incorrectly. | |
| log.Panicf("[WARN] stackstate.Loader: key %s has no associated object; ignoring", rawKey) | |
| return nil | |
| } | |
| msg, err := anypb.UnmarshalNew(rawMsg, proto.UnmarshalOptions{}) | |
| if err != nil { | |
| return fmt.Errorf("invalid raw value for raw state key %q: %w", rawKey, err) | |
| } | |
| err = handleProtoMsg(key, msg, l.ret) | |
| if err != nil { | |
| return err | |
| } | |
| return nil | |
| } | |
| // AddDirectProto is like AddRaw but accepts direct messages of the relevant types | |
| // from the tfstackdata1 package, rather than the [anypb.Raw] representation | |
| // thereof. | |
| // | |
| // This is primarily for internal testing purposes, where it's typically more | |
| // convenient to write out a struct literal for one of the message types | |
| // directly rather than having to first serialize it to [anypb.Any] only for | |
| // it to be unserialized again promptly afterwards. | |
| // | |
| // Unlike [Loader.AddRaw], the object added by this function will not have | |
| // a raw representation recorded in the "raw state" of the final result, | |
| // because this function is bypassing the concept of raw state. [State.InputRaw] | |
| // will therefore return a map where the given key is associated with a nil | |
| // message. | |
| // | |
| // Prefer to use [Loader.AddRaw] when processing user input. This function | |
| // cannot accept [anypb.Any] messages even though the Go compiler can't | |
| // check that at compile time. | |
| func (l *Loader) AddDirectProto(keyStr string, msg protoreflect.ProtoMessage) error { | |
| l.mu.Lock() | |
| defer l.mu.Unlock() | |
| if l.ret == nil { | |
| return fmt.Errorf("loader has been consumed") | |
| } | |
| if _, exists := l.ret.inputRaw[keyStr]; exists { | |
| // This suggests a client bug because the recipient of state events | |
| // from ApplyStackChanges is supposed to keep only the latest | |
| // object associated with each distinct key. | |
| return fmt.Errorf("duplicate raw state object key %q", keyStr) | |
| } | |
| l.ret.inputRaw[keyStr] = nil // this weird entrypoint does not provide raw state | |
| // The following should be equivalent to the similar logic in | |
| // [LoadFromProto] except for skipping the parsing/unmarshalling | |
| // steps since msg is already in its in-memory form. | |
| key, err := statekeys.Parse(keyStr) | |
| if err != nil { | |
| return fmt.Errorf("invalid tracking key %q: %w", keyStr, err) | |
| } | |
| if !statekeys.RecognizedType(key) { | |
| err := handleUnrecognizedKey(key, l.ret) | |
| if err != nil { | |
| return err | |
| } | |
| return nil | |
| } | |
| err = handleProtoMsg(key, msg, l.ret) | |
| if err != nil { | |
| return err | |
| } | |
| return nil | |
| } | |
| // State consumes the loaded state, making the associated loader closed to | |
| // further additions. | |
| func (l *Loader) State() *State { | |
| l.mu.Lock() | |
| defer l.mu.Unlock() | |
| ret := l.ret | |
| l.ret = nil | |
| return ret | |
| } | |
| // LoadFromProto produces a [State] object by decoding a raw state map. | |
| // | |
| // This is a helper wrapper around [Loader.AddRaw] for when the state was already | |
| // loaded into a single map. | |
| func LoadFromProto(msgs map[string]*anypb.Any) (*State, error) { | |
| loader := NewLoader() | |
| for rawKey, rawMsg := range msgs { | |
| err := loader.AddRaw(rawKey, rawMsg) | |
| if err != nil { | |
| return nil, err | |
| } | |
| } | |
| return loader.State(), nil | |
| } | |
| // LoadFromDirectProto is a variation of the primary entry-point [LoadFromProto] | |
| // which accepts direct messages of the relevant types from the tfstackdata1 | |
| // package, rather than the [anypb.Raw] representation thereof. | |
| // | |
| // This is a helper wrapper around [Loader.AddDirectProto] for when the state | |
| // was already built into a single map. | |
| func LoadFromDirectProto(msgs map[string]protoreflect.ProtoMessage) (*State, error) { | |
| loader := NewLoader() | |
| for rawKey, rawMsg := range msgs { | |
| err := loader.AddDirectProto(rawKey, rawMsg) | |
| if err != nil { | |
| return nil, err | |
| } | |
| } | |
| return loader.State(), nil | |
| } | |
| func handleUnrecognizedKey(key statekeys.Key, state *State) error { | |
| // There are three different strategies for dealing with | |
| // unrecognized keys, which we recognize based on naming | |
| // conventions of the key types. | |
| switch handling := key.KeyType().UnrecognizedKeyHandling(); handling { | |
| case statekeys.FailIfUnrecognized: | |
| // This is for keys whose messages materially change the | |
| // meaning of the state and so cannot be ignored. Keys | |
| // with this treatment are forwards-incompatible (old versions | |
| // of Terraform will fail to load a state containing them) so | |
| // should be added only as a last resort. | |
| return fmt.Errorf("state was created by a newer version of Terraform Core (unrecognized tracking key %q)", statekeys.String(key)) | |
| case statekeys.PreserveIfUnrecognized: | |
| // This is for keys whose messages can safely be left entirely | |
| // unchanged if applying a plan with a version of Terraform | |
| // that doesn't understand them. Keys in this category should | |
| // typically be standalone and not refer to or depend on any | |
| // other objects in the state, to ensure that removing or | |
| // updating other objects will not cause the preserved message | |
| // to become misleading or invalid. | |
| // We don't need to do anything special with these ones because | |
| // the caller should preserve any object we don't explicitly | |
| // update or delete during the apply phase. | |
| return nil | |
| case statekeys.DiscardIfUnrecognized: | |
| // This is for keys which can be discarded when planning or | |
| // applying with an older version of Terraform that doesn't | |
| // understand them. This category is for optional ancillary | |
| // information -- not actually required for correct subsequent | |
| // planning -- especially if it could be recomputed again and | |
| // repopulated if later planning and applying with a newer | |
| // version of Terraform Core. | |
| // For these ones we need to remember their keys so that we | |
| // can emit "delete" messages early in the apply phase to | |
| // actually discard them from the caller's records. | |
| state.discardUnsupportedKeys.Add(key) | |
| return nil | |
| default: | |
| // Should not get here. The above should be exhaustive. | |
| panic(fmt.Sprintf("unsupported UnrecognizedKeyHandling value %s", handling)) | |
| } | |
| } | |
| func handleProtoMsg(key statekeys.Key, msg protoreflect.ProtoMessage, state *State) error { | |
| switch key := key.(type) { | |
| case statekeys.ComponentInstance: | |
| return handleComponentInstanceMsg(key, msg, state) | |
| case statekeys.ResourceInstanceObject: | |
| return handleResourceInstanceObjectMsg(key, msg, state) | |
| case statekeys.Output: | |
| return handleOutputMsg(key, msg, state) | |
| case statekeys.Variable: | |
| return handleVariableMsg(key, msg, state) | |
| default: | |
| // Should not get here: the above should be exhaustive for all | |
| // possible key types. | |
| panic(fmt.Sprintf("unsupported state key type %T", key)) | |
| } | |
| } | |
| func handleVariableMsg(key statekeys.Variable, msg protoreflect.ProtoMessage, state *State) error { | |
| switch msg := msg.(type) { | |
| case *emptypb.Empty: | |
| // for backwards compatibility reasons, ephemeral values used to be | |
| // stored in state as empty messages. We'll upgrade these to null | |
| // values with ephemeral marks. | |
| state.addInputVariable(key.VariableAddr, cty.NullVal(cty.DynamicPseudoType)) | |
| return nil | |
| case *tfstackdata1.DynamicValue: | |
| value, err := tfstackdata1.DynamicValueFromTFStackData1(msg, cty.DynamicPseudoType) | |
| if err != nil { | |
| return fmt.Errorf("failed to decode %s: %w", key.VariableAddr, err) | |
| } | |
| state.addInputVariable(key.VariableAddr, value) | |
| return nil | |
| default: | |
| return fmt.Errorf("unsupported message type %T for %s state", msg, key.VariableAddr) | |
| } | |
| } | |
| func handleOutputMsg(key statekeys.Output, msg protoreflect.ProtoMessage, state *State) error { | |
| outputState, ok := msg.(*tfstackdata1.DynamicValue) | |
| if !ok { | |
| return fmt.Errorf("unsupported message type %T for %s state", msg, key.OutputAddr) | |
| } | |
| value, err := tfstackdata1.DynamicValueFromTFStackData1(outputState, cty.DynamicPseudoType) | |
| if err != nil { | |
| return fmt.Errorf("failed to decode %s: %w", key.OutputAddr, err) | |
| } | |
| state.addOutputValue(key.OutputAddr, value) | |
| return nil | |
| } | |
| func handleComponentInstanceMsg(key statekeys.ComponentInstance, msg protoreflect.ProtoMessage, state *State) error { | |
| // For this particular object type all of the information is in the key, | |
| // for now at least. | |
| componentState, ok := msg.(*tfstackdata1.StateComponentInstanceV1) | |
| if !ok { | |
| return fmt.Errorf("unsupported message type %T for %s state", msg, key.ComponentInstanceAddr) | |
| } | |
| instance := state.ensureComponentInstanceState(key.ComponentInstanceAddr) | |
| for _, addr := range componentState.DependencyAddrs { | |
| stackaddr, diags := stackaddrs.ParseAbsComponentInstanceStr(addr) | |
| if diags.HasErrors() { | |
| return fmt.Errorf("invalid required component address %q for %s", addr, key.ComponentInstanceAddr) | |
| } | |
| instance.dependencies.Add(stackaddrs.AbsComponent{ | |
| Stack: stackaddr.Stack, | |
| Item: stackaddr.Item.Component, | |
| }) | |
| } | |
| for _, addr := range componentState.DependentAddrs { | |
| stackaddr, diags := stackaddrs.ParseAbsComponentInstanceStr(addr) | |
| if diags.HasErrors() { | |
| return fmt.Errorf("invalid required component address %q for %s", addr, key.ComponentInstanceAddr) | |
| } | |
| instance.dependents.Add(stackaddrs.AbsComponent{ | |
| Stack: stackaddr.Stack, | |
| Item: stackaddr.Item.Component, | |
| }) | |
| } | |
| for name, output := range componentState.OutputValues { | |
| value, err := tfstackdata1.DynamicValueFromTFStackData1(output, cty.DynamicPseudoType) | |
| if err != nil { | |
| return fmt.Errorf("decoding output value %q for %s: %w", name, key.ComponentInstanceAddr, err) | |
| } | |
| instance.outputValues[addrs.OutputValue{Name: name}] = value | |
| } | |
| for name, input := range componentState.InputVariables { | |
| value, err := tfstackdata1.DynamicValueFromTFStackData1(input, cty.DynamicPseudoType) | |
| if err != nil { | |
| return fmt.Errorf("decoding input value %q for %s: %w", name, key.ComponentInstanceAddr, err) | |
| } | |
| instance.inputVariables[addrs.InputVariable{Name: name}] = value | |
| } | |
| return nil | |
| } | |
| func handleResourceInstanceObjectMsg(key statekeys.ResourceInstanceObject, msg protoreflect.ProtoMessage, state *State) error { | |
| fullAddr := stackaddrs.AbsResourceInstanceObject{ | |
| Component: key.ResourceInstance.Component, | |
| Item: addrs.AbsResourceInstanceObject{ | |
| ResourceInstance: key.ResourceInstance.Item, | |
| DeposedKey: key.DeposedKey, | |
| }, | |
| } | |
| riMsg, ok := msg.(*tfstackdata1.StateResourceInstanceObjectV1) | |
| if !ok { | |
| return fmt.Errorf("unsupported message type %T for state of %s", msg, fullAddr.String()) | |
| } | |
| objSrc, err := tfstackdata1.DecodeProtoResourceInstanceObject(riMsg) | |
| if err != nil { | |
| return fmt.Errorf("invalid stored state object for %s: %w", fullAddr, err) | |
| } | |
| providerConfigAddr, diags := addrs.ParseAbsProviderConfigStr(riMsg.ProviderConfigAddr) | |
| if diags.HasErrors() { | |
| return fmt.Errorf("provider configuration reference %q for %s", riMsg.ProviderConfigAddr, fullAddr) | |
| } | |
| state.addResourceInstanceObject(fullAddr, objSrc, providerConfigAddr) | |
| return nil | |
| } |