blob: b892cbf3057bcfb84a472780ccef214d8d02a0fc [file] [log] [blame] [edit]
// 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
}