| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package stackmigrate | |
| import ( | |
| "fmt" | |
| "github.com/hashicorp/hcl/v2" | |
| "github.com/hashicorp/hcl/v2/hcldec" | |
| "github.com/zclconf/go-cty/cty" | |
| "github.com/hashicorp/terraform/internal/addrs" | |
| "github.com/hashicorp/terraform/internal/collections" | |
| "github.com/hashicorp/terraform/internal/stacks/stackaddrs" | |
| "github.com/hashicorp/terraform/internal/stacks/stackconfig" | |
| "github.com/hashicorp/terraform/internal/stacks/stackstate" | |
| "github.com/hashicorp/terraform/internal/tfdiags" | |
| ) | |
| func (m *migration) migrateComponents(components collections.Map[Instance, collections.Set[*stackResource]]) { | |
| // We need to calculate the dependencies between components, so we can | |
| // populate the dependencies and dependents fields in the component instances. | |
| dependencies, dependents := m.calculateDependencies(components) | |
| for instance := range components.All() { | |
| // We need to see the inputs and outputs from the component, so we can | |
| // create the component instance with the correct values. | |
| // ignore the diag because we already found this when loading the config. | |
| config, _ := m.moduleConfig(m.Config.Component(stackaddrs.ConfigComponentForAbsInstance(instance))) | |
| // We can put unknown values into the state for now, as Stacks should | |
| // perform a refresh before actually using any of these anyway. | |
| inputs := make(map[addrs.InputVariable]cty.Value, len(config.Module.Variables)) | |
| for name := range config.Module.Variables { | |
| inputs[addrs.InputVariable{Name: name}] = cty.DynamicVal | |
| } | |
| outputs := make(map[addrs.OutputValue]cty.Value, len(config.Module.Outputs)) | |
| for name := range config.Module.Outputs { | |
| outputs[addrs.OutputValue{Name: name}] = cty.DynamicVal | |
| } | |
| // We need this address to be able to look up dependencies and | |
| // dependents later. | |
| addr := AbsComponent{ | |
| Stack: instance.Stack, | |
| Item: instance.Item.Component, | |
| } | |
| // We emit a change a change for each component instance | |
| m.emit(&stackstate.AppliedChangeComponentInstance{ | |
| ComponentAddr: AbsComponent{ | |
| Stack: stackaddrs.RootStackInstance, | |
| Item: instance.Item.Component, | |
| }, | |
| ComponentInstanceAddr: instance, | |
| OutputValues: outputs, | |
| InputVariables: inputs, | |
| // If a destroy plan, or a removed block, is executed before the | |
| // next plan is applied, the component will break without this | |
| // metadata. | |
| Dependencies: dependencies.Get(addr), | |
| Dependents: dependents.Get(addr), | |
| }) | |
| } | |
| } | |
| func (m *migration) calculateDependencies(components collections.Map[Instance, collections.Set[*stackResource]]) (collections.Map[AbsComponent, collections.Set[AbsComponent]], collections.Map[AbsComponent, collections.Set[AbsComponent]]) { | |
| // The dependency map cares only about config components rather than instances, | |
| // so we need to convert the map to use the config component address. | |
| cfgComponents := collections.NewMap[AbsComponent, collections.Set[*stackResource]]() | |
| for in, cmpnts := range components.All() { | |
| cfgComponents.Put(AbsComponent{ | |
| Stack: in.Stack, | |
| Item: in.Item.Component, | |
| }, cmpnts) | |
| } | |
| dependencies := collections.NewMap[AbsComponent, collections.Set[AbsComponent]]() | |
| dependents := collections.NewMap[AbsComponent, collections.Set[AbsComponent]]() | |
| // First, we're going to work out the dependencies between components. | |
| for addr, cmpnts := range cfgComponents.All() { | |
| for resource := range cmpnts.All() { | |
| instance := resource.AbsResourceInstance.Component | |
| compDepSet := collections.NewSet[AbsComponent]() | |
| // We collect the component's dependencies, and also | |
| // add the component to the dependent set of its dependencies. | |
| addDependencies := func(dss collections.Set[AbsComponent]) { | |
| compDepSet.AddAll(dss) | |
| for cmpt := range dss.All() { | |
| if !dependents.HasKey(cmpt) { | |
| dependents.Put(cmpt, collections.NewSet[AbsComponent]()) | |
| } | |
| dependents.Get(cmpt).Add(addr) | |
| } | |
| } | |
| component := resource.ComponentConfig | |
| stack := resource.StackConfig | |
| // First, check the inputs. | |
| inputDependencies, inputDiags := m.componentDependenciesFromExpression(component.Inputs, instance.Stack, cfgComponents) | |
| m.emitDiags(inputDiags) | |
| addDependencies(inputDependencies) | |
| // Then, check the depends_on directly. | |
| for _, traversal := range component.DependsOn { | |
| dependsOnDependencies, dependsOnDiags := m.componentDependenciesFromTraversal(traversal, instance.Stack, cfgComponents) | |
| m.emitDiags(dependsOnDiags) | |
| addDependencies(dependsOnDependencies) | |
| } | |
| // Then, check the foreach. | |
| forEachDependencies, forEachDiags := m.componentDependenciesFromExpression(component.ForEach, instance.Stack, cfgComponents) | |
| m.emitDiags(forEachDiags) | |
| addDependencies(forEachDependencies) | |
| // Finally, we're going to look at the providers, and see if they | |
| // depend on any other components. | |
| for _, expr := range component.ProviderConfigs { | |
| pds, diags := m.providerDependencies(expr, instance.Stack, stack, cfgComponents) | |
| m.emitDiags(diags) | |
| addDependencies(pds) | |
| } | |
| // We're happy we got all the dependencies for this component, so we | |
| // can store them now. | |
| dependencies.Put(addr, compDepSet) | |
| } | |
| } | |
| return dependencies, dependents | |
| } | |
| // componentDependenciesFromExpression returns a set of components that are | |
| // referenced in the given expression. | |
| func (m *migration) componentDependenciesFromExpression(expr hcl.Expression, current stackaddrs.StackInstance, components collections.Map[AbsComponent, collections.Set[*stackResource]]) (ds collections.Set[AbsComponent], diags tfdiags.Diagnostics) { | |
| ds = collections.NewSet[AbsComponent]() | |
| if expr == nil { | |
| return ds, diags | |
| } | |
| for _, v := range expr.Variables() { | |
| dss, moreDiags := m.componentDependenciesFromTraversal(v, current, components) | |
| ds.AddAll(dss) | |
| diags = diags.Append(moreDiags) | |
| } | |
| return ds, diags | |
| } | |
| // componentDependenciesFromTraversal returns the component that is referenced | |
| // in the given traversal, if it is a component reference. | |
| func (m *migration) componentDependenciesFromTraversal(traversal hcl.Traversal, current stackaddrs.StackInstance, components collections.Map[AbsComponent, collections.Set[*stackResource]]) (deps collections.Set[AbsComponent], diags tfdiags.Diagnostics) { | |
| deps = collections.NewSet[AbsComponent]() | |
| parsed, _, moreDiags := stackaddrs.ParseReference(traversal) | |
| diags = diags.Append(moreDiags) | |
| if moreDiags.HasErrors() { | |
| // Then the configuration is invalid, so we'll skip this variable. | |
| // The user should have ran a separate validation step before | |
| // performing the migration to catch this. | |
| return deps, diags | |
| } | |
| switch ref := parsed.Target.(type) { | |
| case stackaddrs.Component: | |
| // We have a reference to a component in the current stack. | |
| deps.Add(AbsComponent{ | |
| Stack: current, | |
| Item: ref, | |
| }) | |
| return deps, diags | |
| case stackaddrs.StackCall: | |
| targetStackAddress := append(current.ConfigAddr(), stackaddrs.StackStep(ref)) | |
| stack := m.Config.Stack(targetStackAddress) | |
| if stack == nil { | |
| // reference to a stack that does not exist in the configuration. | |
| diags = diags.Append(hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Stack not found", | |
| Detail: fmt.Sprintf("Stack %q not found in configuration.", targetStackAddress), | |
| Subject: parsed.SourceRange.ToHCL().Ptr(), | |
| }) | |
| return deps, diags | |
| } | |
| for name := range stack.Components { | |
| // If the component in the stack call is part of the mapping, then it will | |
| // be present in the map, and we will add it to the dependencies. | |
| // Otherwise, we will ignore it. | |
| componentAddr := AbsComponent{ | |
| Stack: current.Child(ref.Name, addrs.NoKey), | |
| Item: stackaddrs.Component{Name: name}, | |
| } | |
| if _, ok := components.GetOk(componentAddr); ok { | |
| deps.Add(componentAddr) | |
| } | |
| } | |
| return deps, diags | |
| default: | |
| // This is not a component reference, and we only care about | |
| // component dependencies. | |
| return deps, diags | |
| } | |
| } | |
| func (m *migration) providerDependencies(expr hcl.Expression, current stackaddrs.StackInstance, stack *stackconfig.Stack, components collections.Map[AbsComponent, collections.Set[*stackResource]]) (ds collections.Set[AbsComponent], diags tfdiags.Diagnostics) { | |
| ds = collections.NewSet[AbsComponent]() | |
| for _, v := range expr.Variables() { | |
| ref, _, moreDiags := stackaddrs.ParseReference(v) | |
| diags = diags.Append(moreDiags) | |
| if moreDiags.HasErrors() { | |
| // Invalid configuration, so skip it. | |
| continue | |
| } | |
| switch ref := ref.Target.(type) { | |
| case stackaddrs.ProviderConfigRef: | |
| config := stack.ProviderConfigs[addrs.LocalProviderConfig{ | |
| LocalName: ref.ProviderLocalName, | |
| Alias: ref.Name, | |
| }] | |
| dss, moreDiags := m.componentDependenciesFromExpression(config.ForEach, current, components) | |
| diags = diags.Append(moreDiags) | |
| ds.AddAll(dss) | |
| if config.Config == nil { | |
| // if there is no configuration, then there won't be any | |
| // dependencies. | |
| break | |
| } | |
| addr, ok := stack.RequiredProviders.ProviderForLocalName(ref.ProviderLocalName) | |
| if !ok { | |
| diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Provider not found", fmt.Sprintf("Provider %s not found in required_providers.", ref.ProviderLocalName))) | |
| continue | |
| } | |
| provider, pDiags := m.provider(addr) | |
| if pDiags.HasErrors() { | |
| diags = diags.Append(pDiags) | |
| continue // skip this provider if we can't get the schema | |
| } | |
| spec := provider.GetProviderSchema().Provider.Body.DecoderSpec() | |
| traversals := hcldec.Variables(config.Config, spec) | |
| for _, traversal := range traversals { | |
| dss, moreDiags := m.componentDependenciesFromTraversal(traversal, current, components) | |
| diags = diags.Append(moreDiags) | |
| ds.AddAll(dss) | |
| } | |
| default: | |
| // This is not a provider reference, and we only care about | |
| // provider dependencies. | |
| continue | |
| } | |
| } | |
| return ds, diags | |
| } |