| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package stackmigrate | |
| import ( | |
| "fmt" | |
| "strings" | |
| "github.com/hashicorp/hcl/v2" | |
| "github.com/hashicorp/terraform/internal/addrs" | |
| "github.com/hashicorp/terraform/internal/collections" | |
| "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/tfdiags" | |
| ) | |
| // stackResource represents a resource that was found in the terraform state. | |
| // It contains the stack and component configuration for the resource. | |
| type stackResource struct { | |
| // The fully qualified target address | |
| AbsResourceInstance stackaddrs.AbsResourceInstance | |
| // The stack and component configuration for the resource. | |
| StackConfig *stackconfig.Stack | |
| ComponentConfig *stackconfig.Component | |
| // The source module configuration for the stack component. | |
| StackModuleConfig *configs.Config | |
| } | |
| // implement the UniqueKeyer interface for stackResource | |
| // The key of a stackResource pointer is simply itself. | |
| func (r *stackResource) UniqueKey() collections.UniqueKey[*stackResource] { | |
| return r | |
| } | |
| // implement the UniqueKey interface for stackResource | |
| func (r *stackResource) IsUniqueKey(*stackResource) {} | |
| func (m *migration) migrateResources(resources map[string]string, modules map[string]string) collections.Map[Instance, collections.Set[*stackResource]] { | |
| components := collections.NewMap[Instance, collections.Set[*stackResource]]() | |
| // for each resource in the config, we track the instances that belong to the | |
| // same component. | |
| trackComponent := func(resource *stackResource) { | |
| instance := resource.AbsResourceInstance.Component | |
| if !components.HasKey(instance) { | |
| components.Put(instance, collections.NewSet[*stackResource]()) | |
| } | |
| components.Get(instance).Add(resource) | |
| } | |
| for _, resource := range m.stateResources() { | |
| for key, instance := range resource.Instances { | |
| source := resource.Addr.Instance(key) | |
| // check if the state resource has been requested for migration, | |
| // either by being in the resources map, or its module being in the modules map. | |
| // The returned target builds a new address for the resource within the | |
| // stack component where it will be migrated to. | |
| target, diags := m.search(source, resources, modules) | |
| if diags.HasErrors() { | |
| // if there are errors, we can't migrate this resource. | |
| m.emitDiags(diags) | |
| continue | |
| } | |
| // We have the component address, now load the stack and component configuration | |
| // for the resource. | |
| // If this is successful, we can now start adding source information | |
| // to diagnostics. | |
| diags = m.loadConfig(target) | |
| if diags.HasErrors() { | |
| m.emitDiags(diags) | |
| continue | |
| } | |
| trackComponent(target) | |
| // retrieve the provider that was uses to create the resource instance. | |
| providerAddr, provider, diags := m.getOwningProvider(source, target) | |
| if diags.HasErrors() { | |
| m.emitDiags(diags) | |
| continue | |
| } | |
| schema := provider.GetProviderSchema().SchemaForResourceType(resource.Addr.Resource.Mode, resource.Addr.Resource.Type) | |
| if schema.Body == nil { | |
| m.emitDiags(diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Invalid resource type", | |
| Detail: fmt.Sprintf("Resource type %s not found in provider schema.", resource.Addr.Resource.Type), | |
| Subject: target.StackModuleConfig.SourceAddrRange.Ptr(), | |
| })) | |
| continue | |
| } | |
| m.emit(&stackstate.AppliedChangeResourceInstanceObject{ | |
| ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ | |
| Component: target.AbsResourceInstance.Component, | |
| Item: target.AbsResourceInstance.Item.DeposedObject(addrs.NotDeposed), | |
| }, | |
| NewStateSrc: instance.Current, | |
| ProviderConfigAddr: providerAddr, | |
| Schema: schema, | |
| }) | |
| for deposedKey, deposed := range instance.Deposed { | |
| m.emit(&stackstate.AppliedChangeResourceInstanceObject{ | |
| ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ | |
| Component: target.AbsResourceInstance.Component, | |
| Item: target.AbsResourceInstance.Item.DeposedObject(deposedKey), | |
| }, | |
| NewStateSrc: deposed, | |
| ProviderConfigAddr: providerAddr, | |
| Schema: schema, | |
| }) | |
| } | |
| } | |
| } | |
| return components | |
| } | |
| // search searches for the state resource in the resource mappings and when found, converts and returns the relevant | |
| // stackResource. | |
| // | |
| // If the resource or module is nested within the root module, they will be migrated to the component with the address structure retained. | |
| // For example, a resource with the address module.my_module.module.child.aws_instance.foo will be migrated to | |
| // component.my_component.module.child.aws_instance.foo if the corresponding map key is found. | |
| // E.g module.child.aws_instance.foo will be replaced with component.child.aws_instance.foo | |
| func (m *migration) search(resource addrs.AbsResourceInstance, resources map[string]string, modules map[string]string) (*stackResource, tfdiags.Diagnostics) { | |
| target, ok := resources[resource.String()] | |
| if ok { | |
| // Then we have an exact mapping for this resource. | |
| component, rest, diags := stackaddrs.ParseAbsComponentInstanceStrOnly(target) | |
| if diags.HasErrors() { | |
| return nil, diags | |
| } | |
| if !component.Stack.IsRoot() { | |
| // we only support root components at the moment | |
| diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Invalid component instance", fmt.Sprintf("Only root component instances are allowed, got %q", target))) | |
| return nil, diags | |
| } | |
| if len(rest) > 0 { | |
| // Then we should have an exact mapping to a new resource. | |
| inst, moreDiags := addrs.ParseAbsResourceInstance(rest) | |
| diags = diags.Append(moreDiags) | |
| if moreDiags.HasErrors() { | |
| return nil, diags | |
| } | |
| return &stackResource{ | |
| AbsResourceInstance: stackaddrs.AbsResourceInstance{ | |
| Component: component, | |
| Item: inst, | |
| }, | |
| }, diags | |
| } | |
| // otherwise, we'll just put this resource into the specified component | |
| // at the same address. | |
| return &stackResource{ | |
| AbsResourceInstance: stackaddrs.AbsResourceInstance{ | |
| Component: component, | |
| Item: resource, | |
| }, | |
| }, diags | |
| } else { | |
| // no exact mapping, but if this isn't a resource in the root module | |
| // then we might have a mapping for the module it is in below. | |
| if resource.Module.IsRoot() { | |
| var diags tfdiags.Diagnostics | |
| diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Resource not found", fmt.Sprintf("Resource %q exists in state, but was not included in any provided mapping.", resource.Resource.String()))) | |
| return nil, diags | |
| } | |
| } | |
| // The resource is in a child module, so we need to find the component. | |
| // When found, we replace the module with the component instance, i.e | |
| // a resource of module.child.aws_instance.foo will be replaced with | |
| // component.child.aws_instance.foo | |
| if targetComponent, ok := modules[resource.Module[0].Name]; ok { | |
| inst, rest, diags := stackaddrs.ParseAbsComponentInstanceStrOnly("component." + strings.TrimPrefix(targetComponent, "component.")) | |
| if diags.HasErrors() { | |
| return nil, diags | |
| } | |
| if len(rest) > 0 || !inst.Stack.IsRoot() { | |
| // the module mapping should point directly to a root stack instance | |
| diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Invalid component instance", fmt.Sprintf("Only root component instances are allowed, got %q", target))) | |
| return nil, diags | |
| } | |
| inst.Item.Key = resource.Module[0].InstanceKey // retain the instance key | |
| return &stackResource{ | |
| AbsResourceInstance: stackaddrs.AbsResourceInstance{ | |
| Component: inst, | |
| Item: addrs.AbsResourceInstance{ | |
| Module: resource.Module[1:], // the first module instance is replaced by the component instance | |
| Resource: resource.Resource, | |
| }, | |
| }, | |
| }, diags | |
| } else { | |
| var diags tfdiags.Diagnostics | |
| diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Module not found", fmt.Sprintf("Module %q exists in state, but was not included in any provided mapping.", resource.Module[0].Name))) | |
| return nil, diags | |
| } | |
| } | |
| // getOwningProvider returns the address of the provider configuration, | |
| // as well as the provider instance, that was used to create the given resource instance. | |
| func (m *migration) getOwningProvider(source addrs.AbsResourceInstance, resource *stackResource) (addrs.AbsProviderConfig, providers.Interface, tfdiags.Diagnostics) { | |
| var ret addrs.AbsProviderConfig | |
| // At this point, we already worked out the stack component where we are migrating | |
| // the resource to. Now we need to look into the module configuration of the stack component, | |
| // and ensure that it has a provider configuration that matches the one used to create | |
| // the resource instance. | |
| moduleAddr := resource.AbsResourceInstance.Item.Module.Module() // the module address within the stack component's module configuration | |
| providerConfig, diags := m.findProviderConfig(source, moduleAddr, resource.AbsResourceInstance.Item.Resource.Resource, resource.StackModuleConfig, resource.AbsResourceInstance.Component) | |
| if diags.HasErrors() { | |
| return ret, nil, diags | |
| } | |
| component := resource.ComponentConfig | |
| stackCfg := resource.StackConfig | |
| // we found the provider configuration within the module configuration, | |
| // now look it up in the stack configuration. | |
| expr, ok := component.ProviderConfigs[providerConfig] | |
| if !ok { | |
| // Then the module uses a provider not referenced in the component. | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Provider not found for component", | |
| Detail: fmt.Sprintf("Provider %q not found in component %q.", providerConfig.LocalName, resource.AbsResourceInstance.Component.Item.Component.Name), | |
| Subject: component.SourceAddrRange.ToHCL().Ptr(), | |
| }) | |
| return ret, nil, diags | |
| } | |
| vars := expr.Variables() | |
| if len(vars) != 1 { | |
| // This should be an exact reference to a single provider, if it's not | |
| // we can't really do anything. | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Invalid provider reference", | |
| Detail: "Provider references should be a simple reference to a single provider.", | |
| Subject: expr.Range().Ptr(), | |
| }) | |
| return ret, nil, diags | |
| } | |
| ref, _, moreDiags := stackaddrs.ParseReference(vars[0]) | |
| diags = diags.Append(moreDiags) | |
| switch ref := ref.Target.(type) { | |
| case stackaddrs.ProviderConfigRef: | |
| providerAddr, ok := stackCfg.RequiredProviders.ProviderForLocalName(ref.ProviderLocalName) | |
| if !ok { | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Provider not found for component", | |
| Detail: fmt.Sprintf("Provider %s was needed by the resource %s but was not found in the stack configuration.", ref.ProviderLocalName, resource.AbsResourceInstance.String()), | |
| Subject: component.SourceAddrRange.ToHCL().Ptr(), | |
| }) | |
| return ret, nil, diags | |
| } | |
| addr := addrs.AbsProviderConfig{ | |
| Module: addrs.RootModule, | |
| Provider: providerAddr, | |
| Alias: providerConfig.Alias, // we still use the alias from the module provider as this is referenced as if from within the module. | |
| } | |
| provider, pDiags := m.provider(providerAddr) | |
| // pull in source information for diagnostics if available. | |
| for _, diag := range pDiags { | |
| if diag.Source().Subject == nil { | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: diag.Severity().ToHCL(), | |
| Summary: diag.Description().Summary, | |
| Detail: diag.Description().Detail, | |
| Subject: resource.ComponentConfig.SourceAddrRange.ToHCL().Ptr(), | |
| }) | |
| } | |
| } | |
| return addr, provider, diags | |
| default: | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Invalid reference", | |
| Detail: "Non-provider reference found in provider configuration.", | |
| Subject: expr.Range().Ptr(), | |
| }) | |
| return ret, nil, diags | |
| } | |
| } | |
| // findProviderConfig recursively searches through the stack module configuration to find the provider | |
| // that was used to create the resource instance. | |
| func (m *migration) findProviderConfig(source addrs.AbsResourceInstance, module addrs.Module, resource addrs.Resource, config *configs.Config, component stackaddrs.AbsComponentInstance) (addrs.LocalProviderConfig, tfdiags.Diagnostics) { | |
| var diags tfdiags.Diagnostics | |
| if module.IsRoot() { | |
| r := config.Module.ResourceByAddr(resource) | |
| if r == nil { | |
| diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Resource mapped to non-existent target", fmt.Sprintf("Could not migrate resource %q. Target resource %q not found in component %q.", source, resource.InModule(module), component))) | |
| return addrs.LocalProviderConfig{}, diags | |
| } | |
| return r.ProviderConfigAddr(), diags | |
| } | |
| next, ok := config.Children[module[0]] | |
| if !ok { | |
| diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Resource mapped to non-existent target", fmt.Sprintf("Could not migrate resource %q. Module %q not found in component %q.", source, module, component))) | |
| return addrs.LocalProviderConfig{}, diags | |
| } | |
| // the address points to a nested module, so we continue the search | |
| // within the next module's configuration. | |
| provider, moreDiags := m.findProviderConfig(source, module[1:], resource, next, component) | |
| diags = diags.Append(moreDiags) | |
| if diags.HasErrors() { | |
| return addrs.LocalProviderConfig{}, diags | |
| } | |
| call, ok := config.Module.ModuleCalls[module[0]] | |
| if !ok { | |
| diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Resource mapped to non-existent target", fmt.Sprintf("Could not migrate resource %q. Module %q not found in component %q.", source, module, component))) | |
| return addrs.LocalProviderConfig{}, diags | |
| } | |
| for _, p := range call.Providers { | |
| if p.InChild.Name == provider.LocalName && p.InChild.Alias == provider.Alias { | |
| return p.InParent.Addr(), diags | |
| } | |
| } | |
| // if we reach here, then the provider was not explicitly passed to the module call | |
| // so we can just return the provider config determined by the resource. | |
| return provider, diags | |
| } | |
| // loadConfig loads the module and component configuration from the stack directory. | |
| func (m *migration) loadConfig(resource *stackResource) tfdiags.Diagnostics { | |
| var diags tfdiags.Diagnostics | |
| instance := resource.AbsResourceInstance.Component | |
| stack := m.Config.Stack(instance.Stack.ConfigAddr()) | |
| if stack == nil { | |
| return diags.Append(tfdiags.Sourceless(tfdiags.Error, "Stack not found", fmt.Sprintf("Stack %q not found in configuration.", instance.Stack.ConfigAddr()))) | |
| } | |
| resource.StackConfig = stack | |
| component := m.Config.Component(stackaddrs.ConfigComponentForAbsInstance(instance)) | |
| if component == nil { | |
| return diags.Append(tfdiags.Sourceless(tfdiags.Error, "Component not found", fmt.Sprintf("Component %q not found in stack %q.", instance.Item.Component.Name, instance.Stack.ConfigAddr()))) | |
| } | |
| resource.ComponentConfig = component | |
| moduleConfig, diags := m.moduleConfig(component) | |
| if diags.HasErrors() { | |
| return diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Module configuration not found", | |
| Detail: fmt.Sprintf("Module configuration for component %q not found.", instance.Item.Component.Name), | |
| Subject: component.SourceAddrRange.ToHCL().Ptr(), | |
| }) | |
| } | |
| resource.StackModuleConfig = moduleConfig | |
| return diags | |
| } |