terraform/internal/stacks/stackplan/component.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

168 lines
6.6 KiB
Go
Raw Permalink Normal View History

// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package stackplan
import (
"fmt"
"time"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/collections"
"github.com/hashicorp/terraform/internal/lang"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/states"
)
// Component is a container for a set of changes that all belong to the same
// component instance as declared in a stack configuration.
//
// Each instance of component essentially maps to one call into the main
// Terraform language runtime to apply all of the described changes together as
// a single operation.
type Component struct {
PlannedAction plans.Action
Mode plans.Mode
terraform: Plans can be "complete" and "applyable" These ideas are both already implied by some logic elsewhere in the system, but until now we didn't have the decision logic centralized in a single place that could therefore evolve over time without necessarily always updating every caller together. We'll now have the modules runtime produce its own boolean ruling about each characteristic, which callers can rely on for the mechanical decision-making of whether to offer the user an "approve" prompt, and whether to remind the user after apply that it was an incomplete plan that will probably therefore need at least one more plan/apply round to converge. The "Applyable" flag directly replaces the previous method Plan.CanApply, with equivalent logic. Making this a field instead of a method means that we can freeze it as part of a saved plan, rather than recalculating it when we reload the plan, and we can export the field value in our export formats like JSON while ensuring it'll always be consistent with what Terraform is using internally. Callers can (and should) still use other context in the plan to return more tailored messages for specific situations they already know about that might be useful to users, but with these flags as a baseline callers can now just fall back to a generic presentation when encountering a situation they don't yet understand, rather than making the wrong decision and causing something strange to happen. That is: a lack of awareness of a new rule will now cause just a generic message in the UI, rather than incorrect behavior. This commit mostly just deals with populating the flags, and then all of the direct consequences of that on our various tests. Further changes to actually make use of these flags elsewhere in the system will follow in later commits, both in this repository and in other repositories.
2024-02-08 14:07:55 -05:00
// These fields echo the [plans.Plan.Applyable] and [plans.Plan.Complete]
// field respectively. See the docs for those fields for more information.
PlanApplyable, PlanComplete bool
// ResourceInstancePlanned describes the changes that Terraform is proposing
// to make to try to converge the real system state with the desired state
// as described by the configuration.
ResourceInstancePlanned addrs.Map[addrs.AbsResourceInstanceObject, *plans.ResourceInstanceChangeSrc]
// ResourceInstancePriorState describes the state as it was when making
// the proposals described in [Component.ResourceInstancePlanned].
//
// Elements of this map have nil values if the planned action is "create",
// since in that case there is no prior object.
ResourceInstancePriorState addrs.Map[addrs.AbsResourceInstanceObject, *states.ResourceInstanceObjectSrc]
// ResourceInstanceProviderConfig is a lookup table from resource instance
// object address to the address of the provider configuration that
// will handle any apply-time actions for that object.
ResourceInstanceProviderConfig addrs.Map[addrs.AbsResourceInstanceObject, addrs.AbsProviderConfig]
// DeferredResourceInstanceChanges is a set of resource instance objects
// that have changes that are deferred to a later plan and apply cycle.
DeferredResourceInstanceChanges addrs.Map[addrs.AbsResourceInstanceObject, *plans.DeferredResourceInstanceChangeSrc]
// PlanTimestamp is the time Terraform Core recorded as the single "plan
// timestamp", which is used only for the result of the "plantimestamp"
// function during apply and must not be used for any other purpose.
PlanTimestamp time.Time
// Dependencies is a set of addresses of other components that this one
// expects to exist for as long as this one exists.
Dependencies collections.Set[stackaddrs.AbsComponent]
// Dependents is the reverse of [Component.Dependencies], describing
// the other components that must be destroyed before this one could
// be destroyed.
Dependents collections.Set[stackaddrs.AbsComponent]
// PlannedFunctionResults is a shared table of results from calling
// provider functions. This is stored and loaded from during the planning
// stage to use during apply operations.
2025-05-08 11:42:05 -04:00
PlannedFunctionResults []lang.FunctionResultHash
// PlannedInputValues and PlannedInputValueMarks are the values that
// Terraform has planned to use for input variables in this component.
PlannedInputValues map[addrs.InputVariable]plans.DynamicValue
PlannedInputValueMarks map[addrs.InputVariable][]cty.PathValueMarks
PlannedOutputValues map[addrs.OutputValue]cty.Value
PlannedChecks *states.CheckResults
}
// ForModulesRuntime translates the component instance plan into the form
// expected by the modules runtime, which is what would ultimately be used
// to apply the plan.
//
// The stack component planning model preserves only the most crucial details
// of a component plan produced by the modules runtime, and so the result
// will not exactly match the [plans.Plan] that the component plan was produced
// from, but should be complete enough to successfully apply the plan.
//
// Conversion with this method should always succeed if the given previous
// run state is truly the one that the plan was created from. If this method
// returns an error then that suggests that the recieving plan is inconsistent
// with the given previous run state, which should not happen if the caller
// is using Terraform Core correctly.
func (c *Component) ForModulesRuntime() (*plans.Plan, error) {
changes := &plans.ChangesSrc{}
plan := &plans.Plan{
UIMode: c.Mode,
Changes: changes,
Timestamp: c.PlanTimestamp,
Applyable: c.PlanApplyable,
Complete: c.PlanComplete,
Checks: c.PlannedChecks,
FunctionResults: c.PlannedFunctionResults,
}
for _, elem := range c.ResourceInstancePlanned.Elems {
changeSrc := elem.Value
if changeSrc != nil {
changes.Resources = append(changes.Resources, changeSrc)
}
}
priorState := states.NewState()
ss := priorState.SyncWrapper()
for _, elem := range c.ResourceInstancePriorState.Elems {
addr := elem.Key
providerConfigAddr, ok := c.ResourceInstanceProviderConfig.GetOk(addr)
if !ok {
return nil, fmt.Errorf("no provider config address for %s", addr)
}
stateSrc := elem.Value
if addr.IsCurrent() {
ss.SetResourceInstanceCurrent(addr.ResourceInstance, stateSrc, providerConfigAddr)
} else {
ss.SetResourceInstanceDeposed(addr.ResourceInstance, addr.DeposedKey, stateSrc, providerConfigAddr)
}
}
variableValues := make(map[string]plans.DynamicValue, len(c.PlannedInputValues))
variableMarks := make(map[string][]cty.PathValueMarks, len(c.PlannedInputValueMarks))
for k, v := range c.PlannedInputValues {
variableValues[k.Name] = v
}
plan.VariableValues = variableValues
for k, v := range c.PlannedInputValueMarks {
variableMarks[k.Name] = v
}
plan.VariableMarks = variableMarks
plan.PriorState = priorState
plan.PrevRunState = priorState.DeepCopy() // This is just here to complete the data structure; we don't really do anything with it
return plan, nil
}
// RequiredProviderInstances returns a description of all the provider instance
// slots that are required to satisfy the resource instances planned for this
// component.
//
// See also stackstate.State.RequiredProviderInstances and
// stackeval.ComponentConfig.RequiredProviderInstances for similar functions
// that retrieve the provider instances for a components in the config and in
// the state.
func (c *Component) RequiredProviderInstances() addrs.Set[addrs.RootProviderConfig] {
providerInstances := addrs.MakeSet[addrs.RootProviderConfig]()
for _, elem := range c.ResourceInstanceProviderConfig.Elems {
providerInstances.Add(addrs.RootProviderConfig{
Provider: elem.Value.Provider,
Alias: elem.Value.Alias,
})
}
return providerInstances
}