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