blob: d0a64953220081133f3c38265fea92f18b0485be [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// 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
}