blob: a719da5d8c8ea6cdd7d6c775eca5569e333b2e36 [file] [log] [blame] [edit]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package stackplan
import (
"fmt"
"sync"
"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/lang/marks"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/plans/planfile"
"github.com/hashicorp/terraform/internal/plans/planproto"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/tfstackdata1"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/version"
)
// A helper for loading saved plans in a streaming manner.
type Loader struct {
ret *Plan
foundHeader bool
mu sync.Mutex
}
// Constructs a new [Loader], with an initial empty plan.
func NewLoader() *Loader {
ret := &Plan{
RootInputValues: make(map[stackaddrs.InputVariable]cty.Value),
ApplyTimeInputVariables: collections.NewSetCmp[stackaddrs.InputVariable](),
DeletedInputVariables: collections.NewSet[stackaddrs.InputVariable](),
DeletedOutputValues: collections.NewSet[stackaddrs.OutputValue](),
Components: collections.NewMap[stackaddrs.AbsComponentInstance, *Component](),
DeletedComponents: collections.NewSet[stackaddrs.AbsComponentInstance](),
PrevRunStateRaw: make(map[string]*anypb.Any),
}
return &Loader{
ret: ret,
}
}
// AddRaw adds a single raw change object to the plan being loaded.
func (l *Loader) AddRaw(rawMsg *anypb.Any) error {
l.mu.Lock()
defer l.mu.Unlock()
if l.ret == nil {
return fmt.Errorf("loader has been consumed")
}
msg, err := anypb.UnmarshalNew(rawMsg, proto.UnmarshalOptions{
// Just the default unmarshalling options
})
if err != nil {
return fmt.Errorf("invalid raw message: %w", err)
}
// The references to specific message types below ensure that
// the protobuf descriptors for these types are included in the
// compiled program, and thus available in the global protobuf
// registry that anypb.UnmarshalNew relies on above.
switch msg := msg.(type) {
case *tfstackdata1.PlanHeader:
wantVersion := version.SemVer.String()
gotVersion := msg.TerraformVersion
if gotVersion != wantVersion {
return fmt.Errorf("plan was created by Terraform %s, but this is Terraform %s", gotVersion, wantVersion)
}
l.foundHeader = true
case *tfstackdata1.PlanPriorStateElem:
if _, exists := l.ret.PrevRunStateRaw[msg.Key]; exists {
// Suggests a bug in the caller, because a valid prior state
// can only have one object associated with each key.
return fmt.Errorf("duplicate prior state key %q", msg.Key)
}
// NOTE: We intentionally don't actually decode and validate the
// state elements here; we'll deal with that piecemeal as we make
// further use of this data structure elsewhere. This avoids spending
// time on decoding here if a caller is loading the plan only to
// extract some metadata from it, and doesn't care about the prior
// state.
l.ret.PrevRunStateRaw[msg.Key] = msg.Raw
case *tfstackdata1.PlanApplyable:
l.ret.Applyable = msg.Applyable
case *tfstackdata1.PlanTimestamp:
err = l.ret.PlanTimestamp.UnmarshalText([]byte(msg.PlanTimestamp))
if err != nil {
return fmt.Errorf("invalid plan timestamp %q", msg.PlanTimestamp)
}
case *tfstackdata1.DeletedRootOutputValue:
l.ret.DeletedOutputValues.Add(stackaddrs.OutputValue{Name: msg.Name})
case *tfstackdata1.DeletedRootInputVariable:
l.ret.DeletedInputVariables.Add(stackaddrs.InputVariable{Name: msg.Name})
case *tfstackdata1.DeletedComponent:
addr, diags := stackaddrs.ParseAbsComponentInstanceStr(msg.ComponentInstanceAddr)
if diags.HasErrors() {
// Should not get here because the address we're parsing
// should've been produced by this same version of Terraform.
return fmt.Errorf("invalid component instance address syntax in %q", msg.ComponentInstanceAddr)
}
l.ret.DeletedComponents.Add(addr)
case *tfstackdata1.PlanRootInputValue:
addr := stackaddrs.InputVariable{
Name: msg.Name,
}
val, err := tfstackdata1.DynamicValueFromTFStackData1(msg.Value, cty.DynamicPseudoType)
if err != nil {
return fmt.Errorf("invalid stored value for %s: %w", addr, err)
}
l.ret.RootInputValues[addr] = val
if msg.RequiredOnApply {
if !val.IsNull() {
// A variable can't be both persisted _and_ required on apply.
return fmt.Errorf("plan has value for required-on-apply input variable %s", addr)
}
l.ret.ApplyTimeInputVariables.Add(addr)
}
case *tfstackdata1.ProviderFunctionResults:
for _, hash := range msg.ProviderFunctionResults {
l.ret.ProviderFunctionResults = append(l.ret.ProviderFunctionResults, providers.FunctionHash{
Key: hash.Key,
Result: hash.Result,
})
}
case *tfstackdata1.PlanComponentInstance:
addr, diags := stackaddrs.ParsePartialComponentInstanceStr(msg.ComponentInstanceAddr)
if diags.HasErrors() {
// Should not get here because the address we're parsing
// should've been produced by this same version of Terraform.
return fmt.Errorf("invalid component instance address syntax in %q", msg.ComponentInstanceAddr)
}
dependencies := collections.NewSet[stackaddrs.AbsComponent]()
for _, rawAddr := range msg.DependsOnComponentAddrs {
// NOTE: We're using the component _instance_ address parser
// here, but we really want just components, so we'll need to
// check afterwards to make sure we don't have an instance key.
addr, diags := stackaddrs.ParseAbsComponentInstanceStr(rawAddr)
if diags.HasErrors() {
return fmt.Errorf("invalid component address syntax in %q", rawAddr)
}
if addr.Item.Key != addrs.NoKey {
return fmt.Errorf("invalid component address syntax in %q: is actually a component instance address", rawAddr)
}
realAddr := stackaddrs.AbsComponent{
Stack: addr.Stack,
Item: addr.Item.Component,
}
dependencies.Add(realAddr)
}
plannedAction, err := planproto.FromAction(msg.PlannedAction)
if err != nil {
return fmt.Errorf("decoding plan for %s: %w", addr, err)
}
mode, err := planproto.FromMode(msg.Mode)
if err != nil {
return fmt.Errorf("decoding mode for %s: %w", addr, err)
}
inputVals := make(map[addrs.InputVariable]plans.DynamicValue)
inputValMarks := make(map[addrs.InputVariable][]cty.PathValueMarks)
for name, rawVal := range msg.PlannedInputValues {
val := addrs.InputVariable{
Name: name,
}
inputVals[val] = rawVal.Value.Msgpack
inputValMarks[val] = make([]cty.PathValueMarks, len(rawVal.SensitivePaths))
for _, path := range rawVal.SensitivePaths {
path, err := planfile.PathFromProto(path)
if err != nil {
return fmt.Errorf("decoding sensitive path %q for %s: %w", val, addr, err)
}
inputValMarks[val] = append(inputValMarks[val], cty.PathValueMarks{
Path: path,
Marks: cty.NewValueMarks(marks.Sensitive),
})
}
}
outputVals := make(map[addrs.OutputValue]cty.Value)
for name, rawVal := range msg.PlannedOutputValues {
v, err := tfstackdata1.DynamicValueFromTFStackData1(rawVal, cty.DynamicPseudoType)
if err != nil {
return fmt.Errorf("decoding output value %q for %s: %w", name, addr, err)
}
outputVals[addrs.OutputValue{Name: name}] = v
}
checkResults, err := planfile.CheckResultsFromPlanProto(msg.PlannedCheckResults)
if err != nil {
return fmt.Errorf("decoding check results: %w", err)
}
var functionResults []providers.FunctionHash
for _, hash := range msg.ProviderFunctionResults {
functionResults = append(functionResults, providers.FunctionHash{
Key: hash.Key,
Result: hash.Result,
})
}
if !l.ret.Components.HasKey(addr) {
l.ret.Components.Put(addr, &Component{
PlannedAction: plannedAction,
Mode: mode,
PlanApplyable: msg.PlanApplyable,
PlanComplete: msg.PlanComplete,
Dependencies: dependencies,
Dependents: collections.NewSet[stackaddrs.AbsComponent](),
PlannedInputValues: inputVals,
PlannedInputValueMarks: inputValMarks,
PlannedOutputValues: outputVals,
PlannedChecks: checkResults,
PlannedFunctionResults: functionResults,
ResourceInstancePlanned: addrs.MakeMap[addrs.AbsResourceInstanceObject, *plans.ResourceInstanceChangeSrc](),
ResourceInstancePriorState: addrs.MakeMap[addrs.AbsResourceInstanceObject, *states.ResourceInstanceObjectSrc](),
ResourceInstanceProviderConfig: addrs.MakeMap[addrs.AbsResourceInstanceObject, addrs.AbsProviderConfig](),
DeferredResourceInstanceChanges: addrs.MakeMap[addrs.AbsResourceInstanceObject, *plans.DeferredResourceInstanceChangeSrc](),
})
}
c := l.ret.Components.Get(addr)
err = c.PlanTimestamp.UnmarshalText([]byte(msg.PlanTimestamp))
if err != nil {
return fmt.Errorf("invalid plan timestamp %q for %s", msg.PlanTimestamp, addr)
}
case *tfstackdata1.PlanResourceInstanceChangePlanned:
c, fullAddr, providerConfigAddr, err := LoadComponentForResourceInstance(l.ret, msg)
if err != nil {
return err
}
c.ResourceInstanceProviderConfig.Put(fullAddr, providerConfigAddr)
// Not all "planned changes" for resource instances are actually
// changes in the plans.Change sense, confusingly: sometimes the
// "change" we're recording is just to overwrite the state entry
// with a refreshed copy, in which case riPlan is nil and
// msg.PriorState is the main content of this change, handled below.
if msg.Change != nil {
riPlan, err := ValidateResourceInstanceChange(msg, fullAddr, providerConfigAddr)
if err != nil {
return err
}
c.ResourceInstancePlanned.Put(fullAddr, riPlan)
}
if msg.PriorState != nil {
stateSrc, err := tfstackdata1.DecodeProtoResourceInstanceObject(msg.PriorState)
if err != nil {
return fmt.Errorf("invalid prior state for %s: %w", fullAddr, err)
}
c.ResourceInstancePriorState.Put(fullAddr, stateSrc)
} else {
// We'll record an explicit nil just to affirm that there's
// intentionally no prior state for this resource instance
// object.
c.ResourceInstancePriorState.Put(fullAddr, nil)
}
case *tfstackdata1.PlanDeferredResourceInstanceChange:
if msg.Deferred == nil {
return fmt.Errorf("missing deferred from PlanDeferredResourceInstanceChange")
}
c, fullAddr, providerConfigAddr, err := LoadComponentForPartialResourceInstance(l.ret, msg.Change)
if err != nil {
return err
}
riPlan, err := ValidatePartialResourceInstanceChange(msg.Change, fullAddr, providerConfigAddr)
if err != nil {
return err
}
// We'll just swallow the error here. A missing deferred reason
// could be the only cause and we want to be forward and backward
// compatible. This will just render as INVALID, which is fine.
deferredReason, _ := planfile.DeferredReasonFromProto(msg.Deferred.Reason)
c.DeferredResourceInstanceChanges.Put(fullAddr, &plans.DeferredResourceInstanceChangeSrc{
ChangeSrc: riPlan,
DeferredReason: deferredReason,
})
default:
// Should not get here, because a stack plan can only be loaded by
// the same version of Terraform that created it, and the above
// should cover everything this version of Terraform can possibly
// emit during PlanStackChanges.
return fmt.Errorf("unsupported raw message type %T", msg)
}
return nil
}
// Plan consumes the loaded plan, making the associated loader closed to
// further additions.
func (l *Loader) Plan() (*Plan, error) {
l.mu.Lock()
defer l.mu.Unlock()
// If we got through all of the messages without encountering at least
// one *PlanHeader then we'll abort because we may have lost part of the
// plan sequence somehow.
if !l.foundHeader {
return nil, fmt.Errorf("missing PlanHeader")
}
// Before we return we'll calculate the reverse dependency information
// based on the forward dependency information we loaded above.
for dependentInstAddr, dependencyInst := range l.ret.Components.All() {
dependentAddr := stackaddrs.AbsComponent{
Stack: dependentInstAddr.Stack,
Item: dependentInstAddr.Item.Component,
}
for dependencyAddr := range dependencyInst.Dependencies.All() {
// FIXME: This is very inefficient because the current data structure doesn't
// allow looking up all of the component instances that have a particular
// component. This'll be okay as long as the number of components is
// small, but we'll need to improve this if we ever want to support stacks
// with a large number of components.
for maybeDependencyInstAddr, dependencyInst := range l.ret.Components.All() {
maybeDependencyAddr := stackaddrs.AbsComponent{
Stack: maybeDependencyInstAddr.Stack,
Item: maybeDependencyInstAddr.Item.Component,
}
if dependencyAddr.UniqueKey() == maybeDependencyAddr.UniqueKey() {
dependencyInst.Dependents.Add(dependentAddr)
}
}
}
}
ret := l.ret
l.ret = nil
return ret, nil
}
func LoadFromProto(msgs []*anypb.Any) (*Plan, error) {
loader := NewLoader()
for i, rawMsg := range msgs {
err := loader.AddRaw(rawMsg)
if err != nil {
return nil, fmt.Errorf("raw item %d: %w", i, err)
}
}
return loader.Plan()
}
func ValidateResourceInstanceChange(change *tfstackdata1.PlanResourceInstanceChangePlanned, fullAddr addrs.AbsResourceInstanceObject, providerConfigAddr addrs.AbsProviderConfig) (*plans.ResourceInstanceChangeSrc, error) {
riPlan, err := planfile.ResourceChangeFromProto(change.Change)
if err != nil {
return nil, fmt.Errorf("invalid resource instance change: %w", err)
}
// We currently have some redundant information in the nested
// "change" object due to having reused some protobuf message
// types from the traditional Terraform CLI planproto format.
// We'll make sure the redundant information is consistent
// here because otherwise they're likely to cause
// difficult-to-debug problems downstream.
if !riPlan.Addr.Equal(fullAddr.ResourceInstance) && riPlan.DeposedKey == fullAddr.DeposedKey {
return nil, fmt.Errorf("planned change has inconsistent address to its containing object")
}
if !riPlan.ProviderAddr.Equal(providerConfigAddr) {
return nil, fmt.Errorf("planned change has inconsistent provider configuration address to its containing object")
}
return riPlan, nil
}
func ValidatePartialResourceInstanceChange(change *tfstackdata1.PlanResourceInstanceChangePlanned, fullAddr addrs.AbsResourceInstanceObject, providerConfigAddr addrs.AbsProviderConfig) (*plans.ResourceInstanceChangeSrc, error) {
riPlan, err := planfile.DeferredResourceChangeFromProto(change.Change)
if err != nil {
return nil, fmt.Errorf("invalid resource instance change: %w", err)
}
// We currently have some redundant information in the nested
// "change" object due to having reused some protobuf message
// types from the traditional Terraform CLI planproto format.
// We'll make sure the redundant information is consistent
// here because otherwise they're likely to cause
// difficult-to-debug problems downstream.
if !riPlan.Addr.Equal(fullAddr.ResourceInstance) && riPlan.DeposedKey == fullAddr.DeposedKey {
return nil, fmt.Errorf("planned change has inconsistent address to its containing object")
}
if !riPlan.ProviderAddr.Equal(providerConfigAddr) {
return nil, fmt.Errorf("planned change has inconsistent provider configuration address to its containing object")
}
return riPlan, nil
}
func LoadComponentForResourceInstance(plan *Plan, change *tfstackdata1.PlanResourceInstanceChangePlanned) (*Component, addrs.AbsResourceInstanceObject, addrs.AbsProviderConfig, error) {
cAddr, diags := stackaddrs.ParseAbsComponentInstanceStr(change.ComponentInstanceAddr)
if diags.HasErrors() {
return nil, addrs.AbsResourceInstanceObject{}, addrs.AbsProviderConfig{}, fmt.Errorf("invalid component instance address syntax in %q", change.ComponentInstanceAddr)
}
providerConfigAddr, diags := addrs.ParseAbsProviderConfigStr(change.ProviderConfigAddr)
if diags.HasErrors() {
return nil, addrs.AbsResourceInstanceObject{}, addrs.AbsProviderConfig{}, fmt.Errorf("invalid provider configuration address syntax in %q", change.ProviderConfigAddr)
}
riAddr, diags := addrs.ParseAbsResourceInstanceStr(change.ResourceInstanceAddr)
if diags.HasErrors() {
return nil, addrs.AbsResourceInstanceObject{}, addrs.AbsProviderConfig{}, fmt.Errorf("invalid resource instance address syntax in %q", change.ResourceInstanceAddr)
}
var deposedKey addrs.DeposedKey
if change.DeposedKey != "" {
var err error
deposedKey, err = addrs.ParseDeposedKey(change.DeposedKey)
if err != nil {
return nil, addrs.AbsResourceInstanceObject{}, addrs.AbsProviderConfig{}, fmt.Errorf("invalid deposed key syntax in %q", change.DeposedKey)
}
}
fullAddr := addrs.AbsResourceInstanceObject{
ResourceInstance: riAddr,
DeposedKey: deposedKey,
}
c, ok := plan.Components.GetOk(cAddr)
if !ok {
return nil, addrs.AbsResourceInstanceObject{}, addrs.AbsProviderConfig{}, fmt.Errorf("resource instance change for unannounced component instance %s", cAddr)
}
return c, fullAddr, providerConfigAddr, nil
}
func LoadComponentForPartialResourceInstance(plan *Plan, change *tfstackdata1.PlanResourceInstanceChangePlanned) (*Component, addrs.AbsResourceInstanceObject, addrs.AbsProviderConfig, error) {
cAddr, diags := stackaddrs.ParsePartialComponentInstanceStr(change.ComponentInstanceAddr)
if diags.HasErrors() {
return nil, addrs.AbsResourceInstanceObject{}, addrs.AbsProviderConfig{}, fmt.Errorf("invalid component instance address syntax in %q", change.ComponentInstanceAddr)
}
providerConfigAddr, diags := addrs.ParseAbsProviderConfigStr(change.ProviderConfigAddr)
if diags.HasErrors() {
return nil, addrs.AbsResourceInstanceObject{}, addrs.AbsProviderConfig{}, fmt.Errorf("invalid provider configuration address syntax in %q", change.ProviderConfigAddr)
}
riAddr, diags := addrs.ParsePartialResourceInstanceStr(change.ResourceInstanceAddr)
if diags.HasErrors() {
return nil, addrs.AbsResourceInstanceObject{}, addrs.AbsProviderConfig{}, fmt.Errorf("invalid resource instance address syntax in %q", change.ResourceInstanceAddr)
}
var deposedKey addrs.DeposedKey
if change.DeposedKey != "" {
var err error
deposedKey, err = addrs.ParseDeposedKey(change.DeposedKey)
if err != nil {
return nil, addrs.AbsResourceInstanceObject{}, addrs.AbsProviderConfig{}, fmt.Errorf("invalid deposed key syntax in %q", change.DeposedKey)
}
}
fullAddr := addrs.AbsResourceInstanceObject{
ResourceInstance: riAddr,
DeposedKey: deposedKey,
}
c, ok := plan.Components.GetOk(cAddr)
if !ok {
return nil, addrs.AbsResourceInstanceObject{}, addrs.AbsProviderConfig{}, fmt.Errorf("resource instance change for unannounced component instance %s", cAddr)
}
return c, fullAddr, providerConfigAddr, nil
}