blob: 5ada555be1f9d11038457df67bb7e8b470d3a6cf [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package stackstate
import (
"fmt"
"github.com/zclconf/go-cty/cty"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/collections"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackstate/statekeys"
"github.com/hashicorp/terraform/internal/stacks/stackutils"
"github.com/hashicorp/terraform/internal/stacks/tfstackdata1"
"github.com/hashicorp/terraform/internal/states"
)
// AppliedChange represents a single isolated change, emitted as
// part of a stream of applied changes during the ApplyStackChanges RPC API
// operation.
//
// Each AppliedChange becomes a single event in the RPC API, which itself
// has zero or more opaque raw plan messages that the caller must collect and
// provide verbatim during planning and zero or more "description" messages
// that are to give the caller realtime updates about the planning process.
type AppliedChange interface {
// AppliedChangeProto returns the protocol buffers representation of
// the change, ready to be sent verbatim to an RPC API client.
AppliedChangeProto() (*stacks.AppliedChange, error)
}
// AppliedChangeResourceInstanceObject announces the result of applying changes to
// a particular resource instance object.
type AppliedChangeResourceInstanceObject struct {
// ResourceInstanceObjectAddr is the absolute address of the resource
// instance object within the component instance that declared it.
//
// Typically a stream of applied changes with a resource instance object
// will also include a separate description of the component instance
// that the resource instance belongs to, but that isn't guaranteed in
// cases where problems occur during the apply phase and so consumers
// should tolerate seeing a resource instance for a component instance
// they don't know about yet, and should behave as if that component
// instance had been previously announced.
ResourceInstanceObjectAddr stackaddrs.AbsResourceInstanceObject
NewStateSrc *states.ResourceInstanceObjectSrc
ProviderConfigAddr addrs.AbsProviderConfig
// PreviousResourceInstanceObjectAddr is the absolute address of the
// resource instance object within the component instance if this object
// was moved from another address. This will be nil if the object was not
// moved.
PreviousResourceInstanceObjectAddr *stackaddrs.AbsResourceInstanceObject
// Schema MUST be the same schema that was used to encode the dynamic
// values inside NewStateSrc. This can be left as empty if NewStateSrc
// is nil, which represents that the object has been deleted.
Schema providers.Schema
}
var _ AppliedChange = (*AppliedChangeResourceInstanceObject)(nil)
// AppliedChangeProto implements AppliedChange.
func (ac *AppliedChangeResourceInstanceObject) AppliedChangeProto() (*stacks.AppliedChange, error) {
descs, raws, err := ac.protosForObject()
if err != nil {
return nil, fmt.Errorf("encoding %s: %w", ac.ResourceInstanceObjectAddr, err)
}
return &stacks.AppliedChange{
Raw: raws,
Descriptions: descs,
}, nil
}
func (ac *AppliedChangeResourceInstanceObject) protosForObject() ([]*stacks.AppliedChange_ChangeDescription, []*stacks.AppliedChange_RawChange, error) {
var descs []*stacks.AppliedChange_ChangeDescription
var raws []*stacks.AppliedChange_RawChange
var addr = ac.ResourceInstanceObjectAddr
var provider = ac.ProviderConfigAddr
var objSrc = ac.NewStateSrc
// For resource instance objects we use the same key format for both the
// raw and description representations, but callers MUST NOT rely on this.
objKey := statekeys.ResourceInstanceObject{
ResourceInstance: stackaddrs.AbsResourceInstance{
Component: addr.Component,
Item: addr.Item.ResourceInstance,
},
DeposedKey: addr.Item.DeposedKey,
}
objKeyRaw := statekeys.String(objKey)
if objSrc == nil {
// If the new object is nil then we'll emit a "deleted" description
// to ensure that any existing prior state value gets removed.
descs = append(descs, &stacks.AppliedChange_ChangeDescription{
Key: objKeyRaw,
Description: &stacks.AppliedChange_ChangeDescription_Deleted{
Deleted: &stacks.AppliedChange_Nothing{},
},
})
raws = append(raws, &stacks.AppliedChange_RawChange{
Key: objKeyRaw,
Value: nil, // unset Value field represents "delete" for raw changes
})
return descs, raws, nil
}
if ac.PreviousResourceInstanceObjectAddr != nil {
// If the object was moved, we need to emit a "deleted" description
// for the old address to ensure that any existing prior state value
// gets removed.
prevKey := statekeys.ResourceInstanceObject{
ResourceInstance: stackaddrs.AbsResourceInstance{
Component: ac.PreviousResourceInstanceObjectAddr.Component,
Item: ac.PreviousResourceInstanceObjectAddr.Item.ResourceInstance,
},
DeposedKey: ac.PreviousResourceInstanceObjectAddr.Item.DeposedKey,
}
prevKeyRaw := statekeys.String(prevKey)
descs = append(descs, &stacks.AppliedChange_ChangeDescription{
Key: prevKeyRaw,
Description: &stacks.AppliedChange_ChangeDescription_Moved{
Moved: &stacks.AppliedChange_Nothing{},
},
})
raws = append(raws, &stacks.AppliedChange_RawChange{
Key: prevKeyRaw,
Value: nil, // unset Value field represents "delete" for raw changes
})
// Don't return now - we'll still add the main change below.
}
// TRICKY: For historical reasons, a states.ResourceInstance
// contains pre-JSON-encoded dynamic data ready to be
// inserted verbatim into Terraform CLI's traditional
// JSON-based state file format. However, our RPC API
// exclusively uses MessagePack encoding for dynamic
// values, and so we will need to use the ac.Schema to
// transcode the data.
obj, err := objSrc.Decode(ac.Schema)
if err != nil {
// It would be _very_ strange to get here because we should just
// be reversing the same encoding operation done earlier to
// produce this object, using exactly the same schema.
return nil, nil, fmt.Errorf("cannot decode new state for %s in preparation for saving it: %w", addr, err)
}
protoValue, err := stacks.ToDynamicValue(obj.Value, ac.Schema.Body.ImpliedType())
if err != nil {
return nil, nil, fmt.Errorf("cannot encode new state for %s in preparation for saving it: %w", addr, err)
}
descs = append(descs, &stacks.AppliedChange_ChangeDescription{
Key: objKeyRaw,
Description: &stacks.AppliedChange_ChangeDescription_ResourceInstance{
ResourceInstance: &stacks.AppliedChange_ResourceInstance{
Addr: stacks.NewResourceInstanceObjectInStackAddr(addr),
NewValue: protoValue,
ResourceMode: stackutils.ResourceModeForProto(addr.Item.ResourceInstance.Resource.Resource.Mode),
ResourceType: addr.Item.ResourceInstance.Resource.Resource.Type,
ProviderAddr: provider.Provider.String(),
},
},
})
rawMsg := tfstackdata1.ResourceInstanceObjectStateToTFStackData1(objSrc, ac.ProviderConfigAddr)
var raw anypb.Any
err = anypb.MarshalFrom(&raw, rawMsg, proto.MarshalOptions{})
if err != nil {
return nil, nil, fmt.Errorf("encoding raw state object: %w", err)
}
raws = append(raws, &stacks.AppliedChange_RawChange{
Key: objKeyRaw,
Value: &raw,
})
return descs, raws, nil
}
// AppliedChangeComponentInstanceRemoved is the equivalent of
// AppliedChangeComponentInstance but it represents the component instance
// being removed from state instead of created or updated.
type AppliedChangeComponentInstanceRemoved struct {
ComponentAddr stackaddrs.AbsComponent
ComponentInstanceAddr stackaddrs.AbsComponentInstance
}
var _ AppliedChange = (*AppliedChangeComponentInstanceRemoved)(nil)
// AppliedChangeProto implements AppliedChange.
func (ac *AppliedChangeComponentInstanceRemoved) AppliedChangeProto() (*stacks.AppliedChange, error) {
stateKey := statekeys.String(statekeys.ComponentInstance{
ComponentInstanceAddr: ac.ComponentInstanceAddr,
})
return &stacks.AppliedChange{
Raw: []*stacks.AppliedChange_RawChange{
{
Key: stateKey,
Value: nil,
},
},
Descriptions: []*stacks.AppliedChange_ChangeDescription{
{
Key: stateKey,
Description: &stacks.AppliedChange_ChangeDescription_Deleted{
Deleted: &stacks.AppliedChange_Nothing{},
},
},
},
}, nil
}
// AppliedChangeComponentInstance announces the result of applying changes to
// an overall component instance.
//
// This deals with external-facing metadata about component instances, but
// does not directly track any resource instances inside. Those are tracked
// using individual [AppliedChangeResourceInstanceObject] objects for each.
type AppliedChangeComponentInstance struct {
ComponentAddr stackaddrs.AbsComponent
ComponentInstanceAddr stackaddrs.AbsComponentInstance
// Dependencies "remembers" the set of component instances that were
// required by the most recent apply of this component instance.
//
// This will be used by the stacks runtime to determine the order in
// which components should be destroyed when the original component block
// is no longer available.
Dependencies collections.Set[stackaddrs.AbsComponent]
// Dependents "remembers" the set of component instances that depended on
// this component instance at the most recent apply of this component
// instance.
//
// This will be used by the stacks runtime to determine the order in
// which components should be destroyed when the original component block
// is no longer available.
Dependents collections.Set[stackaddrs.AbsComponent]
// OutputValues "remembers" the output values from the most recent
// apply of the component instance. We store this primarily for external
// consumption, since the stacks runtime is able to recalculate the
// output values based on the prior state when needed, but we do have
// the option of using this internally in certain special cases where it
// would be too expensive to recalculate.
//
// If any output values are declared as sensitive then they should be
// marked as such here using the usual cty marking strategy.
OutputValues map[addrs.OutputValue]cty.Value
// InputVariables "remembers" the input values from the most recent
// apply of the component instance. We store this primarily for usage
// within the removed blocks in which the input values from the last
// applied state are required to destroy the existing resources.
InputVariables map[addrs.InputVariable]cty.Value
}
var _ AppliedChange = (*AppliedChangeComponentInstance)(nil)
// AppliedChangeProto implements AppliedChange.
func (ac *AppliedChangeComponentInstance) AppliedChangeProto() (*stacks.AppliedChange, error) {
stateKey := statekeys.String(statekeys.ComponentInstance{
ComponentInstanceAddr: ac.ComponentInstanceAddr,
})
outputDescs := make(map[string]*stacks.DynamicValue, len(ac.OutputValues))
for addr, val := range ac.OutputValues {
protoValue, err := stacks.ToDynamicValue(val, cty.DynamicPseudoType)
if err != nil {
return nil, fmt.Errorf("encoding new state for %s in %s in preparation for saving it: %w", addr, ac.ComponentInstanceAddr, err)
}
outputDescs[addr.Name] = protoValue
}
inputDescs := make(map[string]*stacks.DynamicValue, len(ac.InputVariables))
for addr, val := range ac.InputVariables {
protoValue, err := stacks.ToDynamicValue(val, cty.DynamicPseudoType)
if err != nil {
return nil, fmt.Errorf("encoding new state for %s in %s in preparation for saving it: %w", addr, ac.ComponentInstanceAddr, err)
}
inputDescs[addr.Name] = protoValue
}
var raw anypb.Any
if err := anypb.MarshalFrom(&raw, &tfstackdata1.StateComponentInstanceV1{
OutputValues: func() map[string]*tfstackdata1.DynamicValue {
outputs := make(map[string]*tfstackdata1.DynamicValue, len(outputDescs))
for name, value := range outputDescs {
outputs[name] = tfstackdata1.Terraform1ToStackDataDynamicValue(value)
}
return outputs
}(),
InputVariables: func() map[string]*tfstackdata1.DynamicValue {
inputs := make(map[string]*tfstackdata1.DynamicValue, len(inputDescs))
for name, value := range inputDescs {
inputs[name] = tfstackdata1.Terraform1ToStackDataDynamicValue(value)
}
return inputs
}(),
DependencyAddrs: func() []string {
var dependencies []string
for dependency := range ac.Dependencies.All() {
dependencies = append(dependencies, dependency.String())
}
return dependencies
}(),
DependentAddrs: func() []string {
var dependents []string
for dependent := range ac.Dependents.All() {
dependents = append(dependents, dependent.String())
}
return dependents
}(),
}, proto.MarshalOptions{}); err != nil {
return nil, fmt.Errorf("encoding raw state for %s: %w", ac.ComponentInstanceAddr, err)
}
return &stacks.AppliedChange{
Raw: []*stacks.AppliedChange_RawChange{
{
Key: stateKey,
Value: &raw,
},
},
Descriptions: []*stacks.AppliedChange_ChangeDescription{
{
Key: stateKey,
Description: &stacks.AppliedChange_ChangeDescription_ComponentInstance{
ComponentInstance: &stacks.AppliedChange_ComponentInstance{
ComponentAddr: ac.ComponentAddr.String(),
ComponentInstanceAddr: ac.ComponentInstanceAddr.String(),
OutputValues: outputDescs,
},
},
},
},
}, nil
}
type AppliedChangeInputVariable struct {
Addr stackaddrs.InputVariable
Value cty.Value
}
var _ AppliedChange = (*AppliedChangeInputVariable)(nil)
func (ac *AppliedChangeInputVariable) AppliedChangeProto() (*stacks.AppliedChange, error) {
key := statekeys.String(statekeys.Variable{
VariableAddr: ac.Addr,
})
if ac.Value == cty.NilVal {
// Then we're deleting this input variable from the state.
return &stacks.AppliedChange{
Raw: []*stacks.AppliedChange_RawChange{
{
Key: key,
Value: nil,
},
},
Descriptions: []*stacks.AppliedChange_ChangeDescription{
{
Key: key,
Description: &stacks.AppliedChange_ChangeDescription_Deleted{
Deleted: &stacks.AppliedChange_Nothing{},
},
},
},
}, nil
}
var raw anypb.Any
description := &stacks.AppliedChange_InputVariable{
Name: ac.Addr.Name,
}
value, err := stacks.ToDynamicValue(ac.Value, cty.DynamicPseudoType)
if err != nil {
return nil, fmt.Errorf("encoding new state for %s in preparation for saving it: %w", ac.Addr, err)
}
description.NewValue = value
if err := anypb.MarshalFrom(&raw, tfstackdata1.Terraform1ToStackDataDynamicValue(value), proto.MarshalOptions{}); err != nil {
return nil, fmt.Errorf("encoding raw state for %s: %w", ac.Addr, err)
}
return &stacks.AppliedChange{
Raw: []*stacks.AppliedChange_RawChange{
{
Key: key,
Value: &raw,
},
},
Descriptions: []*stacks.AppliedChange_ChangeDescription{
{
Key: key,
Description: &stacks.AppliedChange_ChangeDescription_InputVariable{
InputVariable: description,
},
},
},
}, nil
}
type AppliedChangeOutputValue struct {
Addr stackaddrs.OutputValue
Value cty.Value
}
var _ AppliedChange = (*AppliedChangeOutputValue)(nil)
func (ac *AppliedChangeOutputValue) AppliedChangeProto() (*stacks.AppliedChange, error) {
key := statekeys.String(statekeys.Output{
OutputAddr: ac.Addr,
})
if ac.Value == cty.NilVal {
// Then we're deleting this output value from the state.
return &stacks.AppliedChange{
Raw: []*stacks.AppliedChange_RawChange{
{
Key: key,
Value: nil,
},
},
Descriptions: []*stacks.AppliedChange_ChangeDescription{
{
Key: key,
Description: &stacks.AppliedChange_ChangeDescription_Deleted{
Deleted: &stacks.AppliedChange_Nothing{},
},
},
},
}, nil
}
value, err := stacks.ToDynamicValue(ac.Value, cty.DynamicPseudoType)
if err != nil {
return nil, fmt.Errorf("encoding new state for %s in preparation for saving it: %w", ac.Addr, err)
}
var raw anypb.Any
if err := anypb.MarshalFrom(&raw, tfstackdata1.Terraform1ToStackDataDynamicValue(value), proto.MarshalOptions{}); err != nil {
return nil, fmt.Errorf("encoding raw state for %s: %w", ac.Addr, err)
}
return &stacks.AppliedChange{
Raw: []*stacks.AppliedChange_RawChange{
{
Key: key,
Value: &raw,
},
},
Descriptions: []*stacks.AppliedChange_ChangeDescription{
{
Key: key,
Description: &stacks.AppliedChange_ChangeDescription_OutputValue{
OutputValue: &stacks.AppliedChange_OutputValue{
Name: ac.Addr.Name,
NewValue: value,
},
},
},
},
}, nil
}
type AppliedChangeDiscardKeys struct {
DiscardRawKeys collections.Set[statekeys.Key]
DiscardDescKeys collections.Set[statekeys.Key]
}
var _ AppliedChange = (*AppliedChangeDiscardKeys)(nil)
// AppliedChangeProto implements AppliedChange.
func (ac *AppliedChangeDiscardKeys) AppliedChangeProto() (*stacks.AppliedChange, error) {
ret := &stacks.AppliedChange{
Raw: make([]*stacks.AppliedChange_RawChange, 0, ac.DiscardRawKeys.Len()),
Descriptions: make([]*stacks.AppliedChange_ChangeDescription, 0, ac.DiscardDescKeys.Len()),
}
for key := range ac.DiscardRawKeys.All() {
ret.Raw = append(ret.Raw, &stacks.AppliedChange_RawChange{
Key: statekeys.String(key),
Value: nil, // nil represents deletion
})
}
for key := range ac.DiscardDescKeys.All() {
ret.Descriptions = append(ret.Descriptions, &stacks.AppliedChange_ChangeDescription{
Key: statekeys.String(key),
Description: &stacks.AppliedChange_ChangeDescription_Deleted{
// Selection of this empty variant represents deletion
},
})
}
return ret, nil
}