| // Copyright IBM Corp. 2014, 2026 | |
| // 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()))) | |
| } | |
| } | |
| } |