mirror of https://github.com/hashicorp/terraform
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
372 lines
13 KiB
372 lines
13 KiB
// 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
|
|
}
|