| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: BUSL-1.1 |
| |
| package stackmigrate |
| |
| import ( |
| stdcmp "cmp" |
| "encoding/json" |
| "fmt" |
| "path/filepath" |
| "strings" |
| "testing" |
| |
| "github.com/google/go-cmp/cmp" |
| "github.com/google/go-cmp/cmp/cmpopts" |
| "github.com/hashicorp/go-slug/sourceaddrs" |
| "github.com/hashicorp/go-slug/sourcebundle" |
| "github.com/hashicorp/hcl/v2" |
| "github.com/hashicorp/terraform/internal/addrs" |
| "github.com/hashicorp/terraform/internal/collections" |
| "github.com/hashicorp/terraform/internal/depsfile" |
| "github.com/hashicorp/terraform/internal/getproviders/providerreqs" |
| "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/stackplan" |
| stacks_testing_provider "github.com/hashicorp/terraform/internal/stacks/stackruntime/testing" |
| "github.com/hashicorp/terraform/internal/stacks/stackstate" |
| "github.com/hashicorp/terraform/internal/states" |
| "github.com/hashicorp/terraform/internal/tfdiags" |
| "github.com/zclconf/go-cty-debug/ctydebug" |
| "github.com/zclconf/go-cty/cty" |
| ) |
| |
| func TestMigrate_Module(t *testing.T) { |
| cfg := loadMainBundleConfigForTest(t, filepath.Join("with-single-input", "valid")) |
| |
| lock := depsfile.NewLocks() |
| lock.SetProvider( |
| addrs.NewDefaultProvider("testing"), |
| providerreqs.MustParseVersion("0.0.0"), |
| providerreqs.MustParseVersionConstraints("=0.0.0"), |
| providerreqs.PreferredHashes([]providerreqs.Hash{}), |
| ) |
| |
| state := states.BuildState(func(ss *states.SyncState) { |
| ss.SetOutputValue(addrs.AbsOutputValue{ |
| Module: addrs.RootModuleInstance, |
| OutputValue: addrs.OutputValue{Name: "output"}, |
| }, cty.StringVal("before"), false) |
| }) |
| rootModule := state.RootModule() |
| rootModule.SetResourceInstanceCurrent( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "testing_resource", |
| Name: "data", |
| }.Instance(addrs.NoKey), |
| &states.ResourceInstanceObjectSrc{ |
| Status: states.ObjectReady, |
| AttrsJSON: []byte(`{ |
| "id": "foo", |
| "value": "hello" |
| }`), |
| }, |
| mustDefaultRootProvider("testing"), |
| ) |
| rootModule.SetResourceInstanceDeposed( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "testing_resource", |
| Name: "data", |
| }.Instance(addrs.NoKey), |
| states.NewDeposedKey(), |
| &states.ResourceInstanceObjectSrc{ |
| Status: states.ObjectReady, |
| AttrsJSON: []byte(`{ |
| "id": "foo", |
| "value": "hello" |
| }`), |
| }, |
| mustDefaultRootProvider("testing"), |
| ) |
| mig := Migration{ |
| Providers: map[addrs.Provider]providers.Factory{ |
| addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { |
| return stacks_testing_provider.NewProvider(t), nil |
| }, |
| }, |
| PreviousState: state, |
| Config: cfg, |
| } |
| resources := map[string]string{ |
| "testing_resource.data": "self", |
| } |
| modules := map[string]string{} |
| |
| applied := []stackstate.AppliedChange{} |
| expected := []stackstate.AppliedChange{ |
| &stackstate.AppliedChangeResourceInstanceObject{ |
| ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), |
| NewStateSrc: &states.ResourceInstanceObjectSrc{ |
| AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ |
| "id": "foo", |
| "value": "hello", |
| }), |
| Status: states.ObjectReady, |
| Private: nil, |
| }, |
| ProviderConfigAddr: mustDefaultRootProvider("testing"), |
| Schema: stacks_testing_provider.TestingResourceSchema, |
| }, |
| &stackstate.AppliedChangeResourceInstanceObject{ |
| ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ |
| Component: mustAbsResourceInstanceObject("component.self.testing_resource.data").Component, |
| Item: addrs.AbsResourceInstanceObject{ |
| ResourceInstance: mustAbsResourceInstanceObject("component.self.testing_resource.data").Item.ResourceInstance, |
| DeposedKey: states.NewDeposedKey(), |
| }, |
| }, |
| NewStateSrc: &states.ResourceInstanceObjectSrc{ |
| AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ |
| "id": "foo", |
| "value": "hello", |
| }), |
| Status: states.ObjectReady, |
| Private: nil, |
| }, |
| ProviderConfigAddr: mustDefaultRootProvider("testing"), |
| Schema: stacks_testing_provider.TestingResourceSchema, |
| }, |
| &stackstate.AppliedChangeComponentInstance{ |
| ComponentAddr: mustAbsComponent("component.self"), |
| ComponentInstanceAddr: mustAbsComponentInstance("component.self"), |
| OutputValues: map[addrs.OutputValue]cty.Value{}, |
| InputVariables: map[addrs.InputVariable]cty.Value{ |
| {Name: "id"}: cty.DynamicVal, |
| {Name: "input"}: cty.DynamicVal, |
| }, |
| }, |
| } |
| |
| var expDiags, gotDiags tfdiags.Diagnostics |
| mig.Migrate(resources, modules, func(change stackstate.AppliedChange) { |
| applied = append(applied, change) |
| }, func(diagnostic tfdiags.Diagnostic) { |
| gotDiags = append(gotDiags, diagnostic) |
| }) |
| |
| if diff := cmp.Diff(expected, applied, changesCmpOpts, cmpopts.IgnoreFields( |
| addrs.AbsResourceInstanceObject{}, "DeposedKey", |
| )); diff != "" { |
| t.Fatalf("unexpected applied changes:\n%s", diff) |
| } |
| |
| if diff := cmp.Diff(expDiags, gotDiags); diff != "" { |
| t.Fatalf("unexpected diagnostics:\n%s", diff) |
| } |
| } |
| |
| func TestMigrate_RootResources(t *testing.T) { |
| cfg := loadMainBundleConfigForTest(t, filepath.Join("with-single-input", "valid")) |
| |
| lock := depsfile.NewLocks() |
| lock.SetProvider( |
| addrs.NewDefaultProvider("testing"), |
| providerreqs.MustParseVersion("0.0.0"), |
| providerreqs.MustParseVersionConstraints("=0.0.0"), |
| providerreqs.PreferredHashes([]providerreqs.Hash{}), |
| ) |
| |
| state := states.BuildState(func(ss *states.SyncState) { |
| ss.SetOutputValue(addrs.AbsOutputValue{ |
| Module: addrs.RootModuleInstance, |
| OutputValue: addrs.OutputValue{Name: "output"}, |
| }, cty.StringVal("before"), false) |
| }) |
| rootModule := state.RootModule() |
| rootModule.SetResourceInstanceCurrent( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "testing_resource", |
| Name: "data", |
| }.Instance(addrs.NoKey), |
| &states.ResourceInstanceObjectSrc{ |
| Status: states.ObjectReady, |
| AttrsJSON: []byte(`{ |
| "id": "foo", |
| "value": "hello" |
| }`), |
| }, |
| mustDefaultRootProvider("testing"), |
| ) |
| rootModule.SetResourceInstanceDeposed( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "testing_resource", |
| Name: "data", |
| }.Instance(addrs.NoKey), |
| states.NewDeposedKey(), |
| &states.ResourceInstanceObjectSrc{ |
| Status: states.ObjectReady, |
| AttrsJSON: []byte(`{ |
| "id": "foo", |
| "value": "hello" |
| }`), |
| }, |
| mustDefaultRootProvider("testing"), |
| ) |
| mig := Migration{ |
| Providers: map[addrs.Provider]providers.Factory{ |
| addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { |
| return stacks_testing_provider.NewProvider(t), nil |
| }, |
| }, |
| PreviousState: state, |
| Config: cfg, |
| } |
| resources := map[string]string{ |
| "testing_resource.data": "self", |
| } |
| modules := map[string]string{} |
| |
| applied := []stackstate.AppliedChange{} |
| expected := []stackstate.AppliedChange{ |
| &stackstate.AppliedChangeResourceInstanceObject{ |
| ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), |
| NewStateSrc: &states.ResourceInstanceObjectSrc{ |
| AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ |
| "id": "foo", |
| "value": "hello", |
| }), |
| Status: states.ObjectReady, |
| Private: nil, |
| }, |
| ProviderConfigAddr: mustDefaultRootProvider("testing"), |
| Schema: stacks_testing_provider.TestingResourceSchema, |
| }, |
| &stackstate.AppliedChangeResourceInstanceObject{ |
| ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ |
| Component: mustAbsResourceInstanceObject("component.self.testing_resource.data").Component, |
| Item: addrs.AbsResourceInstanceObject{ |
| ResourceInstance: mustAbsResourceInstanceObject("component.self.testing_resource.data").Item.ResourceInstance, |
| DeposedKey: states.NewDeposedKey(), |
| }, |
| }, |
| NewStateSrc: &states.ResourceInstanceObjectSrc{ |
| AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ |
| "id": "foo", |
| "value": "hello", |
| }), |
| Status: states.ObjectReady, |
| Private: nil, |
| }, |
| ProviderConfigAddr: mustDefaultRootProvider("testing"), |
| Schema: stacks_testing_provider.TestingResourceSchema, |
| }, |
| &stackstate.AppliedChangeComponentInstance{ |
| ComponentAddr: mustAbsComponent("component.self"), |
| ComponentInstanceAddr: mustAbsComponentInstance("component.self"), |
| OutputValues: map[addrs.OutputValue]cty.Value{}, |
| InputVariables: map[addrs.InputVariable]cty.Value{ |
| {Name: "id"}: cty.DynamicVal, |
| {Name: "input"}: cty.DynamicVal, |
| }, |
| }, |
| } |
| |
| var expDiags, gotDiags tfdiags.Diagnostics |
| mig.Migrate(resources, modules, func(change stackstate.AppliedChange) { |
| applied = append(applied, change) |
| }, func(diagnostic tfdiags.Diagnostic) { |
| gotDiags = append(gotDiags, diagnostic) |
| }) |
| |
| if diff := cmp.Diff(expected, applied, changesCmpOpts, cmpopts.IgnoreFields( |
| addrs.AbsResourceInstanceObject{}, "DeposedKey", |
| )); diff != "" { |
| t.Fatalf("unexpected applied changes:\n%s", diff) |
| } |
| |
| if diff := cmp.Diff(expDiags, gotDiags); diff != "" { |
| t.Fatalf("unexpected diagnostics:\n%s", diff) |
| } |
| } |
| |
| func TestMigrate_ComponentDependency(t *testing.T) { |
| cfg := loadMainBundleConfigForTest(t, filepath.Join("for-stacks-migrate", "with-dependency", "input-dependency")) |
| |
| lock := depsfile.NewLocks() |
| lock.SetProvider( |
| addrs.NewDefaultProvider("testing"), |
| providerreqs.MustParseVersion("0.0.0"), |
| providerreqs.MustParseVersionConstraints("=0.0.0"), |
| providerreqs.PreferredHashes([]providerreqs.Hash{}), |
| ) |
| |
| state := states.BuildState(func(ss *states.SyncState) { |
| ss.SetOutputValue(addrs.AbsOutputValue{ |
| Module: addrs.RootModuleInstance, |
| OutputValue: addrs.OutputValue{Name: "output"}, |
| }, cty.StringVal("before"), false) |
| }) |
| rootModule := state.RootModule() |
| rootModule.SetResourceInstanceCurrent( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "testing_resource", |
| Name: "data", |
| }.Instance(addrs.NoKey), |
| &states.ResourceInstanceObjectSrc{ |
| Status: states.ObjectReady, |
| AttrsJSON: []byte(`{ |
| "id": "foo", |
| "value": "hello" |
| }`), |
| }, |
| mustDefaultRootProvider("testing"), |
| ) |
| rootModule.SetResourceInstanceCurrent( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "testing_resource", |
| Name: "another", |
| }.Instance(addrs.IntKey(0)), |
| &states.ResourceInstanceObjectSrc{ |
| Status: states.ObjectReady, |
| AttrsJSON: []byte(`{ |
| "id": "foo", |
| "value": "hello" |
| }`), |
| }, |
| mustDefaultRootProvider("testing"), |
| ) |
| rootModule.SetResourceInstanceCurrent( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "testing_resource", |
| Name: "another", |
| }.Instance(addrs.IntKey(1)), |
| &states.ResourceInstanceObjectSrc{ |
| Status: states.ObjectReady, |
| AttrsJSON: []byte(`{ |
| "id": "foo", |
| "value": "hello" |
| }`), |
| }, |
| mustDefaultRootProvider("testing"), |
| ) |
| |
| mig := Migration{ |
| Providers: map[addrs.Provider]providers.Factory{ |
| addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { |
| return stacks_testing_provider.NewProvider(t), nil |
| }, |
| }, |
| PreviousState: state, |
| Config: cfg, |
| } |
| resources := map[string]string{ |
| "testing_resource.data": "parent", |
| "testing_resource.another": "child", |
| } |
| modules := map[string]string{} |
| |
| appliedResources := []*stackstate.AppliedChangeResourceInstanceObject{} |
| appliedComponents := []*stackstate.AppliedChangeComponentInstance{} |
| expectedResources := []*stackstate.AppliedChangeResourceInstanceObject{ |
| { |
| ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.data"), |
| NewStateSrc: &states.ResourceInstanceObjectSrc{ |
| AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ |
| "id": "foo", |
| "value": "hello", |
| }), |
| Status: states.ObjectReady, |
| Private: nil, |
| }, |
| ProviderConfigAddr: mustDefaultRootProvider("testing"), |
| Schema: stacks_testing_provider.TestingResourceSchema, |
| }, |
| { |
| ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.child.testing_resource.another[0]"), |
| NewStateSrc: &states.ResourceInstanceObjectSrc{ |
| AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ |
| "id": "foo", |
| "value": "hello", |
| }), |
| Status: states.ObjectReady, |
| Private: nil, |
| }, |
| ProviderConfigAddr: mustDefaultRootProvider("testing"), |
| Schema: stacks_testing_provider.TestingResourceSchema, |
| }, |
| { |
| ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.child.testing_resource.another[1]"), |
| NewStateSrc: &states.ResourceInstanceObjectSrc{ |
| AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ |
| "id": "foo", |
| "value": "hello", |
| }), |
| Status: states.ObjectReady, |
| Private: nil, |
| }, |
| ProviderConfigAddr: mustDefaultRootProvider("testing"), |
| Schema: stacks_testing_provider.TestingResourceSchema, |
| }, |
| } |
| expectedComponents := []*stackstate.AppliedChangeComponentInstance{ |
| { |
| ComponentAddr: mustAbsComponent("component.parent"), |
| ComponentInstanceAddr: mustAbsComponentInstance("component.parent"), |
| OutputValues: map[addrs.OutputValue]cty.Value{ |
| {Name: "id"}: cty.DynamicVal, |
| }, |
| InputVariables: map[addrs.InputVariable]cty.Value{ |
| {Name: "id"}: cty.DynamicVal, |
| {Name: "input"}: cty.DynamicVal, |
| }, |
| Dependents: collections.NewSet(mustAbsComponent("component.child")), |
| }, |
| { |
| ComponentAddr: mustAbsComponent("component.child"), |
| ComponentInstanceAddr: mustAbsComponentInstance("component.child"), |
| OutputValues: map[addrs.OutputValue]cty.Value{ |
| {Name: "id"}: cty.DynamicVal, |
| }, |
| InputVariables: map[addrs.InputVariable]cty.Value{ |
| {Name: "id"}: cty.DynamicVal, |
| {Name: "input"}: cty.DynamicVal, |
| }, |
| Dependencies: collections.NewSet(mustAbsComponent("component.parent")), |
| }, |
| } |
| |
| var expDiags, gotDiags tfdiags.Diagnostics |
| mig.Migrate(resources, modules, func(change stackstate.AppliedChange) { |
| switch c := change.(type) { |
| case *stackstate.AppliedChangeResourceInstanceObject: |
| appliedResources = append(appliedResources, c) |
| case *stackstate.AppliedChangeComponentInstance: |
| appliedComponents = append(appliedComponents, c) |
| } |
| }, func(diagnostic tfdiags.Diagnostic) { |
| gotDiags = append(gotDiags, diagnostic) |
| }) |
| |
| if diff := compareAppliedChanges(t, expectedResources, appliedResources, func(c *stackstate.AppliedChangeResourceInstanceObject) string { |
| return c.ResourceInstanceObjectAddr.String() |
| }); diff != "" { |
| t.Fatalf("unexpected applied resource changes:\n%s", diff) |
| } |
| |
| if diff := compareAppliedChanges(t, expectedComponents, appliedComponents, func(c *stackstate.AppliedChangeComponentInstance) string { |
| return c.ComponentAddr.String() |
| }); diff != "" { |
| t.Fatalf("unexpected applied component changes:\n%s", diff) |
| } |
| |
| if diff := cmp.Diff(expDiags, gotDiags); diff != "" { |
| t.Fatalf("unexpected diagnostics:\n%s", diff) |
| } |
| } |
| |
| func TestMigrateConfig_NestedModuleResources(t *testing.T) { |
| cfg := loadMainBundleConfigForTest(t, filepath.Join("for-stacks-migrate", "with-nested-module")) |
| |
| lock := depsfile.NewLocks() |
| lock.SetProvider( |
| addrs.NewDefaultProvider("testing"), |
| providerreqs.MustParseVersion("0.0.0"), |
| providerreqs.MustParseVersionConstraints("=0.0.0"), |
| providerreqs.PreferredHashes([]providerreqs.Hash{}), |
| ) |
| |
| state := states.BuildState(func(ss *states.SyncState) { |
| ss.SetOutputValue(addrs.AbsOutputValue{ |
| Module: addrs.RootModuleInstance, |
| OutputValue: addrs.OutputValue{Name: "output"}, |
| }, cty.StringVal("before"), false) |
| }) |
| rootModule := state.RootModule() |
| rootModule.SetResourceInstanceCurrent( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "testing_resource", |
| Name: "data", |
| }.Instance(addrs.NoKey), |
| &states.ResourceInstanceObjectSrc{ |
| Status: states.ObjectReady, |
| AttrsJSON: []byte(`{ |
| "id": "foo", |
| "value": "hello" |
| }`), |
| }, |
| mustDefaultRootProvider("testing"), |
| ) |
| rootModule.SetResourceInstanceCurrent( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "testing_resource", |
| Name: "another", |
| }.Instance(addrs.IntKey(0)), |
| &states.ResourceInstanceObjectSrc{ |
| Status: states.ObjectReady, |
| AttrsJSON: []byte(`{ |
| "id": "foo", |
| "value": "hello" |
| }`), |
| }, |
| mustDefaultRootProvider("testing"), |
| ) |
| rootModule.SetResourceInstanceCurrent( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "testing_resource", |
| Name: "another", |
| }.Instance(addrs.IntKey(1)), |
| &states.ResourceInstanceObjectSrc{ |
| Status: states.ObjectReady, |
| AttrsJSON: []byte(`{ |
| "id": "foo", |
| "value": "hello" |
| }`), |
| }, |
| mustDefaultRootProvider("testing"), |
| ) |
| |
| childModule := state.EnsureModule(addrs.RootModuleInstance.Child("child_mod", addrs.NoKey)) |
| childModule.SetResourceInstanceCurrent( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "testing_resource", |
| Name: "child_data", |
| }.Instance(addrs.NoKey), |
| &states.ResourceInstanceObjectSrc{ |
| Status: states.ObjectReady, |
| AttrsJSON: []byte(`{ |
| "id": "foo", |
| "value": "hello" |
| }`), |
| }, |
| mustDefaultRootProvider("testing"), |
| ) |
| childModule.SetResourceInstanceCurrent( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "testing_resource", |
| Name: "another_child_data", |
| }.Instance(addrs.IntKey(0)), |
| &states.ResourceInstanceObjectSrc{ |
| Status: states.ObjectReady, |
| AttrsJSON: []byte(`{ |
| "id": "foo", |
| "value": "hello" |
| }`), |
| }, |
| mustDefaultRootProvider("testing"), |
| ) |
| childModule.SetResourceInstanceCurrent( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "testing_resource", |
| Name: "another_child_data", |
| }.Instance(addrs.IntKey(1)), |
| &states.ResourceInstanceObjectSrc{ |
| Status: states.ObjectReady, |
| AttrsJSON: []byte(`{ |
| "id": "foo", |
| "value": "hello" |
| }`), |
| }, |
| mustDefaultRootProvider("testing"), |
| ) |
| |
| mig := Migration{ |
| Providers: map[addrs.Provider]providers.Factory{ |
| addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { |
| return stacks_testing_provider.NewProvider(t), nil |
| }, |
| }, |
| PreviousState: state, |
| Config: cfg, |
| } |
| resources := map[string]string{ |
| "testing_resource.data": "parent", |
| "testing_resource.another": "parent", |
| } |
| modules := map[string]string{ |
| "child_mod": "child", |
| } |
| |
| appliedResources := []*stackstate.AppliedChangeResourceInstanceObject{} |
| appliedComponents := []*stackstate.AppliedChangeComponentInstance{} |
| expectedResources := []*stackstate.AppliedChangeResourceInstanceObject{ |
| { |
| ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.data"), |
| NewStateSrc: &states.ResourceInstanceObjectSrc{ |
| AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ |
| "id": "foo", |
| "value": "hello", |
| }), |
| Status: states.ObjectReady, |
| Private: nil, |
| }, |
| ProviderConfigAddr: mustDefaultRootProvider("testing"), |
| Schema: stacks_testing_provider.TestingResourceSchema, |
| }, |
| { |
| ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.another[0]"), |
| NewStateSrc: &states.ResourceInstanceObjectSrc{ |
| AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ |
| "id": "foo", |
| "value": "hello", |
| }), |
| Status: states.ObjectReady, |
| Private: nil, |
| }, |
| ProviderConfigAddr: mustDefaultRootProvider("testing"), |
| Schema: stacks_testing_provider.TestingResourceSchema, |
| }, |
| { |
| ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.another[1]"), |
| NewStateSrc: &states.ResourceInstanceObjectSrc{ |
| AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ |
| "id": "foo", |
| "value": "hello", |
| }), |
| Status: states.ObjectReady, |
| Private: nil, |
| }, |
| ProviderConfigAddr: mustDefaultRootProvider("testing"), |
| Schema: stacks_testing_provider.TestingResourceSchema, |
| }, |
| { |
| ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.child.testing_resource.child_data"), |
| NewStateSrc: &states.ResourceInstanceObjectSrc{ |
| AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ |
| "id": "foo", |
| "value": "hello", |
| }), |
| Status: states.ObjectReady, |
| Private: nil, |
| }, |
| ProviderConfigAddr: mustDefaultRootProvider("testing"), |
| Schema: stacks_testing_provider.TestingResourceSchema, |
| }, |
| { |
| ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.child.testing_resource.another_child_data[0]"), |
| NewStateSrc: &states.ResourceInstanceObjectSrc{ |
| AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ |
| "id": "foo", |
| "value": "hello", |
| }), |
| Status: states.ObjectReady, |
| Private: nil, |
| }, |
| ProviderConfigAddr: mustDefaultRootProvider("testing"), |
| Schema: stacks_testing_provider.TestingResourceSchema, |
| }, |
| { |
| ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.child.testing_resource.another_child_data[1]"), |
| NewStateSrc: &states.ResourceInstanceObjectSrc{ |
| AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ |
| "id": "foo", |
| "value": "hello", |
| }), |
| Status: states.ObjectReady, |
| Private: nil, |
| }, |
| ProviderConfigAddr: mustDefaultRootProvider("testing"), |
| Schema: stacks_testing_provider.TestingResourceSchema, |
| }, |
| } |
| expectedComponents := []*stackstate.AppliedChangeComponentInstance{ |
| { |
| ComponentAddr: mustAbsComponent("component.parent"), |
| ComponentInstanceAddr: mustAbsComponentInstance("component.parent"), |
| OutputValues: map[addrs.OutputValue]cty.Value{ |
| {Name: "id"}: cty.DynamicVal, |
| }, |
| InputVariables: map[addrs.InputVariable]cty.Value{ |
| {Name: "id"}: cty.DynamicVal, |
| {Name: "input"}: cty.DynamicVal, |
| }, |
| Dependents: collections.NewSet(mustAbsComponent("component.child")), |
| }, |
| { |
| ComponentAddr: mustAbsComponent("component.child"), |
| ComponentInstanceAddr: mustAbsComponentInstance("component.child"), |
| OutputValues: map[addrs.OutputValue]cty.Value{ |
| {Name: "id"}: cty.DynamicVal, |
| }, |
| InputVariables: map[addrs.InputVariable]cty.Value{ |
| {Name: "id"}: cty.DynamicVal, |
| {Name: "input"}: cty.DynamicVal, |
| }, |
| Dependencies: collections.NewSet(mustAbsComponent("component.parent")), |
| }, |
| } |
| |
| var expDiags, gotDiags tfdiags.Diagnostics |
| mig.Migrate(resources, modules, func(change stackstate.AppliedChange) { |
| switch c := change.(type) { |
| case *stackstate.AppliedChangeResourceInstanceObject: |
| appliedResources = append(appliedResources, c) |
| case *stackstate.AppliedChangeComponentInstance: |
| appliedComponents = append(appliedComponents, c) |
| } |
| }, func(diagnostic tfdiags.Diagnostic) { |
| gotDiags = append(gotDiags, diagnostic) |
| }) |
| |
| if diff := cmp.Diff(expDiags, gotDiags); diff != "" { |
| t.Errorf("unexpected diagnostics:\n%s", diff) |
| } |
| |
| if diff := compareAppliedChanges(t, expectedResources, appliedResources, func(c *stackstate.AppliedChangeResourceInstanceObject) string { |
| return c.ResourceInstanceObjectAddr.String() |
| }); diff != "" { |
| t.Errorf("unexpected applied resource changes:\n%s", diff) |
| } |
| |
| if diff := compareAppliedChanges(t, expectedComponents, appliedComponents, func(c *stackstate.AppliedChangeComponentInstance) string { |
| return c.ComponentAddr.String() |
| }); diff != "" { |
| t.Errorf("unexpected applied component changes:\n%s", diff) |
| } |
| } |
| |
| func TestMigrateConfig_MissingConfigResource(t *testing.T) { |
| cfg := loadMainBundleConfigForTest(t, filepath.Join("for-stacks-migrate", "with-nested-module")) |
| |
| lock := depsfile.NewLocks() |
| lock.SetProvider( |
| addrs.NewDefaultProvider("testing"), |
| providerreqs.MustParseVersion("0.0.0"), |
| providerreqs.MustParseVersionConstraints("=0.0.0"), |
| providerreqs.PreferredHashes([]providerreqs.Hash{}), |
| ) |
| |
| state := states.BuildState(func(ss *states.SyncState) { |
| ss.SetOutputValue(addrs.AbsOutputValue{ |
| Module: addrs.RootModuleInstance, |
| OutputValue: addrs.OutputValue{Name: "output"}, |
| }, cty.StringVal("before"), false) |
| }) |
| rootModule := state.RootModule() |
| rootModule.SetResourceInstanceCurrent( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "testing_resource", |
| Name: "data", |
| }.Instance(addrs.NoKey), |
| &states.ResourceInstanceObjectSrc{ |
| Status: states.ObjectReady, |
| AttrsJSON: []byte(`{ |
| "id": "foo", |
| "value": "hello" |
| }`), |
| }, |
| mustDefaultRootProvider("testing"), |
| ) |
| rootModule.SetResourceInstanceCurrent( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "testing_resource", |
| Name: "another", |
| }.Instance(addrs.IntKey(0)), |
| &states.ResourceInstanceObjectSrc{ |
| Status: states.ObjectReady, |
| AttrsJSON: []byte(`{ |
| "id": "foo", |
| "value": "hello" |
| }`), |
| }, |
| mustDefaultRootProvider("testing"), |
| ) |
| rootModule.SetResourceInstanceCurrent( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "testing_resource", |
| Name: "another", |
| }.Instance(addrs.IntKey(1)), |
| &states.ResourceInstanceObjectSrc{ |
| Status: states.ObjectReady, |
| AttrsJSON: []byte(`{ |
| "id": "foo", |
| "value": "hello" |
| }`), |
| }, |
| mustDefaultRootProvider("testing"), |
| ) |
| |
| rootModule.SetResourceInstanceCurrent( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "testing_resource", |
| Name: "for_child", |
| }.Instance(addrs.NoKey), |
| &states.ResourceInstanceObjectSrc{ |
| Status: states.ObjectReady, |
| AttrsJSON: []byte(`{ |
| "id": "foo", |
| "value": "hello" |
| }`), |
| }, |
| mustDefaultRootProvider("testing"), |
| ) |
| |
| mig := Migration{ |
| Providers: map[addrs.Provider]providers.Factory{ |
| addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { |
| return stacks_testing_provider.NewProvider(t), nil |
| }, |
| }, |
| PreviousState: state, |
| Config: cfg, |
| } |
| resources := map[string]string{ |
| "testing_resource.data": "parent", |
| "testing_resource.another": "parent", |
| "testing_resource.for_child": "child", |
| } |
| modules := map[string]string{} |
| |
| appliedResources := []*stackstate.AppliedChangeResourceInstanceObject{} |
| appliedComponents := []*stackstate.AppliedChangeComponentInstance{} |
| |
| expectedResources := []*stackstate.AppliedChangeResourceInstanceObject{ |
| { |
| ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.data"), |
| NewStateSrc: &states.ResourceInstanceObjectSrc{ |
| AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ |
| "id": "foo", |
| "value": "hello", |
| }), |
| Status: states.ObjectReady, |
| Private: nil, |
| }, |
| ProviderConfigAddr: mustDefaultRootProvider("testing"), |
| Schema: stacks_testing_provider.TestingResourceSchema, |
| }, |
| { |
| ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.another[0]"), |
| NewStateSrc: &states.ResourceInstanceObjectSrc{ |
| AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ |
| "id": "foo", |
| "value": "hello", |
| }), |
| Status: states.ObjectReady, |
| Private: nil, |
| }, |
| ProviderConfigAddr: mustDefaultRootProvider("testing"), |
| Schema: stacks_testing_provider.TestingResourceSchema, |
| }, |
| { |
| ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.another[1]"), |
| NewStateSrc: &states.ResourceInstanceObjectSrc{ |
| AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ |
| "id": "foo", |
| "value": "hello", |
| }), |
| Status: states.ObjectReady, |
| Private: nil, |
| }, |
| ProviderConfigAddr: mustDefaultRootProvider("testing"), |
| Schema: stacks_testing_provider.TestingResourceSchema, |
| }, |
| } |
| |
| expectedComponents := []*stackstate.AppliedChangeComponentInstance{ |
| { |
| ComponentAddr: mustAbsComponent("component.parent"), |
| ComponentInstanceAddr: mustAbsComponentInstance("component.parent"), |
| OutputValues: map[addrs.OutputValue]cty.Value{ |
| {Name: "id"}: cty.DynamicVal, |
| }, |
| InputVariables: map[addrs.InputVariable]cty.Value{ |
| {Name: "id"}: cty.DynamicVal, |
| {Name: "input"}: cty.DynamicVal, |
| }, |
| Dependents: collections.NewSet(mustAbsComponent("component.child")), |
| }, |
| { |
| ComponentAddr: mustAbsComponent("component.child"), |
| ComponentInstanceAddr: mustAbsComponentInstance("component.child"), |
| OutputValues: map[addrs.OutputValue]cty.Value{ |
| {Name: "id"}: cty.DynamicVal, |
| }, |
| InputVariables: map[addrs.InputVariable]cty.Value{ |
| {Name: "id"}: cty.DynamicVal, |
| {Name: "input"}: cty.DynamicVal, |
| }, |
| Dependencies: collections.NewSet(mustAbsComponent("component.parent")), |
| }, |
| } |
| |
| var expDiags, gotDiags tfdiags.Diagnostics |
| // all components and resources should be migrated except for the missing "testing_resource.for_child" |
| expDiags = expDiags.Append(hcl.Diagnostics{ |
| { |
| Severity: hcl.DiagError, |
| Summary: "Provider not found", |
| Detail: "Resource \"testing_resource.for_child\" not found in root module.", |
| }, |
| }) |
| mig.Migrate(resources, modules, func(change stackstate.AppliedChange) { |
| switch c := change.(type) { |
| case *stackstate.AppliedChangeResourceInstanceObject: |
| appliedResources = append(appliedResources, c) |
| case *stackstate.AppliedChangeComponentInstance: |
| appliedComponents = append(appliedComponents, c) |
| } |
| }, func(diagnostic tfdiags.Diagnostic) { |
| gotDiags = append(gotDiags, diagnostic) |
| }) |
| |
| if diff := cmp.Diff(expDiags.ForRPC(), gotDiags.ForRPC(), tfdiags.DiagnosticComparer); diff != "" { |
| t.Fatalf("unexpected diagnostics:\n%s", diff) |
| } |
| |
| if diff := compareAppliedChanges(t, expectedResources, appliedResources, func(c *stackstate.AppliedChangeResourceInstanceObject) string { |
| return c.ResourceInstanceObjectAddr.String() |
| }); diff != "" { |
| t.Errorf("unexpected applied resource changes:\n%s", diff) |
| } |
| |
| if diff := compareAppliedChanges(t, expectedComponents, appliedComponents, func(c *stackstate.AppliedChangeComponentInstance) string { |
| return c.ComponentAddr.String() |
| }); diff != "" { |
| t.Errorf("unexpected applied component changes:\n%s", diff) |
| } |
| } |
| |
| func TestMigrateConfig_MissingMappingForStateResource(t *testing.T) { |
| cfg := loadMainBundleConfigForTest(t, filepath.Join("for-stacks-migrate", "with-nested-module")) |
| |
| lock := depsfile.NewLocks() |
| lock.SetProvider( |
| addrs.NewDefaultProvider("testing"), |
| providerreqs.MustParseVersion("0.0.0"), |
| providerreqs.MustParseVersionConstraints("=0.0.0"), |
| providerreqs.PreferredHashes([]providerreqs.Hash{}), |
| ) |
| |
| state := states.BuildState(func(ss *states.SyncState) { |
| ss.SetOutputValue(addrs.AbsOutputValue{ |
| Module: addrs.RootModuleInstance, |
| OutputValue: addrs.OutputValue{Name: "output"}, |
| }, cty.StringVal("before"), false) |
| }) |
| rootModule := state.RootModule() |
| rootModule.SetResourceInstanceCurrent( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "testing_resource", |
| Name: "data", |
| }.Instance(addrs.NoKey), |
| &states.ResourceInstanceObjectSrc{ |
| Status: states.ObjectReady, |
| AttrsJSON: []byte(`{ |
| "id": "foo", |
| "value": "hello" |
| }`), |
| }, |
| mustDefaultRootProvider("testing"), |
| ) |
| rootModule.SetResourceInstanceCurrent( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "testing_resource", |
| Name: "another", |
| }.Instance(addrs.IntKey(0)), |
| &states.ResourceInstanceObjectSrc{ |
| Status: states.ObjectReady, |
| AttrsJSON: []byte(`{ |
| "id": "foo", |
| "value": "hello" |
| }`), |
| }, |
| mustDefaultRootProvider("testing"), |
| ) |
| rootModule.SetResourceInstanceCurrent( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "testing_resource", |
| Name: "another", |
| }.Instance(addrs.IntKey(1)), |
| &states.ResourceInstanceObjectSrc{ |
| Status: states.ObjectReady, |
| AttrsJSON: []byte(`{ |
| "id": "foo", |
| "value": "hello" |
| }`), |
| }, |
| mustDefaultRootProvider("testing"), |
| ) |
| |
| rootModule.SetResourceInstanceCurrent( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "testing_resource", |
| Name: "for_child", |
| }.Instance(addrs.NoKey), |
| &states.ResourceInstanceObjectSrc{ |
| Status: states.ObjectReady, |
| AttrsJSON: []byte(`{ |
| "id": "foo", |
| "value": "hello" |
| }`), |
| }, |
| mustDefaultRootProvider("testing"), |
| ) |
| |
| mig := Migration{ |
| Providers: map[addrs.Provider]providers.Factory{ |
| addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { |
| return stacks_testing_provider.NewProvider(t), nil |
| }, |
| }, |
| PreviousState: state, |
| Config: cfg, |
| } |
| resources := map[string]string{ |
| "testing_resource.data": "parent", |
| "testing_resource.another": "parent", |
| } |
| modules := map[string]string{} |
| |
| appliedResources := []*stackstate.AppliedChangeResourceInstanceObject{} |
| appliedComponents := []*stackstate.AppliedChangeComponentInstance{} |
| |
| expectedResources := []*stackstate.AppliedChangeResourceInstanceObject{ |
| { |
| ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.data"), |
| NewStateSrc: &states.ResourceInstanceObjectSrc{ |
| AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ |
| "id": "foo", |
| "value": "hello", |
| }), |
| Status: states.ObjectReady, |
| Private: nil, |
| }, |
| ProviderConfigAddr: mustDefaultRootProvider("testing"), |
| Schema: stacks_testing_provider.TestingResourceSchema, |
| }, |
| { |
| ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.another[0]"), |
| NewStateSrc: &states.ResourceInstanceObjectSrc{ |
| AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ |
| "id": "foo", |
| "value": "hello", |
| }), |
| Status: states.ObjectReady, |
| Private: nil, |
| }, |
| ProviderConfigAddr: mustDefaultRootProvider("testing"), |
| Schema: stacks_testing_provider.TestingResourceSchema, |
| }, |
| { |
| ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.another[1]"), |
| NewStateSrc: &states.ResourceInstanceObjectSrc{ |
| AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ |
| "id": "foo", |
| "value": "hello", |
| }), |
| Status: states.ObjectReady, |
| Private: nil, |
| }, |
| ProviderConfigAddr: mustDefaultRootProvider("testing"), |
| Schema: stacks_testing_provider.TestingResourceSchema, |
| }, |
| } |
| |
| expectedComponents := []*stackstate.AppliedChangeComponentInstance{ |
| // this component has a dependent "child", but that other component |
| // is not present in the modules mapping, so it is not included here |
| { |
| ComponentAddr: mustAbsComponent("component.parent"), |
| ComponentInstanceAddr: mustAbsComponentInstance("component.parent"), |
| OutputValues: map[addrs.OutputValue]cty.Value{ |
| {Name: "id"}: cty.DynamicVal, |
| }, |
| InputVariables: map[addrs.InputVariable]cty.Value{ |
| {Name: "id"}: cty.DynamicVal, |
| {Name: "input"}: cty.DynamicVal, |
| }, |
| }, |
| } |
| |
| var expDiags, gotDiags tfdiags.Diagnostics |
| expDiags = expDiags.Append(hcl.Diagnostics{ |
| { |
| Severity: hcl.DiagError, |
| Summary: "Resource not found", |
| Detail: "Resource \"testing_resource.for_child\" not found in mapping.", |
| }, |
| }) |
| mig.Migrate(resources, modules, func(change stackstate.AppliedChange) { |
| switch c := change.(type) { |
| case *stackstate.AppliedChangeResourceInstanceObject: |
| appliedResources = append(appliedResources, c) |
| case *stackstate.AppliedChangeComponentInstance: |
| appliedComponents = append(appliedComponents, c) |
| } |
| }, func(diagnostic tfdiags.Diagnostic) { |
| gotDiags = append(gotDiags, diagnostic) |
| }) |
| |
| if diff := cmp.Diff(expDiags.ForRPC(), gotDiags.ForRPC(), tfdiags.DiagnosticComparer); diff != "" { |
| t.Fatalf("unexpected diagnostics:\n%s", diff) |
| } |
| |
| if diff := compareAppliedChanges(t, expectedResources, appliedResources, func(c *stackstate.AppliedChangeResourceInstanceObject) string { |
| return c.ResourceInstanceObjectAddr.String() |
| }); diff != "" { |
| t.Errorf("unexpected applied resource changes:\n%s", diff) |
| } |
| |
| if diff := compareAppliedChanges(t, expectedComponents, appliedComponents, func(c *stackstate.AppliedChangeComponentInstance) string { |
| return c.ComponentAddr.String() |
| }); diff != "" { |
| t.Errorf("unexpected applied component changes:\n%s", diff) |
| } |
| } |
| |
| func TestMigrateConfigDependsOn(t *testing.T) { |
| cfg := loadMainBundleConfigForTest(t, filepath.Join("for-stacks-migrate", "with-depends-on")) |
| |
| lock := depsfile.NewLocks() |
| lock.SetProvider( |
| addrs.NewDefaultProvider("testing"), |
| providerreqs.MustParseVersion("0.0.0"), |
| providerreqs.MustParseVersionConstraints("=0.0.0"), |
| providerreqs.PreferredHashes([]providerreqs.Hash{}), |
| ) |
| |
| state := states.BuildState(func(ss *states.SyncState) { |
| ss.SetOutputValue(addrs.AbsOutputValue{ |
| Module: addrs.RootModuleInstance, |
| OutputValue: addrs.OutputValue{Name: "output"}, |
| }, cty.StringVal("before"), false) |
| }) |
| |
| rootModule := state.RootModule() |
| rootModule.SetResourceInstanceCurrent( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "testing_resource", |
| Name: "data", |
| }.Instance(addrs.NoKey), |
| &states.ResourceInstanceObjectSrc{ |
| Status: states.ObjectReady, |
| AttrsJSON: []byte(`{ |
| "id": "foo", |
| "value": "depends_test" |
| }`), |
| }, |
| mustDefaultRootProvider("testing"), |
| ) |
| rootModule.SetResourceInstanceCurrent( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "testing_resource", |
| Name: "second", |
| }.Instance(addrs.NoKey), |
| &states.ResourceInstanceObjectSrc{ |
| Status: states.ObjectReady, |
| AttrsJSON: []byte(`{ |
| "id": "foo", |
| "value": "depends_test" |
| }`), |
| }, |
| mustDefaultRootProvider("testing"), |
| ) |
| rootModule.SetResourceInstanceCurrent( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "testing_resource", |
| Name: "third", |
| }.Instance(addrs.NoKey), |
| &states.ResourceInstanceObjectSrc{ |
| Status: states.ObjectReady, |
| AttrsJSON: []byte(`{ |
| "id": "foo", |
| "value": "depends_test" |
| }`), |
| }, |
| mustDefaultRootProvider("testing"), |
| ) |
| |
| mig := Migration{ |
| Providers: map[addrs.Provider]providers.Factory{ |
| addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { |
| return stacks_testing_provider.NewProvider(t), nil |
| }, |
| }, |
| PreviousState: state, |
| Config: cfg, |
| } |
| |
| resources := map[string]string{ |
| "testing_resource.data": "component.first", |
| "testing_resource.second": "component.second", |
| "testing_resource.third": "component.second", |
| } |
| modules := map[string]string{} |
| |
| appliedResources := []*stackstate.AppliedChangeResourceInstanceObject{} |
| appliedComponents := []*stackstate.AppliedChangeComponentInstance{} |
| expectedComponents := []*stackstate.AppliedChangeComponentInstance{ |
| // component.first depends on component.second |
| { |
| ComponentAddr: mustAbsComponent("component.first"), |
| ComponentInstanceAddr: mustAbsComponentInstance("component.first"), |
| |
| InputVariables: map[addrs.InputVariable]cty.Value{ |
| {Name: "input"}: cty.DynamicVal, |
| {Name: "id"}: cty.DynamicVal, |
| }, |
| Dependents: collections.NewSet(mustAbsComponent("component.second")), |
| }, |
| { |
| ComponentAddr: mustAbsComponent("component.second"), |
| ComponentInstanceAddr: mustAbsComponentInstance("component.second"), |
| InputVariables: map[addrs.InputVariable]cty.Value{ |
| {Name: "input"}: cty.DynamicVal, |
| {Name: "id"}: cty.DynamicVal, |
| }, |
| Dependencies: collections.NewSet(mustAbsComponent("component.first")), |
| }, |
| } |
| |
| expectedResources := []*stackstate.AppliedChangeResourceInstanceObject{ |
| { |
| ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.first.testing_resource.data"), |
| NewStateSrc: &states.ResourceInstanceObjectSrc{ |
| AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ |
| "id": "foo", |
| "value": "depends_test", |
| }), |
| Status: states.ObjectReady, |
| Private: nil, |
| }, |
| ProviderConfigAddr: mustDefaultRootProvider("testing"), |
| Schema: stacks_testing_provider.TestingResourceSchema, |
| }, |
| { |
| ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.second.testing_resource.second"), |
| NewStateSrc: &states.ResourceInstanceObjectSrc{ |
| AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ |
| "id": "foo", |
| "value": "depends_test", |
| }), |
| Status: states.ObjectReady, |
| Private: nil, |
| }, |
| ProviderConfigAddr: mustDefaultRootProvider("testing"), |
| Schema: stacks_testing_provider.TestingResourceSchema, |
| }, |
| { |
| ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.second.testing_resource.third"), |
| NewStateSrc: &states.ResourceInstanceObjectSrc{ |
| AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ |
| "id": "foo", |
| "value": "depends_test", |
| }), |
| Status: states.ObjectReady, |
| Private: nil, |
| }, |
| ProviderConfigAddr: mustDefaultRootProvider("testing"), |
| Schema: stacks_testing_provider.TestingResourceSchema, |
| }, |
| } |
| |
| var expDiags, gotDiags tfdiags.Diagnostics |
| mig.Migrate(resources, modules, func(change stackstate.AppliedChange) { |
| switch c := change.(type) { |
| case *stackstate.AppliedChangeResourceInstanceObject: |
| appliedResources = append(appliedResources, c) |
| case *stackstate.AppliedChangeComponentInstance: |
| appliedComponents = append(appliedComponents, c) |
| } |
| }, func(diagnostic tfdiags.Diagnostic) { |
| gotDiags = append(gotDiags, diagnostic) |
| }) |
| |
| if diff := compareAppliedChanges(t, expectedComponents, appliedComponents, func(c *stackstate.AppliedChangeComponentInstance) string { |
| return c.ComponentAddr.String() |
| }); diff != "" { |
| t.Fatalf("unexpected applied component changes:\n%s", diff) |
| } |
| |
| if diff := compareAppliedChanges(t, expectedResources, appliedResources, func(c *stackstate.AppliedChangeResourceInstanceObject) string { |
| return c.ResourceInstanceObjectAddr.String() |
| }); diff != "" { |
| t.Fatalf("unexpected applied resource changes:\n%s", diff) |
| } |
| |
| if diff := cmp.Diff(expDiags, gotDiags); diff != "" { |
| t.Fatalf("unexpected diagnostics:\n%s", diff) |
| } |
| } |
| |
| func TestMigrate_UnsupportedComponentRef(t *testing.T) { |
| cfg := loadMainBundleConfigForTest(t, filepath.Join("for-stacks-migrate", "with-depends-on")) |
| |
| lock := depsfile.NewLocks() |
| lock.SetProvider( |
| addrs.NewDefaultProvider("testing"), |
| providerreqs.MustParseVersion("0.0.0"), |
| providerreqs.MustParseVersionConstraints("=0.0.0"), |
| providerreqs.PreferredHashes([]providerreqs.Hash{}), |
| ) |
| |
| state := states.BuildState(func(ss *states.SyncState) { |
| ss.SetOutputValue(addrs.AbsOutputValue{ |
| Module: addrs.RootModuleInstance, |
| OutputValue: addrs.OutputValue{Name: "output"}, |
| }, cty.StringVal("before"), false) |
| }) |
| |
| rootModule := state.RootModule() |
| rootModule.SetResourceInstanceCurrent( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "testing_resource", |
| Name: "data", |
| }.Instance(addrs.NoKey), |
| &states.ResourceInstanceObjectSrc{ |
| Status: states.ObjectReady, |
| AttrsJSON: []byte(`{ |
| "id": "foo", |
| "value": "depends_test" |
| }`), |
| }, |
| mustDefaultRootProvider("testing"), |
| ) |
| rootModule.SetResourceInstanceCurrent( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "testing_resource", |
| Name: "second", |
| }.Instance(addrs.NoKey), |
| &states.ResourceInstanceObjectSrc{ |
| Status: states.ObjectReady, |
| AttrsJSON: []byte(`{ |
| "id": "foo", |
| "value": "depends_test" |
| }`), |
| }, |
| mustDefaultRootProvider("testing"), |
| ) |
| rootModule.SetResourceInstanceCurrent( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "testing_resource", |
| Name: "third", |
| }.Instance(addrs.NoKey), |
| &states.ResourceInstanceObjectSrc{ |
| Status: states.ObjectReady, |
| AttrsJSON: []byte(`{ |
| "id": "foo", |
| "value": "depends_test" |
| }`), |
| }, |
| mustDefaultRootProvider("testing"), |
| ) |
| |
| mig := Migration{ |
| Providers: map[addrs.Provider]providers.Factory{ |
| addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { |
| return stacks_testing_provider.NewProvider(t), nil |
| }, |
| }, |
| PreviousState: state, |
| Config: cfg, |
| } |
| |
| resources := map[string]string{ |
| "testing_resource.data": "component.first", |
| "testing_resource.second": "component.second", |
| "testing_resource.third": "stack.embedded.component.self", |
| } |
| modules := map[string]string{} |
| |
| appliedResources := []*stackstate.AppliedChangeResourceInstanceObject{} |
| appliedComponents := []*stackstate.AppliedChangeComponentInstance{} |
| expectedComponents := []*stackstate.AppliedChangeComponentInstance{ |
| { |
| ComponentAddr: mustAbsComponent("component.first"), |
| ComponentInstanceAddr: mustAbsComponentInstance("component.first"), |
| |
| InputVariables: map[addrs.InputVariable]cty.Value{ |
| {Name: "input"}: cty.DynamicVal, |
| {Name: "id"}: cty.DynamicVal, |
| }, |
| Dependents: collections.NewSet(mustAbsComponent("component.second")), |
| }, |
| { |
| ComponentAddr: mustAbsComponent("component.second"), |
| ComponentInstanceAddr: mustAbsComponentInstance("component.second"), |
| InputVariables: map[addrs.InputVariable]cty.Value{ |
| {Name: "input"}: cty.DynamicVal, |
| {Name: "id"}: cty.DynamicVal, |
| }, |
| Dependencies: collections.NewSet(mustAbsComponent("component.first")), |
| }, |
| } |
| |
| expectedResources := []*stackstate.AppliedChangeResourceInstanceObject{ |
| { |
| ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.first.testing_resource.data"), |
| NewStateSrc: &states.ResourceInstanceObjectSrc{ |
| AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ |
| "id": "foo", |
| "value": "depends_test", |
| }), |
| Status: states.ObjectReady, |
| Private: nil, |
| }, |
| ProviderConfigAddr: mustDefaultRootProvider("testing"), |
| Schema: stacks_testing_provider.TestingResourceSchema, |
| }, |
| { |
| ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.second.testing_resource.second"), |
| NewStateSrc: &states.ResourceInstanceObjectSrc{ |
| AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ |
| "id": "foo", |
| "value": "depends_test", |
| }), |
| Status: states.ObjectReady, |
| Private: nil, |
| }, |
| ProviderConfigAddr: mustDefaultRootProvider("testing"), |
| Schema: stacks_testing_provider.TestingResourceSchema, |
| }, |
| } |
| |
| var gotDiags tfdiags.Diagnostics |
| expDiags := tfdiags.Diagnostics{}.Append(hcl.Diagnostics{ |
| { |
| Severity: hcl.DiagError, |
| Summary: "Invalid component instance", |
| Detail: "Only root component instances are allowed, got \"stack.embedded.component.self\"", |
| }, |
| }) |
| |
| mig.Migrate(resources, modules, func(change stackstate.AppliedChange) { |
| switch c := change.(type) { |
| case *stackstate.AppliedChangeResourceInstanceObject: |
| appliedResources = append(appliedResources, c) |
| case *stackstate.AppliedChangeComponentInstance: |
| appliedComponents = append(appliedComponents, c) |
| } |
| }, func(diagnostic tfdiags.Diagnostic) { |
| gotDiags = append(gotDiags, diagnostic) |
| }) |
| |
| if diff := compareAppliedChanges(t, expectedComponents, appliedComponents, func(c *stackstate.AppliedChangeComponentInstance) string { |
| return c.ComponentAddr.String() |
| }); diff != "" { |
| t.Fatalf("unexpected applied component changes:\n%s", diff) |
| } |
| |
| if diff := compareAppliedChanges(t, expectedResources, appliedResources, func(c *stackstate.AppliedChangeResourceInstanceObject) string { |
| return c.ResourceInstanceObjectAddr.String() |
| }); diff != "" { |
| t.Fatalf("unexpected applied resource changes:\n%s", diff) |
| } |
| |
| if diff := cmp.Diff(expDiags.ForRPC(), gotDiags.ForRPC(), tfdiags.DiagnosticComparer); diff != "" { |
| t.Fatalf("unexpected diagnostics:\n%s", diff) |
| } |
| } |
| |
| func compareAppliedChanges[A stackstate.AppliedChange, U stdcmp.Ordered](t *testing.T, expected, actual []A, cb func(A) U) string { |
| t.Helper() |
| |
| if len(expected) != len(actual) { |
| t.Fatalf("expected %d changes, got %d", len(expected), len(actual)) |
| } |
| |
| _exp := make([]U, len(expected)) |
| _act := make([]U, len(actual)) |
| mp_exp := make(map[U]A) |
| mp_act := make(map[U]A) |
| |
| for i, exp := range expected { |
| _exp[i] = cb(exp) |
| _act[i] = cb(actual[i]) |
| mp_exp[_exp[i]] = exp |
| mp_act[_act[i]] = actual[i] |
| } |
| |
| sorter := cmpopts.SortMaps(func(a, b U) bool { |
| return a < b |
| }) |
| |
| return cmp.Diff(mp_exp, mp_act, sorter, changesCmpOpts, cmpopts.EquateEmpty()) |
| } |
| |
| func cmpJSONMap() cmp.Option { |
| return cmp.FilterValues(func(x, y interface{}) bool { |
| _, okX := x.([]uint8) |
| _, okY := y.([]uint8) |
| return okX && okY |
| }, cmp.Comparer(func(x, y interface{}) bool { |
| var xJSON, yJSON map[string]interface{} |
| err := json.Unmarshal(x.([]uint8), &xJSON) |
| if err != nil { |
| return false |
| } |
| err = json.Unmarshal(y.([]uint8), &yJSON) |
| if err != nil { |
| return false |
| } |
| |
| return cmp.Equal(xJSON, yJSON) |
| })) |
| } |
| |
| var changesCmpOpts = cmp.Options{ |
| ctydebug.CmpOptions, |
| cmpCollectionsSet, |
| cmpopts.IgnoreUnexported(addrs.InputVariable{}), |
| cmpopts.IgnoreUnexported(states.ResourceInstanceObjectSrc{}), |
| cmpJSONMap(), |
| cmpopts.IgnoreFields(states.ResourceInstanceObjectSrc{}, "Private"), |
| cmpopts.IgnoreFields(states.ResourceInstanceObjectSrc{}, "IdentityJSON"), |
| } |
| |
| var cmpCollectionsSet = cmp.Comparer(func(x, y collections.Set[stackaddrs.AbsComponent]) bool { |
| if x.Len() != y.Len() { |
| return false |
| } |
| |
| for v := range x.All() { |
| if !y.Has(v) { |
| return false |
| } |
| } |
| |
| return true |
| }) |
| |
| func TestMigrateConfigWithNoChanges(t *testing.T) { |
| |
| } |
| |
| // collectPlanOutput consumes the two output channels emitting results from |
| // a call to [Plan], and collects all of the data written to them before |
| // returning once changesCh has been closed by the sender to indicate that |
| // the planning process is complete. |
| func collectPlanOutput(changesCh <-chan stackplan.PlannedChange, diagsCh <-chan tfdiags.Diagnostic) ([]stackplan.PlannedChange, tfdiags.Diagnostics) { |
| var changes []stackplan.PlannedChange |
| var diags tfdiags.Diagnostics |
| |
| for { |
| select { |
| case change, ok := <-changesCh: |
| if !ok { |
| // The plan operation is complete but we might still have |
| // some buffered diagnostics to consume. |
| if diagsCh != nil { |
| for diag := range diagsCh { |
| diags = append(diags, diag) |
| } |
| } |
| return changes, diags |
| } |
| changes = append(changes, change) |
| case diag, ok := <-diagsCh: |
| if !ok { |
| // no more diagnostics to read |
| diagsCh = nil |
| continue |
| } |
| diags = append(diags, diag) |
| } |
| } |
| } |
| |
| func mustMarshalJSONAttrs(attrs map[string]interface{}) []byte { |
| jsonAttrs, err := json.Marshal(attrs) |
| if err != nil { |
| panic(err) |
| } |
| return jsonAttrs |
| } |
| |
| func mustDefaultRootProvider(provider string) addrs.AbsProviderConfig { |
| return addrs.AbsProviderConfig{ |
| Module: addrs.RootModule, |
| Provider: addrs.NewDefaultProvider(provider), |
| } |
| } |
| |
| func mustAbsResourceInstance(addr string) addrs.AbsResourceInstance { |
| ret, diags := addrs.ParseAbsResourceInstanceStr(addr) |
| if len(diags) > 0 { |
| panic(fmt.Sprintf("failed to parse resource instance address %q: %s", addr, diags)) |
| } |
| return ret |
| } |
| |
| func mustAbsResourceInstanceObject(addr string) stackaddrs.AbsResourceInstanceObject { |
| ret, diags := stackaddrs.ParseAbsResourceInstanceObjectStr(addr) |
| if len(diags) > 0 { |
| panic(fmt.Sprintf("failed to parse resource instance object address %q: %s", addr, diags)) |
| } |
| return ret |
| } |
| |
| func mustAbsResourceInstanceObjectPtr(addr string) *stackaddrs.AbsResourceInstanceObject { |
| ret := mustAbsResourceInstanceObject(addr) |
| return &ret |
| } |
| |
| func mustAbsComponentInstance(addr string) stackaddrs.AbsComponentInstance { |
| ret, diags := stackaddrs.ParsePartialComponentInstanceStr(addr) |
| if len(diags) > 0 { |
| panic(fmt.Sprintf("failed to parse component instance address %q: %s", addr, diags)) |
| } |
| return ret |
| } |
| |
| func mustAbsComponent(addr string) stackaddrs.AbsComponent { |
| ret, diags := stackaddrs.ParsePartialComponentInstanceStr(addr) |
| if len(diags) > 0 { |
| panic(fmt.Sprintf("failed to parse component instance address %q: %s", addr, diags)) |
| } |
| return stackaddrs.AbsComponent{ |
| Stack: ret.Stack, |
| Item: ret.Item.Component, |
| } |
| } |
| |
| // TODO: Perhaps export this from helper_test instead |
| func loadMainBundleConfigForTest(t *testing.T, dirName string) *stackconfig.Config { |
| t.Helper() |
| fullSourceAddr := mainBundleSourceAddrStr(dirName) |
| return loadConfigForTest(t, "../stackruntime/testdata/mainbundle", fullSourceAddr) |
| } |
| |
| func mainBundleSourceAddrStr(dirName string) string { |
| return "git::https://example.com/test.git//" + dirName |
| } |
| |
| // loadConfigForTest is a test helper that tries to open bundleRoot as a |
| // source bundle, and then if successful tries to load the given source address |
| // from it as a stack configuration. If any part of the operation fails then |
| // it halts execution of the test and doesn't return. |
| func loadConfigForTest(t *testing.T, bundleRoot string, configSourceAddr string) *stackconfig.Config { |
| t.Helper() |
| sources, err := sourcebundle.OpenDir(bundleRoot) |
| if err != nil { |
| t.Fatalf("cannot load source bundle: %s", err) |
| } |
| |
| // We force using remote source addresses here because that avoids |
| // us having to deal with the extra version constraints argument |
| // that registry sources require. Exactly what source address type |
| // we use isn't relevant for tests in this package, since it's |
| // the sourcebundle package's responsibility to make sure its |
| // abstraction works for all of the source types. |
| sourceAddr, err := sourceaddrs.ParseRemoteSource(configSourceAddr) |
| if err != nil { |
| t.Fatalf("invalid config source address: %s", err) |
| } |
| |
| cfg, diags := stackconfig.LoadConfigDir(sourceAddr, sources) |
| reportDiagnosticsForTest(t, diags) |
| return cfg |
| } |
| |
| // reportDiagnosticsForTest creates a test log entry for every diagnostic in |
| // the given diags, and halts the test if any of them are error diagnostics. |
| func reportDiagnosticsForTest(t *testing.T, diags tfdiags.Diagnostics) { |
| t.Helper() |
| for _, diag := range diags { |
| var b strings.Builder |
| desc := diag.Description() |
| locs := diag.Source() |
| |
| switch sev := diag.Severity(); sev { |
| case tfdiags.Error: |
| b.WriteString("Error: ") |
| case tfdiags.Warning: |
| b.WriteString("Warning: ") |
| default: |
| t.Errorf("unsupported diagnostic type %s", sev) |
| } |
| b.WriteString(desc.Summary) |
| if desc.Address != "" { |
| b.WriteString("\nwith ") |
| b.WriteString(desc.Summary) |
| } |
| if locs.Subject != nil { |
| b.WriteString("\nat ") |
| b.WriteString(locs.Subject.StartString()) |
| } |
| if desc.Detail != "" { |
| b.WriteString("\n\n") |
| b.WriteString(desc.Detail) |
| } |
| t.Log(b.String()) |
| } |
| if diags.HasErrors() { |
| t.FailNow() |
| } |
| } |