// 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()))) } } }