// Copyright (c) HashiCorp, Inc. // 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 }