blob: 8218f4e01131e691aeacd7c07c4f065cf08d381c [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package stackmigrate
import (
"fmt"
"iter"
"github.com/hashicorp/go-slug/sourceaddrs"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/providers"
"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/states"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// Migration is a struct that aids in migrating a terraform state to a stack configuration.
type Migration struct {
// Providers is a map of provider addresses available to the stack.
Providers map[addrs.Provider]providers.Factory
// PreviousState is the terraform core state that we are migrating from.
PreviousState *states.State
Config *stackconfig.Config
}
// Alias common types to make the code more readable.
type (
// ConfigComponent is the definition of a component in a stack configuration,
// and therefore is unique for all instances of a component in a stack.
Config = stackaddrs.ConfigComponent
// Every instance of a component in a stack instance has a unique address.
Instance = stackaddrs.AbsComponentInstance
// Every instance of a component in a stack has the same AbsComponent address.
AbsComponent = stackaddrs.AbsComponent
)
func (m *Migration) Migrate(resources map[string]string, modules map[string]string, emit func(change stackstate.AppliedChange), emitDiag func(diagnostic tfdiags.Diagnostic)) {
migration := &migration{
Migration: m,
emit: emit,
emitDiag: emitDiag,
providers: make(map[addrs.Provider]providers.Interface),
parser: configs.NewSourceBundleParser(m.Config.Sources),
configs: make(map[sourceaddrs.FinalSource]*configs.Config),
}
defer migration.close() // cleanup any opened providers.
components := migration.migrateResources(resources, modules)
migration.migrateComponents(components)
// Everything is migrated!
}
type migration struct {
*Migration
emit func(change stackstate.AppliedChange)
emitDiag func(diagnostic tfdiags.Diagnostic)
providers map[addrs.Provider]providers.Interface
parser *configs.SourceBundleParser
configs map[sourceaddrs.FinalSource]*configs.Config
}
func (m *migration) stateResources() iter.Seq2[addrs.AbsResource, *states.Resource] {
return func(yield func(addrs.AbsResource, *states.Resource) bool) {
for _, module := range m.PreviousState.Modules {
for _, resource := range module.Resources {
if !yield(resource.Addr, resource) {
return
}
}
}
}
}
// moduleConfig returns the module configuration for the component. If the configuration
// has already been loaded, it will be returned from the cache.
func (m *migration) moduleConfig(component *stackconfig.Component) (*configs.Config, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
if component.FinalSourceAddr == nil {
// if there is no final source address, then the configuration was likely
// loaded via a shallow load, but we need the full configuration.
panic("component has no final source address")
}
if cfg, ok := m.configs[component.FinalSourceAddr]; ok {
return cfg, diags
}
moduleConfig, diags := component.ModuleConfig(m.parser.Bundle())
if diags.HasErrors() {
return nil, diags
}
m.configs[component.FinalSourceAddr] = moduleConfig
return moduleConfig, diags
}
func (m *migration) emitDiags(diags tfdiags.Diagnostics) {
for _, diag := range diags {
m.emitDiag(diag)
}
}
func (m *migration) provider(provider addrs.Provider) (providers.Interface, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
if p, ok := m.providers[provider]; ok {
return p, diags
}
factory, ok := m.Migration.Providers[provider]
if !ok {
return nil, tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Error, "Provider not found", fmt.Sprintf("Provider %s not found in required_providers.", provider.ForDisplay()))}
}
p, err := factory()
if err != nil {
return nil, tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Error, "Provider initialization failed", fmt.Sprintf("Failed to initialize provider %s: %s", provider.ForDisplay(), err.Error()))}
}
m.providers[provider] = p
return p, diags
}
func (m *migration) close() {
for addr, provider := range m.providers {
if err := provider.Close(); err != nil {
m.emitDiag(tfdiags.Sourceless(tfdiags.Error, "Provider cleanup failed", fmt.Sprintf("Failed to close provider %s: %s", addr.ForDisplay(), err.Error())))
}
}
}