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