| // 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/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" |
| "github.com/zclconf/go-cty/cty" |
| ) |
| |
| 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.AbsResource.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 |
| } |