| // 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 | |
| // 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] | |
| // ActionInvocations describes planned action invocations that should be | |
| // preserved into the modules runtime apply plan. | |
| ActionInvocations []*plans.ActionInvocationInstanceSrc | |
| // DeferredActionInvocations describes action invocations that were deferred | |
| // to a later plan and apply cycle. | |
| DeferredActionInvocations []*plans.DeferredActionInvocationSrc | |
| // 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. | |
| 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) | |
| } | |
| } | |
| for _, action := range c.ActionInvocations { | |
| if action != nil { | |
| changes.ActionInvocations = append(changes.ActionInvocations, action) | |
| } | |
| } | |
| for _, deferredAction := range c.DeferredActionInvocations { | |
| if deferredAction != nil { | |
| plan.DeferredActionInvocations = append(plan.DeferredActionInvocations, deferredAction) | |
| } | |
| } | |
| 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, | |
| }) | |
| } | |
| for _, action := range c.ActionInvocations { | |
| if action == nil { | |
| continue | |
| } | |
| providerInstances.Add(addrs.RootProviderConfig{ | |
| Provider: action.ProviderAddr.Provider, | |
| Alias: action.ProviderAddr.Alias, | |
| }) | |
| } | |
| for _, deferredAction := range c.DeferredActionInvocations { | |
| if deferredAction == nil || deferredAction.ActionInvocationInstanceSrc == nil { | |
| continue | |
| } | |
| providerInstances.Add(addrs.RootProviderConfig{ | |
| Provider: deferredAction.ActionInvocationInstanceSrc.ProviderAddr.Provider, | |
| Alias: deferredAction.ActionInvocationInstanceSrc.ProviderAddr.Alias, | |
| }) | |
| } | |
| return providerInstances | |
| } |