diff --git a/internal/stacks/stackmigrate/migrate_test.go b/internal/stacks/stackmigrate/migrate_test.go index 27dbd25d13..1be6abd474 100644 --- a/internal/stacks/stackmigrate/migrate_test.go +++ b/internal/stacks/stackmigrate/migrate_test.go @@ -793,8 +793,8 @@ func TestMigrate(t *testing.T) { }, expectedDiags: tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, - Summary: "Provider not found", - Detail: "Resource \"testing_resource.for_child\" not found in root module.", + Summary: "Resource mapped to non-existent target", + Detail: "Could not migrate resource \"testing_resource.for_child\". Target resource \"testing_resource.for_child\" not found in component \"component.child\".", }), }, @@ -924,7 +924,7 @@ func TestMigrate(t *testing.T) { expectedDiags: tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Resource not found", - Detail: "Resource \"testing_resource.for_child\" not found in mapping.", + Detail: "Resource \"testing_resource.for_child\" exists in state, but was not included in any provided mapping.", }), }, "config depends on": { @@ -1335,6 +1335,117 @@ func TestMigrate(t *testing.T) { }, }, }, + "single component": { + path: filepath.Join("for-stacks-migrate", "single-component"), + state: func(ss *states.SyncState) { + ss.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "one", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "one", + "value": "one", + }), + }, + mustDefaultRootProvider("testing"), + ) + ss.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "two", + }.Instance(addrs.NoKey).Absolute(addrs.ModuleInstance{ + { + Name: "two", + }, + }), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "two", + "value": "two", + }), + }, + mustDefaultRootProvider("testing"), + ) + ss.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "three", + }.Instance(addrs.NoKey).Absolute(addrs.ModuleInstance{ + { + Name: "three", + }, + }), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "three", + "value": "three", + }), + }, + mustDefaultRootProvider("testing"), + ) + }, + resources: map[string]string{ + "testing_resource.one": "component.single.testing_resource.one", + }, + modules: map[string]string{ + "two": "single", + "three": "single", + }, expected: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.single"), + ComponentInstanceAddr: mustAbsComponentInstance("component.single"), + OutputValues: map[addrs.OutputValue]cty.Value{}, + InputVariables: map[addrs.InputVariable]cty.Value{}, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.single.testing_resource.one"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "one", + "value": "one", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.single.testing_resource.three"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "three", + "value": "three", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.single.testing_resource.two"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "two", + "value": "two", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + }, + }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { diff --git a/internal/stacks/stackmigrate/resources.go b/internal/stacks/stackmigrate/resources.go index 846c705bd8..58722f6b03 100644 --- a/internal/stacks/stackmigrate/resources.go +++ b/internal/stacks/stackmigrate/resources.go @@ -57,11 +57,13 @@ func (m *migration) migrateResources(resources map[string]string, modules map[st 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(resource.Addr.Instance(key), resources, modules) + target, diags := m.search(source, resources, modules) if diags.HasErrors() { // if there are errors, we can't migrate this resource. m.emitDiags(diags) @@ -80,7 +82,7 @@ func (m *migration) migrateResources(resources map[string]string, modules map[st trackComponent(target) // retrieve the provider that was uses to create the resource instance. - providerAddr, provider, diags := m.getOwningProvider(target) + providerAddr, provider, diags := m.getOwningProvider(source, target) if diags.HasErrors() { m.emitDiags(diags) continue @@ -90,7 +92,7 @@ func (m *migration) migrateResources(resources map[string]string, modules map[st if schema.Body == nil { m.emitDiags(diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, - Summary: "Resource type not found", + Summary: "Invalid resource type", Detail: fmt.Sprintf("Resource type %s not found in provider schema.", resource.Addr.Resource.Type), Subject: target.StackModuleConfig.SourceAddrRange.Ptr(), })) @@ -175,7 +177,7 @@ func (m *migration) search(resource addrs.AbsResourceInstance, resources map[str // 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 not found in mapping.", resource.Resource.String()))) + 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 } } @@ -207,14 +209,14 @@ func (m *migration) search(resource addrs.AbsResourceInstance, resources map[str }, diags } else { var diags tfdiags.Diagnostics - diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Module not found", fmt.Sprintf("Module %q not found in mapping.", resource.Module[0].Name))) + 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(resource *stackResource) (addrs.AbsProviderConfig, providers.Interface, tfdiags.Diagnostics) { +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, @@ -222,7 +224,7 @@ func (m *migration) getOwningProvider(resource *stackResource) (addrs.AbsProvide // the resource instance. moduleAddr := resource.AbsResourceInstance.Item.Module.Module() // the module address within the stack component's module configuration - providerConfig, diags := m.findProviderConfig(moduleAddr, resource.AbsResourceInstance.Item.Resource.Resource, resource.StackModuleConfig) + providerConfig, diags := m.findProviderConfig(source, moduleAddr, resource.AbsResourceInstance.Item.Resource.Resource, resource.StackModuleConfig, resource.AbsResourceInstance.Component) if diags.HasErrors() { return ret, nil, diags } @@ -266,7 +268,7 @@ func (m *migration) getOwningProvider(resource *stackResource) (addrs.AbsProvide 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.Item.Resource.String()), + 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 @@ -305,13 +307,13 @@ func (m *migration) getOwningProvider(resource *stackResource) (addrs.AbsProvide // findProviderConfig recursively searches through the stack module configuration to find the provider // that was used to create the resource instance. -func (m *migration) findProviderConfig(module addrs.Module, resource addrs.Resource, config *configs.Config) (addrs.LocalProviderConfig, tfdiags.Diagnostics) { +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, "Provider not found", fmt.Sprintf("Resource %q not found in root module.", resource.String()))) + 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 } @@ -320,13 +322,13 @@ func (m *migration) findProviderConfig(module addrs.Module, resource addrs.Resou next, ok := config.Children[module[0]] if !ok { - diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Provider not found", fmt.Sprintf("Module %q not found in root module children.", module[0]))) + 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(module[1:], resource, next) + provider, moreDiags := m.findProviderConfig(source, module[1:], resource, next, component) diags = diags.Append(moreDiags) if diags.HasErrors() { return addrs.LocalProviderConfig{}, diags @@ -334,7 +336,7 @@ func (m *migration) findProviderConfig(module addrs.Module, resource addrs.Resou call, ok := config.Module.ModuleCalls[module[0]] if !ok { - diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Provider not found", fmt.Sprintf("Module call %q not found in configuration.", module[0]))) + 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 } @@ -348,7 +350,7 @@ func (m *migration) findProviderConfig(module addrs.Module, resource addrs.Resou // Let's check the provider within the child module configuration. r := next.Module.ResourceByAddr(resource) if r == nil { - diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Provider not found", fmt.Sprintf("Resource %q not found in containing module.", resource.String()))) + diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Resource mapped to non-existent target", fmt.Sprintf("Could not migrate resource %q. Resource %q not found component %q.", source, resource.InModule(module), component))) return addrs.LocalProviderConfig{}, diags } return r.ProviderConfigAddr(), diags @@ -376,7 +378,7 @@ func (m *migration) loadConfig(resource *stackResource) tfdiags.Diagnostics { 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), + Detail: fmt.Sprintf("Module configuration for component %q not found.", instance.Item.Component.Name), Subject: component.SourceAddrRange.ToHCL().Ptr(), }) } diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/single-component/child/child.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/single-component/child/child.tf new file mode 100644 index 0000000000..d782352018 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/single-component/child/child.tf @@ -0,0 +1,24 @@ + +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +resource "testing_resource" "one" { + id = "one" + value = "one" +} + +resource "testing_resource" "two" { + id = "two" + value = "two" +} + +resource "testing_resource" "three" { + id = "three" + value = "three" +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/single-component/child/grandchild/main.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/single-component/child/grandchild/main.tf new file mode 100644 index 0000000000..8857ebb7c1 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/single-component/child/grandchild/main.tf @@ -0,0 +1,14 @@ + +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +resource "testing_resource" "one" { + id = "one" + value = "one" +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/single-component/single-component.tfcomponent.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/single-component/single-component.tfcomponent.hcl new file mode 100644 index 0000000000..5c25709a0e --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/single-component/single-component.tfcomponent.hcl @@ -0,0 +1,16 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "single" { + source = "./child" + + providers = { + testing = provider.testing.default + } +}