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.
terraform/internal/stacks/stackplan/from_proto.go

253 lines
9.8 KiB

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package stackplan
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/plans"
"github.com/hashicorp/terraform/internal/plans/planfile"
"github.com/hashicorp/terraform/internal/plans/planproto"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackstate"
"github.com/hashicorp/terraform/internal/stacks/tfstackdata1"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/version"
)
func LoadFromProto(msgs []*anypb.Any) (*Plan, error) {
ret := &Plan{
RootInputValues: make(map[stackaddrs.InputVariable]cty.Value),
Components: collections.NewMap[stackaddrs.AbsComponentInstance, *Component](),
}
foundHeader := false
for i, rawMsg := range msgs {
msg, err := anypb.UnmarshalNew(rawMsg, proto.UnmarshalOptions{
// Just the default unmarshalling options
})
if err != nil {
return nil, fmt.Errorf("invalid raw message %d: %w", i, 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 nil, fmt.Errorf("plan was created by Terraform %s, but this is Terraform %s", gotVersion, wantVersion)
}
ret.PrevRunStateRaw = msg.PrevRunStateRaw
foundHeader = true
case *tfstackdata1.PlanApplyable:
ret.Applyable = msg.Applyable
case *tfstackdata1.PlanRootInputValue:
addr := stackaddrs.InputVariable{
Name: msg.Name,
}
dv := plans.DynamicValue(msg.Value.Msgpack)
val, err := dv.Decode(cty.DynamicPseudoType)
if err != nil {
return nil, fmt.Errorf("invalid stored value for %s: %w", addr, err)
}
ret.RootInputValues[addr] = val
case *tfstackdata1.PlanComponentInstance:
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 nil, 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 nil, fmt.Errorf("invalid component address syntax in %q", rawAddr)
}
if addr.Item.Key != addrs.NoKey {
return nil, 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 nil, fmt.Errorf("decoding plan for %s: %w", addr, err)
}
outputVals := make(map[addrs.OutputValue]cty.Value)
for name, rawVal := range msg.PlannedOutputValues {
v, err := tfstackdata1.DynamicValueFromTFStackData1(rawVal, cty.DynamicPseudoType)
if err != nil {
return nil, 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 nil, fmt.Errorf("decoding check results: %w", err)
}
if !ret.Components.HasKey(addr) {
ret.Components.Put(addr, &Component{
PlannedAction: plannedAction,
PlanApplyable: msg.PlanApplyable,
PlanComplete: msg.PlanComplete,
Dependencies: dependencies,
Dependents: collections.NewSet[stackaddrs.AbsComponent](),
PlannedOutputValues: outputVals,
PlannedChecks: checkResults,
ResourceInstancePlanned: addrs.MakeMap[addrs.AbsResourceInstanceObject, *plans.ResourceInstanceChangeSrc](),
ResourceInstancePriorState: addrs.MakeMap[addrs.AbsResourceInstanceObject, *states.ResourceInstanceObjectSrc](),
ResourceInstanceProviderConfig: addrs.MakeMap[addrs.AbsResourceInstanceObject, addrs.AbsProviderConfig](),
})
}
c := ret.Components.Get(addr)
err = c.PlanTimestamp.UnmarshalText([]byte(msg.PlanTimestamp))
if err != nil {
return nil, fmt.Errorf("invalid plan timestamp %q for %s", msg.PlanTimestamp, addr)
}
case *tfstackdata1.PlanResourceInstanceChangePlanned:
cAddr, diags := stackaddrs.ParseAbsComponentInstanceStr(msg.ComponentInstanceAddr)
if diags.HasErrors() {
return nil, fmt.Errorf("invalid component instance address syntax in %q", msg.ComponentInstanceAddr)
}
riAddr, diags := addrs.ParseAbsResourceInstanceStr(msg.ResourceInstanceAddr)
if diags.HasErrors() {
return nil, fmt.Errorf("invalid resource instance address syntax in %q", msg.ResourceInstanceAddr)
}
var deposedKey addrs.DeposedKey
if msg.DeposedKey != "" {
deposedKey, err = addrs.ParseDeposedKey(msg.DeposedKey)
if err != nil {
return nil, fmt.Errorf("invalid deposed key syntax in %q", msg.DeposedKey)
}
}
providerConfigAddr, diags := addrs.ParseAbsProviderConfigStr(msg.ProviderConfigAddr)
if diags.HasErrors() {
return nil, fmt.Errorf("invalid provider configuration address syntax in %q", msg.ProviderConfigAddr)
}
fullAddr := addrs.AbsResourceInstanceObject{
ResourceInstance: riAddr,
DeposedKey: deposedKey,
}
c, ok := ret.Components.GetOk(cAddr)
if !ok {
return nil, fmt.Errorf("resource instance change for unannounced component instance %s", cAddr)
}
c.ResourceInstanceProviderConfig.Put(fullAddr, providerConfigAddr)
var riPlan *plans.ResourceInstanceChangeSrc
// 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 = planfile.ResourceChangeFromProto(msg.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")
}
c.ResourceInstancePlanned.Put(fullAddr, riPlan)
}
if msg.PriorState != nil {
stateSrc, err := stackstate.DecodeProtoResourceInstanceObject(msg.PriorState)
if err != nil {
return nil, 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)
}
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 nil, fmt.Errorf("unsupported raw message type %T at index %d", msg, i)
}
}
// 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 !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 _, elem := range ret.Components.Elems() {
dependentInstAddr := elem.K
dependentAddr := stackaddrs.AbsComponent{
Stack: dependentInstAddr.Stack,
Item: dependentInstAddr.Item.Component,
}
for _, dependencyAddr := range elem.V.Dependencies.Elems() {
// 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 _, elem := range ret.Components.Elems() {
maybeDependencyInstAddr := elem.K
maybeDependencyAddr := stackaddrs.AbsComponent{
Stack: maybeDependencyInstAddr.Stack,
Item: maybeDependencyInstAddr.Item.Component,
}
if dependencyAddr.UniqueKey() == maybeDependencyAddr.UniqueKey() {
elem.V.Dependents.Add(dependentAddr)
}
}
}
}
return ret, nil
}