// 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 }