// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package stackruntime import ( "context" "encoding/json" "fmt" "path" "path/filepath" "sort" "strings" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty-debug/ctydebug" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/checks" "github.com/hashicorp/terraform/internal/depsfile" "github.com/hashicorp/terraform/internal/getproviders/providerreqs" "github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks" "github.com/hashicorp/terraform/internal/addrs" terraformProvider "github.com/hashicorp/terraform/internal/builtin/providers/terraform" "github.com/hashicorp/terraform/internal/collections" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" default_testing_provider "github.com/hashicorp/terraform/internal/providers/testing" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" "github.com/hashicorp/terraform/internal/stacks/stackplan" "github.com/hashicorp/terraform/internal/stacks/stackruntime/internal/stackeval" 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/hashicorp/terraform/version" ) // TestPlan_valid runs the same set of configurations as TestValidate_valid. // // Plan should execute the same set of validations as validate, so we expect // all of the following to be valid for both plan and validate. // // We also want to make sure the static and dynamic evaluations are not // returning duplicate / conflicting diagnostics. This test will tell us if // either plan or validate is reporting diagnostics the others are missing. func TestPlan_valid(t *testing.T) { for name, tc := range validConfigurations { t.Run(name, func(t *testing.T) { if tc.skip { // We've added this test before the implementation was ready. t.SkipNow() } ctx := context.Background() lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), providerreqs.MustParseVersion("0.0.0"), providerreqs.MustParseVersionConstraints("=0.0.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) lock.SetProvider( addrs.NewDefaultProvider("other"), providerreqs.MustParseVersion("0.0.0"), providerreqs.MustParseVersionConstraints("=0.0.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") if err != nil { t.Fatal(err) } testContext := TestContext{ config: loadMainBundleConfigForTest(t, name), providers: map[addrs.Provider]providers.Factory{ // We support both hashicorp/testing and // terraform.io/builtin/testing as providers. This lets us // test the provider aliasing feature. Both providers // support the same set of resources and data sources. addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, addrs.NewBuiltInProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, // We also support an "other" provider out of the box to // test the provider aliasing feature. addrs.NewDefaultProvider("other"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, dependencyLocks: *lock, timestamp: &fakePlanTimestamp, } cycle := TestCycle{ planInputs: tc.planInputVars, wantPlannedChanges: nil, // don't care about the planned changes in this test. wantPlannedDiags: nil, // should return no diagnostics. } testContext.Plan(t, ctx, nil, cycle) }) } } // TestPlan_invalid runs the same set of configurations as TestValidate_invalid. // // Plan should execute the same set of validations as validate, so we expect // all of the following to be invalid for both plan and validate. // // We also want to make sure the static and dynamic evaluations are not // returning duplicate / conflicting diagnostics. This test will tell us if // either plan or validate is reporting diagnostics the others are missing. // // The dynamic validation that happens during the plan *might* introduce // additional diagnostics that are not present in the static validation. These // should be added manually into this function. func TestPlan_invalid(t *testing.T) { for name, tc := range invalidConfigurations { t.Run(name, func(t *testing.T) { if tc.skip { // We've added this test before the implementation was ready. t.SkipNow() } ctx := context.Background() lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), providerreqs.MustParseVersion("0.0.0"), providerreqs.MustParseVersionConstraints("=0.0.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") if err != nil { t.Fatal(err) } testContext := TestContext{ config: loadMainBundleConfigForTest(t, name), providers: map[addrs.Provider]providers.Factory{ // We support both hashicorp/testing and // terraform.io/builtin/testing as providers. This lets us // test the provider aliasing feature. Both providers // support the same set of resources and data sources. addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, addrs.NewBuiltInProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, dependencyLocks: *lock, timestamp: &fakePlanTimestamp, } cycle := TestCycle{ planInputs: tc.planInputVars, wantPlannedChanges: nil, // don't care about the planned changes in this test. wantPlannedDiags: tc.diags(), } testContext.Plan(t, ctx, nil, cycle) }) } } // TestPlan uses a generic framework for running plan integration tests // against Stacks. Generally, new tests should be added into this function // rather than copying the large amount of duplicate code from the other // tests in this file. // // If you are editing other tests in this file, please consider moving them // into this test function so they can reuse the shared setup and boilerplate // code managing the boring parts of the test. func TestPlan(t *testing.T) { fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") if err != nil { t.Fatal(err) } tcs := map[string]struct { path string state *stackstate.State store *stacks_testing_provider.ResourceStore cycle TestCycle }{ "empty-destroy-with-data-source": { path: path.Join("with-data-source", "dependent"), cycle: TestCycle{ planMode: plans.DestroyMode, planInputs: map[string]cty.Value{ "id": cty.StringVal("foo"), }, wantPlannedChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.data"), PlanApplyable: true, PlanComplete: true, Action: plans.Delete, Mode: plans.DestroyMode, RequiredComponents: collections.NewSet(mustAbsComponent("component.self")), PlannedOutputValues: make(map[string]cty.Value), PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.self"), PlanComplete: true, PlanApplyable: true, Action: plans.Delete, Mode: plans.DestroyMode, PlannedOutputValues: map[string]cty.Value{ "id": cty.StringVal("foo"), }, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: mustStackInputVariable("id"), Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.StringVal("foo"), DeleteOnApply: true, }, }, }, }, "deferred-provider-with-write-only": { path: "with-write-only-attribute", cycle: TestCycle{ planInputs: map[string]cty.Value{ "providers": cty.UnknownVal(cty.Set(cty.String)), }, wantPlannedChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.main"), Action: plans.Create, PlannedInputValues: map[string]plans.DynamicValue{ "datasource_id": mustPlanDynamicValueDynamicType(cty.StringVal("datasource")), "resource_id": mustPlanDynamicValueDynamicType(cty.StringVal("resource")), "write_only_input": mustPlanDynamicValueDynamicType(cty.StringVal("secret")), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "datasource_id": nil, "resource_id": nil, "write_only_input": nil, }, PlannedOutputValues: make(map[string]cty.Value), PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeDeferredResourceInstancePlanned{ ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.main.data.testing_write_only_data_source.data"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("data.testing_write_only_data_source.data"), PrevRunAddr: mustAbsResourceInstance("data.testing_write_only_data_source.data"), ProviderAddr: mustDefaultRootProvider("testing"), ChangeSrc: plans.ChangeSrc{ Action: plans.Read, Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("datasource"), "value": cty.UnknownVal(cty.String), "write_only": cty.NullVal(cty.String), })), AfterSensitivePaths: []cty.Path{ cty.GetAttrPath("write_only"), }, }, ActionReason: plans.ResourceInstanceReadBecauseDependencyPending, }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.WriteOnlyDataSourceSchema, }, DeferredReason: providers.DeferredReasonProviderConfigUnknown, }, &stackplan.PlannedChangeDeferredResourceInstancePlanned{ ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.main.testing_write_only_resource.data"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_write_only_resource.data"), PrevRunAddr: mustAbsResourceInstance("testing_write_only_resource.data"), ProviderAddr: mustDefaultRootProvider("testing"), ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("resource"), "value": cty.UnknownVal(cty.String), "write_only": cty.NullVal(cty.String), })), AfterSensitivePaths: []cty.Path{ cty.GetAttrPath("write_only"), }, }, }, PriorStateSrc: nil, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.WriteOnlyResourceSchema, }, DeferredReason: providers.DeferredReasonProviderConfigUnknown, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: mustStackInputVariable("providers"), Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.UnknownVal(cty.Set(cty.String)), }, }, }, }, "deferred-provider-with-data-sources": { path: path.Join("with-data-source", "deferred-provider-for-each"), store: stacks_testing_provider.NewResourceStoreBuilder(). AddResource("data_known", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("data_known"), "value": cty.StringVal("known"), })). Build(), cycle: TestCycle{ planInputs: map[string]cty.Value{ "providers": cty.UnknownVal(cty.Set(cty.String)), }, wantPlannedChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.const"), PlanApplyable: true, PlanComplete: true, Action: plans.Create, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.StringVal("data_known")), "resource": mustPlanDynamicValueDynamicType(cty.StringVal("resource_known")), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "id": nil, "resource": nil, }, PlannedOutputValues: make(map[string]cty.Value), PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.const.data.testing_data_source.data"), ChangeSrc: nil, PriorStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "data_known", "value": "known", }), Status: states.ObjectReady, Dependencies: make([]addrs.ConfigResource, 0), }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingDataSourceSchema, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.const.testing_resource.data"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource.data"), PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), ProviderAddr: mustDefaultRootProvider("testing"), ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("resource_known"), "value": cty.StringVal("known"), })), }, }, PriorStateSrc: nil, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.main[*]"), PlanApplyable: false, // only deferred changes PlanComplete: false, // deferred Action: plans.Create, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.StringVal("data_unknown")), "resource": mustPlanDynamicValueDynamicType(cty.StringVal("resource_unknown")), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "id": nil, "resource": nil, }, PlannedOutputValues: make(map[string]cty.Value), PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeDeferredResourceInstancePlanned{ ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ Component: stackaddrs.AbsComponentInstance{ Item: stackaddrs.ComponentInstance{ Component: stackaddrs.Component{ Name: "main", }, Key: addrs.WildcardKey, }, }, Item: addrs.AbsResourceInstanceObject{ ResourceInstance: mustAbsResourceInstance("data.testing_data_source.data"), }, }, ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("data.testing_data_source.data"), PrevRunAddr: mustAbsResourceInstance("data.testing_data_source.data"), ProviderAddr: mustDefaultRootProvider("testing"), ChangeSrc: plans.ChangeSrc{ Action: plans.Read, Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("data_unknown"), "value": cty.UnknownVal(cty.String), })), }, ActionReason: plans.ResourceInstanceReadBecauseDependencyPending, }, PriorStateSrc: nil, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingDataSourceSchema, }, DeferredReason: providers.DeferredReasonProviderConfigUnknown, }, &stackplan.PlannedChangeDeferredResourceInstancePlanned{ ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ Component: stackaddrs.AbsComponentInstance{ Item: stackaddrs.ComponentInstance{ Component: stackaddrs.Component{ Name: "main", }, Key: addrs.WildcardKey, }, }, Item: addrs.AbsResourceInstanceObject{ ResourceInstance: mustAbsResourceInstance("testing_resource.data"), }, }, ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource.data"), PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), ProviderAddr: mustDefaultRootProvider("testing"), ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("resource_unknown"), "value": cty.UnknownVal(cty.String), })), }, }, PriorStateSrc: nil, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, DeferredReason: providers.DeferredReasonProviderConfigUnknown, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: mustStackInputVariable("providers"), Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.UnknownVal(cty.Set(cty.String)), }, }, }, }, "removed embedded component duplicate": { path: filepath.Join("with-single-input", "removed-component-from-stack-dynamic"), cycle: TestCycle{ planInputs: map[string]cty.Value{ "for_each_input": cty.MapVal(map[string]cty.Value{ "foo": cty.StringVal("bar"), }), "simple_input": cty.MapVal(map[string]cty.Value{ "foo": cty.StringVal("bar"), }), "for_each_removed": cty.SetVal([]cty.Value{ cty.StringVal("foo"), }), "simple_removed": cty.SetVal([]cty.Value{ cty.StringVal("foo"), }), }, wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Cannot remove component instance", Detail: "The component instance stack.for_each.component.self[\"foo\"] is targeted by a component block and cannot be removed. The relevant component is defined at git::https://example.com/test.git//with-single-input/for-each-component/for-each-component.tfcomponent.hcl:15,1-17.", Subject: &hcl.Range{ Filename: "git::https://example.com/test.git//with-single-input/removed-component-from-stack-dynamic/removed-component-from-stack-dynamic.tfcomponent.hcl", Start: hcl.Pos{Line: 38, Column: 1, Byte: 505}, End: hcl.Pos{Line: 38, Column: 8, Byte: 512}, }, }) diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Cannot remove component instance", Detail: "The component instance stack.simple[\"foo\"].component.self is targeted by a component block and cannot be removed. The relevant component is defined at git::https://example.com/test.git//with-single-input/valid/valid.tfcomponent.hcl:19,1-17.", Subject: &hcl.Range{ Filename: "git::https://example.com/test.git//with-single-input/removed-component-from-stack-dynamic/removed-component-from-stack-dynamic.tfcomponent.hcl", Start: hcl.Pos{Line: 60, Column: 1, Byte: 811}, End: hcl.Pos{Line: 60, Column: 8, Byte: 818}, }, }) return diags }), }, }, "deferred-embedded-stack-update": { path: path.Join("with-single-input", "deferred-embedded-stack-for-each"), state: stackstate.NewStateBuilder(). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.a[\"deferred\"].component.self")). AddInputVariable("id", cty.StringVal("deferred")). AddInputVariable("input", cty.StringVal("deferred"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("stack.a[\"deferred\"].component.self.testing_resource.data")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "deferred", "value": "deferred", }), })). AddInput("stacks", cty.MapVal(map[string]cty.Value{ "deferred": cty.StringVal("deferred"), })). Build(), store: stacks_testing_provider.NewResourceStoreBuilder(). AddResource("deferred", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("deferred"), "value": cty.StringVal("deferred"), })). Build(), cycle: TestCycle{ planInputs: map[string]cty.Value{ "stacks": cty.UnknownVal(cty.Map(cty.String)), }, wantPlannedChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeComponentInstance{ Addr: stackaddrs.Absolute( stackaddrs.RootStackInstance.Child("a", addrs.StringKey("deferred")), stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "self"}, }, ), PlanApplyable: false, // Everything is deferred, so nothing to apply. PlanComplete: false, Action: plans.Update, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.StringVal("deferred")), "input": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)), }, PlannedOutputValues: map[string]cty.Value{}, PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "id": nil, "input": nil, }, }, &stackplan.PlannedChangeDeferredResourceInstancePlanned{ DeferredReason: providers.DeferredReasonDeferredPrereq, ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ Component: stackaddrs.Absolute( stackaddrs.RootStackInstance.Child("a", addrs.StringKey("deferred")), stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "self"}, }, ), Item: addrs.AbsResourceInstanceObject{ ResourceInstance: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), }, }, ProviderConfigAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), PrevRunAddr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), ProviderAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, ChangeSrc: plans.ChangeSrc{ Action: plans.Update, Before: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("deferred"), "value": cty.StringVal("deferred"), }), stacks_testing_provider.TestingResourceSchema.Body), After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("deferred"), "value": cty.UnknownVal(cty.String), }), stacks_testing_provider.TestingResourceSchema.Body), AfterSensitivePaths: nil, }, }, PriorStateSrc: &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "deferred", "value": "deferred", }), Dependencies: make([]addrs.ConfigResource, 0), }, Schema: stacks_testing_provider.TestingResourceSchema, }, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "stacks"}, Action: plans.Update, Before: cty.MapVal(map[string]cty.Value{ "deferred": cty.StringVal("deferred"), }), After: cty.UnknownVal(cty.Map(cty.String)), }, }, }, }, "deferred-embedded-stack-create": { path: path.Join("with-single-input", "deferred-embedded-stack-for-each"), cycle: TestCycle{ planInputs: map[string]cty.Value{ "stacks": cty.UnknownVal(cty.Map(cty.String)), }, wantPlannedChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeComponentInstance{ Addr: stackaddrs.Absolute( stackaddrs.RootStackInstance.Child("a", addrs.WildcardKey), stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "self"}, }, ), PlanApplyable: false, // Everything is deferred, so nothing to apply. PlanComplete: false, Action: plans.Create, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)), "input": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)), }, PlannedOutputValues: map[string]cty.Value{}, PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "id": nil, "input": nil, }, }, &stackplan.PlannedChangeDeferredResourceInstancePlanned{ DeferredReason: providers.DeferredReasonDeferredPrereq, ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ Component: stackaddrs.Absolute( stackaddrs.RootStackInstance.Child("a", addrs.WildcardKey), stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "self"}, }, ), Item: addrs.AbsResourceInstanceObject{ ResourceInstance: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), }, }, ProviderConfigAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), PrevRunAddr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), ProviderAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.UnknownVal(cty.String), }), stacks_testing_provider.TestingResourceSchema.Body), AfterSensitivePaths: nil, }, }, Schema: stacks_testing_provider.TestingResourceSchema, }, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "stacks"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.UnknownVal(cty.Map(cty.String)), }, }, }, }, "deferred-embedded-stack-and-component-for-each": { path: path.Join("with-single-input", "deferred-embedded-stack-and-component-for-each"), cycle: TestCycle{ planInputs: map[string]cty.Value{ "stacks": cty.UnknownVal(cty.Map(cty.Set(cty.String))), }, wantPlannedChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeComponentInstance{ Addr: stackaddrs.Absolute( stackaddrs.RootStackInstance.Child("a", addrs.WildcardKey), stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "self"}, Key: addrs.WildcardKey, }, ), PlanApplyable: false, // Everything is deferred, so nothing to apply. PlanComplete: false, Action: plans.Create, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)), "input": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)), }, PlannedOutputValues: map[string]cty.Value{}, PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "id": nil, "input": nil, }, }, &stackplan.PlannedChangeDeferredResourceInstancePlanned{ DeferredReason: providers.DeferredReasonDeferredPrereq, ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ Component: stackaddrs.Absolute( stackaddrs.RootStackInstance.Child("a", addrs.WildcardKey), stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "self"}, Key: addrs.WildcardKey, }, ), Item: addrs.AbsResourceInstanceObject{ ResourceInstance: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), }, }, ProviderConfigAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), PrevRunAddr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), ProviderAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.UnknownVal(cty.String), }), stacks_testing_provider.TestingResourceSchema.Body), AfterSensitivePaths: nil, }, }, Schema: stacks_testing_provider.TestingResourceSchema, }, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "stacks"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.UnknownVal(cty.Map(cty.Set(cty.String))), }, }, }, }, "removed block targets stack not in configuration or state": { path: filepath.Join("with-single-input", "removed-stack-instance-dynamic"), cycle: TestCycle{ planInputs: map[string]cty.Value{ "input": cty.MapValEmpty(cty.String), "removed": cty.MapVal(map[string]cty.Value{ "component": cty.StringVal("component"), }), }, wantPlannedChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "input"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.MapValEmpty(cty.String), }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "removed"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.MapVal(map[string]cty.Value{ "component": cty.StringVal("component"), }), }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "removed-direct"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.SetValEmpty(cty.String), }, }, }, }, "embedded stack in state but not in configuration": { path: filepath.Join("with-single-input", "valid"), state: stackstate.NewStateBuilder(). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.child.component.self"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("stack.child.component.self.testing_resource.data")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "leftover", "value": "leftover", }), })). Build(), store: stacks_testing_provider.NewResourceStoreBuilder(). AddResource("leftover", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("leftover"), "value": cty.StringVal("leftover"), })). Build(), cycle: TestCycle{ planInputs: map[string]cty.Value{ "input": cty.StringVal("input"), }, wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { return diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Unclaimed component instance", Detail: "The component instance stack.child.component.self is not claimed by any component or removed block in the configuration. Make sure it is instantiated by a component block, or targeted for removal by a removed block.", }) }), }, }, "removed and stack block target the same stack": { path: filepath.Join("with-single-input", "removed-stack-instance-dynamic"), cycle: TestCycle{ planInputs: map[string]cty.Value{ "input": cty.MapVal(map[string]cty.Value{ "component": cty.StringVal("component"), }), "removed": cty.MapVal(map[string]cty.Value{ "component": cty.StringVal("component"), }), }, wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { return diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Cannot remove stack instance", Detail: "The stack instance stack.simple[\"component\"] is targeted by an embedded stack block and cannot be removed. The relevant embedded stack is defined at git::https://example.com/test.git//with-single-input/removed-stack-instance-dynamic/removed-stack-instance-dynamic.tfcomponent.hcl:25,1-15.", Subject: &hcl.Range{ Filename: "git::https://example.com/test.git//with-single-input/removed-stack-instance-dynamic/removed-stack-instance-dynamic.tfcomponent.hcl", Start: hcl.Pos{Line: 36, Column: 1, Byte: 441}, End: hcl.Pos{Line: 36, Column: 8, Byte: 448}, }, }) }), }, }, "removed targets stack block in embedded stack that exists": { path: filepath.Join("with-single-input", "removed-stack-from-embedded-stack"), cycle: TestCycle{ planInputs: map[string]cty.Value{ "input": cty.MapVal(map[string]cty.Value{ "component": cty.MapVal(map[string]cty.Value{ "component": cty.StringVal("component"), }), }), "removed": cty.MapVal(map[string]cty.Value{ "component": cty.MapVal(map[string]cty.Value{ "id": cty.StringVal("component"), "input": cty.StringVal("component"), }), }), }, wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { return diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Cannot remove stack instance", Detail: "The stack instance stack.embedded[\"component\"].stack.simple[\"component\"] is targeted by an embedded stack block and cannot be removed. The relevant embedded stack is defined at git::https://example.com/test.git//with-single-input/removed-stack-instance-dynamic/removed-stack-instance-dynamic.tfcomponent.hcl:25,1-15.", Subject: &hcl.Range{ Filename: "git::https://example.com/test.git//with-single-input/removed-stack-from-embedded-stack/removed-stack-from-embedded-stack.tfcomponent.hcl", Start: hcl.Pos{Line: 28, Column: 1, Byte: 360}, End: hcl.Pos{Line: 28, Column: 8, Byte: 367}, }, }) }), }, }, "removed block targets component inside removed stack": { path: filepath.Join("with-single-input", "removed-stack-instance-dynamic"), state: stackstate.NewStateBuilder(). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.simple[\"component\"].component.self")). AddInputVariable("id", cty.StringVal("component")). AddInputVariable("input", cty.StringVal("component"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("stack.simple[\"component\"].component.self.testing_resource.data")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "component", "value": "component", }), })). Build(), store: stacks_testing_provider.NewResourceStoreBuilder(). AddResource("component", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("component"), "value": cty.StringVal("component"), })). Build(), cycle: TestCycle{ planInputs: map[string]cty.Value{ "removed": cty.MapVal(map[string]cty.Value{ "component": cty.StringVal("component"), }), "removed-direct": cty.SetVal([]cty.Value{ cty.StringVal("component"), }), }, wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { return diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Cannot remove component instance", Detail: "The component instance stack.simple[\"component\"].component.self is targeted by a component block and cannot be removed. The relevant component is defined at git::https://example.com/test.git//with-single-input/valid/valid.tfcomponent.hcl:19,1-17.", Subject: &hcl.Range{ Filename: "git::https://example.com/test.git//with-single-input/removed-stack-instance-dynamic/removed-stack-instance-dynamic.tfcomponent.hcl", Start: hcl.Pos{Line: 51, Column: 1, Byte: 708}, End: hcl.Pos{Line: 51, Column: 8, Byte: 715}, }, }) }), }, }, "removed block targets orphaned component": { path: filepath.Join("with-single-input", "removed-component-from-stack-dynamic"), state: stackstate.NewStateBuilder(). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.simple[\"component\"].component.self")). AddInputVariable("id", cty.StringVal("component")). AddInputVariable("input", cty.StringVal("component"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("stack.simple[\"component\"].component.self.testing_resource.data")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "component", "value": "component", }), })). Build(), store: stacks_testing_provider.NewResourceStoreBuilder(). AddResource("component", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("component"), "value": cty.StringVal("component"), })). Build(), cycle: TestCycle{ planInputs: map[string]cty.Value{ "simple_input": cty.MapValEmpty(cty.String), "simple_removed": cty.SetVal([]cty.Value{ cty.StringVal("component"), }), }, wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { return diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid removed block", Detail: "The component instance stack.simple[\"component\"].component.self could not be removed. The linked removed block was not executed because the `from` attribute of the removed block targets a component or embedded stack within an orphaned embedded stack.\n\nIn order to remove an entire stack, update your removed block to target the entire removed stack itself instead of the specific elements within it.", Subject: &hcl.Range{ Filename: "git::https://example.com/test.git//with-single-input/removed-component-from-stack-dynamic/removed-component-from-stack-dynamic.tfcomponent.hcl", Start: hcl.Pos{Line: 60, Column: 1, Byte: 811}, End: hcl.Pos{Line: 60, Column: 8, Byte: 818}, }, }) }), wantPlannedChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: false, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "for_each_input"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.MapValEmpty(cty.String), }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "for_each_removed"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.SetValEmpty(cty.String), }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "simple_input"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.MapValEmpty(cty.String), }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "simple_removed"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.SetVal([]cty.Value{ cty.StringVal("component"), }), }, }, }, }, "removed block targets orphaned stack": { path: filepath.Join("with-single-input", "removed-stack-from-embedded-stack"), state: stackstate.NewStateBuilder(). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.embedded[\"component\"].stack.simple[\"component\"].component.self")). AddInputVariable("id", cty.StringVal("component")). AddInputVariable("input", cty.StringVal("component"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("stack.embedded[\"component\"].stack.simple[\"component\"].component.self.testing_resource.data")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "component", "value": "component", }), })). Build(), store: stacks_testing_provider.NewResourceStoreBuilder(). AddResource("component", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("component"), "value": cty.StringVal("component"), })). Build(), cycle: TestCycle{ planInputs: map[string]cty.Value{ "input": cty.MapValEmpty(cty.Map(cty.String)), "removed": cty.MapVal(map[string]cty.Value{ "component": cty.MapVal(map[string]cty.Value{ "id": cty.StringVal("component"), "input": cty.StringVal("component"), }), }), }, wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { return diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid removed block", Detail: "The component instance stack.embedded[\"component\"].stack.simple[\"component\"].component.self could not be removed. The linked removed block was not executed because the `from` attribute of the removed block targets a component or embedded stack within an orphaned embedded stack.\n\nIn order to remove an entire stack, update your removed block to target the entire removed stack itself instead of the specific elements within it.", Subject: &hcl.Range{ Filename: "git::https://example.com/test.git//with-single-input/removed-stack-from-embedded-stack/removed-stack-from-embedded-stack.tfcomponent.hcl", Start: hcl.Pos{Line: 28, Column: 1, Byte: 360}, End: hcl.Pos{Line: 28, Column: 8, Byte: 367}, }, }) }), }, }, "removed block targets orphaned component without config definition": { path: filepath.Join("with-single-input", "orphaned-component"), state: stackstate.NewStateBuilder(). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.embedded.component.self")). AddInputVariable("id", cty.StringVal("component")). AddInputVariable("input", cty.StringVal("component"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("stack.embedded.component.self.testing_resource.data")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "component", "value": "component", }), })). Build(), store: stacks_testing_provider.NewResourceStoreBuilder(). AddResource("component", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("component"), "value": cty.StringVal("component"), })). Build(), cycle: TestCycle{ wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { return diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid removed block", Detail: "The component instance stack.embedded.component.self could not be removed. The linked removed block was not executed because the `from` attribute of the removed block targets a component or embedded stack within an orphaned embedded stack.\n\nIn order to remove an entire stack, update your removed block to target the entire removed stack itself instead of the specific elements within it.", Subject: &hcl.Range{ Filename: "git::https://example.com/test.git//with-single-input/orphaned-component/orphaned-component.tfcomponent.hcl", Start: hcl.Pos{Line: 10, Column: 1, Byte: 131}, End: hcl.Pos{Line: 10, Column: 8, Byte: 138}, }, }) }), }, }, "unknown embedded stack with internal component targeted by concrete removed block": { path: filepath.Join("with-single-input", "removed-stack-instance-dynamic"), state: stackstate.NewStateBuilder(). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.simple[\"component\"].component.self")). AddInputVariable("id", cty.StringVal("component")). AddInputVariable("input", cty.StringVal("component"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("stack.simple[\"component\"].component.self.testing_resource.data")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "component", "value": "component", }), })). Build(), store: stacks_testing_provider.NewResourceStoreBuilder(). AddResource("component", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("component"), "value": cty.StringVal("component"), })). Build(), cycle: TestCycle{ planInputs: map[string]cty.Value{ "removed": cty.UnknownVal(cty.Map(cty.String)), }, wantPlannedChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("stack.simple[\"component\"].component.self"), Action: plans.Delete, Mode: plans.DestroyMode, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)), "input": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "id": nil, "input": nil, }, PlannedOutputValues: make(map[string]cty.Value), PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeDeferredResourceInstancePlanned{ ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.simple[\"component\"].component.self.testing_resource.data"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource.data"), PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), ProviderAddr: mustDefaultRootProvider("testing"), ChangeSrc: plans.ChangeSrc{ Action: plans.Delete, Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("component"), "value": cty.StringVal("component"), })), After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ "id": cty.String, "value": cty.String, }))), }, }, PriorStateSrc: &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "component", "value": "component", }), Dependencies: make([]addrs.ConfigResource, 0), }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, DeferredReason: providers.DeferredReasonDeferredPrereq, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "input"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.MapValEmpty(cty.String), }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "removed"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.UnknownVal(cty.Map(cty.String)), }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "removed-direct"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.SetValEmpty(cty.String), }, }, }, }, "remove partial stack": { path: filepath.Join("with-single-input", "multiple-components", "removed"), state: stackstate.NewStateBuilder(). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.multiple.component.one")). AddInputVariable("id", cty.StringVal("one")). AddInputVariable("input", cty.StringVal("one"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("stack.multiple.component.one.testing_resource.data")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "one", "value": "one", }), })). Build(), store: stacks_testing_provider.NewResourceStoreBuilder(). AddResource("one", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("one"), "value": cty.StringVal("one"), })). Build(), cycle: TestCycle{ wantPlannedChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("stack.multiple.component.one"), PlanApplyable: true, PlanComplete: true, Action: plans.Delete, Mode: plans.DestroyMode, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.StringVal("one")), "input": mustPlanDynamicValueDynamicType(cty.StringVal("one")), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "id": nil, "input": nil, }, PlannedOutputValues: make(map[string]cty.Value), PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.multiple.component.one.testing_resource.data"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource.data"), PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), ProviderAddr: mustDefaultRootProvider("testing"), ChangeSrc: plans.ChangeSrc{ Action: plans.Delete, Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("one"), "value": cty.StringVal("one"), })), After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ "id": cty.String, "value": cty.String, }))), }, }, PriorStateSrc: &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "one", "value": "one", }), Dependencies: make([]addrs.ConfigResource, 0), }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("stack.multiple.component.two"), PlanApplyable: true, PlanComplete: true, Action: plans.Delete, Mode: plans.DestroyMode, PlannedOutputValues: make(map[string]cty.Value), PlanTimestamp: fakePlanTimestamp, }, }, }, }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { ctx := context.Background() lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), providerreqs.MustParseVersion("0.0.0"), providerreqs.MustParseVersionConstraints("=0.0.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) store := tc.store if store == nil { store = stacks_testing_provider.NewResourceStore() } testContext := TestContext{ timestamp: &fakePlanTimestamp, config: loadMainBundleConfigForTest(t, tc.path), providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProviderWithData(t, store), nil }, }, dependencyLocks: *lock, } testContext.Plan(t, ctx, tc.state, tc.cycle) }) } } func TestPlanWithMissingInputVariable(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, "plan-undeclared-variable-in-component") fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z") if err != nil { t.Fatal(err) } changesCh := make(chan stackplan.PlannedChange, 8) diagsCh := make(chan tfdiags.Diagnostic, 2) req := PlanRequest{ Config: cfg, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) { return terraformProvider.NewProvider(), nil }, }, ForcePlanTimestamp: &fakePlanTimestamp, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) _, gotDiags := collectPlanOutput(changesCh, diagsCh) // We'll normalize the diagnostics to be of consistent underlying type // using ForRPC, so that we can easily diff them; we don't actually care // about which underlying implementation is in use. gotDiags = gotDiags.ForRPC() var wantDiags tfdiags.Diagnostics wantDiags = wantDiags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Reference to undeclared input variable", Detail: `There is no variable "input" block declared in this stack.`, Subject: &hcl.Range{ Filename: mainBundleSourceAddrStr("plan-undeclared-variable-in-component/undeclared-variable.tfcomponent.hcl"), Start: hcl.Pos{Line: 17, Column: 13, Byte: 250}, End: hcl.Pos{Line: 17, Column: 22, Byte: 259}, }, }) wantDiags = wantDiags.ForRPC() if diff := cmp.Diff(wantDiags, gotDiags); diff != "" { t.Errorf("wrong diagnostics\n%s", diff) } } func TestPlanWithNoValueForRequiredVariable(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, "plan-no-value-for-required-variable") fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z") if err != nil { t.Fatal(err) } changesCh := make(chan stackplan.PlannedChange, 8) diagsCh := make(chan tfdiags.Diagnostic, 2) req := PlanRequest{ Config: cfg, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) { return terraformProvider.NewProvider(), nil }, }, ForcePlanTimestamp: &fakePlanTimestamp, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) _, gotDiags := collectPlanOutput(changesCh, diagsCh) // We'll normalize the diagnostics to be of consistent underlying type // using ForRPC, so that we can easily diff them; we don't actually care // about which underlying implementation is in use. gotDiags = gotDiags.ForRPC() var wantDiags tfdiags.Diagnostics wantDiags = wantDiags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "No value for required variable", Detail: `The root input variable "var.beep" is not set, and has no default value.`, Subject: &hcl.Range{ Filename: mainBundleSourceAddrStr("plan-no-value-for-required-variable/unset-variable.tfcomponent.hcl"), Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, End: hcl.Pos{Line: 1, Column: 16, Byte: 15}, }, }) wantDiags = wantDiags.ForRPC() if diff := cmp.Diff(wantDiags, gotDiags); diff != "" { t.Errorf("wrong diagnostics\n%s", diff) } } func TestPlanWithVariableDefaults(t *testing.T) { // Test that defaults are applied correctly for both unspecified input // variables and those with an explicit null value. testCases := map[string]struct { inputs map[stackaddrs.InputVariable]ExternalInputValue }{ "unspecified": { inputs: make(map[stackaddrs.InputVariable]ExternalInputValue), }, "explicit null": { inputs: map[stackaddrs.InputVariable]ExternalInputValue{ {Name: "beep"}: { Value: cty.NullVal(cty.DynamicPseudoType), DefRange: tfdiags.SourceRange{Filename: "fake.tfcomponent.hcl"}, }, }, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, "plan-variable-defaults") fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z") if err != nil { t.Fatal(err) } changesCh := make(chan stackplan.PlannedChange, 8) diagsCh := make(chan tfdiags.Diagnostic, 2) req := PlanRequest{ Config: cfg, InputValues: tc.inputs, ForcePlanTimestamp: &fakePlanTimestamp, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) gotChanges, diags := collectPlanOutput(changesCh, diagsCh) if len(diags) != 0 { t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error()) } wantChanges := []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangeOutputValue{ Addr: stackaddrs.OutputValue{Name: "beep"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.StringVal("BEEP"), }, &stackplan.PlannedChangeOutputValue{ Addr: stackaddrs.OutputValue{Name: "defaulted"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.StringVal("BOOP"), }, &stackplan.PlannedChangeOutputValue{ Addr: stackaddrs.OutputValue{Name: "specified"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.StringVal("BEEP"), }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{ Name: "beep", }, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.StringVal("BEEP"), }, } sort.SliceStable(gotChanges, func(i, j int) bool { return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) }) if diff := cmp.Diff(wantChanges, gotChanges, ctydebug.CmpOptions); diff != "" { t.Errorf("wrong changes\n%s", diff) } }) } } func TestPlanWithComplexVariableDefaults(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, path.Join("complex-inputs")) fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") if err != nil { t.Fatal(err) } changesCh := make(chan stackplan.PlannedChange) diagsCh := make(chan tfdiags.Diagnostic) lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), providerreqs.MustParseVersion("0.0.0"), providerreqs.MustParseVersionConstraints("=0.0.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ Config: cfg, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, DependencyLocks: *lock, InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ {Name: "optional"}: { Value: cty.EmptyObjectVal, // This should be populated by defaults. DefRange: tfdiags.SourceRange{}, }, }, ForcePlanTimestamp: &fakePlanTimestamp, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) changes, diags := collectPlanOutput(changesCh, diagsCh) if len(diags) != 0 { t.Fatalf("unexpected diagnostics: %s", diags) } sort.SliceStable(changes, func(i, j int) bool { return plannedChangeSortKey(changes[i]) < plannedChangeSortKey(changes[j]) }) wantChanges := []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.self"), PlanComplete: true, PlanApplyable: true, Action: plans.Create, RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](), PlannedInputValues: map[string]plans.DynamicValue{ "input": mustPlanDynamicValueDynamicType(cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("cec9bc39"), "value": cty.StringVal("hello, mercury!"), }), cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("78d8b3d7"), "value": cty.StringVal("hello, venus!"), }), cty.ObjectVal(map[string]cty.Value{ "id": cty.NullVal(cty.String), "value": cty.StringVal("hello, earth!"), }), })), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "input": nil, }, PlannedOutputValues: make(map[string]cty.Value), PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data[0]"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource.data[0]"), PrevRunAddr: mustAbsResourceInstance("testing_resource.data[0]"), ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("cec9bc39"), "value": cty.StringVal("hello, mercury!"), })), }, ProviderAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, }, ProviderConfigAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, Schema: stacks_testing_provider.TestingResourceSchema, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data[1]"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource.data[1]"), PrevRunAddr: mustAbsResourceInstance("testing_resource.data[1]"), ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("78d8b3d7"), "value": cty.StringVal("hello, venus!"), })), }, ProviderAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, }, ProviderConfigAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, Schema: stacks_testing_provider.TestingResourceSchema, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data[2]"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource.data[2]"), PrevRunAddr: mustAbsResourceInstance("testing_resource.data[2]"), ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.StringVal("hello, earth!"), })), }, ProviderAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, }, ProviderConfigAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, Schema: stacks_testing_provider.TestingResourceSchema, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("stack.child.component.parent"), PlanComplete: true, PlanApplyable: true, Action: plans.Create, RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](), PlannedInputValues: map[string]plans.DynamicValue{ "input": mustPlanDynamicValueDynamicType(cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("cec9bc39"), "value": cty.StringVal("hello, mercury!"), }), cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("78d8b3d7"), "value": cty.StringVal("hello, venus!"), }), cty.ObjectVal(map[string]cty.Value{ "id": cty.NullVal(cty.String), "value": cty.StringVal("hello, earth!"), }), })), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "input": nil, }, PlannedOutputValues: make(map[string]cty.Value), PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.child.component.parent.testing_resource.data[0]"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource.data[0]"), PrevRunAddr: mustAbsResourceInstance("testing_resource.data[0]"), ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("cec9bc39"), "value": cty.StringVal("hello, mercury!"), })), }, ProviderAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, }, ProviderConfigAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, Schema: stacks_testing_provider.TestingResourceSchema, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.child.component.parent.testing_resource.data[1]"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource.data[1]"), PrevRunAddr: mustAbsResourceInstance("testing_resource.data[1]"), ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("78d8b3d7"), "value": cty.StringVal("hello, venus!"), })), }, ProviderAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, }, ProviderConfigAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, Schema: stacks_testing_provider.TestingResourceSchema, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.child.component.parent.testing_resource.data[2]"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource.data[2]"), PrevRunAddr: mustAbsResourceInstance("testing_resource.data[2]"), ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.StringVal("hello, earth!"), })), }, ProviderAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, }, ProviderConfigAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, Schema: stacks_testing_provider.TestingResourceSchema, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "default"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("cec9bc39"), "value": cty.StringVal("hello, mercury!"), }), }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "optional"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.NullVal(cty.String), "value": cty.StringVal("hello, earth!"), }), }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "optional_default"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("78d8b3d7"), "value": cty.StringVal("hello, venus!"), }), }, } if diff := cmp.Diff(wantChanges, changes, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func TestPlanWithSingleResource(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, "with-single-resource") fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z") if err != nil { t.Fatal(err) } changesCh := make(chan stackplan.PlannedChange, 8) diagsCh := make(chan tfdiags.Diagnostic, 2) req := PlanRequest{ Config: cfg, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) { return terraformProvider.NewProvider(), nil }, }, ForcePlanTimestamp: &fakePlanTimestamp, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) gotChanges, diags := collectPlanOutput(changesCh, diagsCh) if len(diags) != 0 { t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error()) } // The order of emission for our planned changes is unspecified since it // depends on how the various goroutines get scheduled, and so we'll // arbitrarily sort gotChanges lexically by the name of the change type // so that we have some dependable order to diff against below. sort.Slice(gotChanges, func(i, j int) bool { ic := gotChanges[i] jc := gotChanges[j] return fmt.Sprintf("%T", ic) < fmt.Sprintf("%T", jc) }) wantChanges := []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: stackaddrs.Absolute( stackaddrs.RootStackInstance, stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "self"}, }, ), Action: plans.Create, PlanApplyable: true, PlanComplete: true, PlannedCheckResults: &states.CheckResults{}, PlannedInputValues: make(map[string]plans.DynamicValue), PlannedOutputValues: map[string]cty.Value{ "input": cty.StringVal("hello"), "output": cty.UnknownVal(cty.String), }, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangeOutputValue{ Addr: stackaddrs.OutputValue{Name: "obj"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.ObjectVal(map[string]cty.Value{ "input": cty.StringVal("hello"), "output": cty.UnknownVal(cty.String), }), }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ Component: stackaddrs.Absolute( stackaddrs.RootStackInstance, stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "self"}, }, ), Item: addrs.AbsResourceInstanceObject{ ResourceInstance: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "terraform_data", Name: "main", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), }, }, ProviderConfigAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("terraform.io/builtin/terraform"), }, ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "terraform_data", Name: "main", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), PrevRunAddr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "terraform_data", Name: "main", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), ProviderAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewBuiltInProvider("terraform"), }, ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), After: plans.DynamicValue{ // This is an object conforming to the terraform_data // resource type's schema. // // FIXME: Should write this a different way that is // scrutable and won't break each time something gets // added to the terraform_data schema. (We can't use // mustPlanDynamicValue here because the resource type // uses DynamicPseudoType attributes, which require // explicitly-typed encoding.) 0x84, 0xa2, 0x69, 0x64, 0xc7, 0x03, 0x0c, 0x81, 0x01, 0xc2, 0xa5, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x92, 0xc4, 0x08, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x22, 0xa5, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0xa6, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x92, 0xc4, 0x08, 0x22, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x22, 0xd4, 0x00, 0x00, 0xb0, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x73, 0x5f, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0xc0, }, }, }, // The following is schema for the real terraform_data resource // type from the real terraform.io/builtin/terraform provider // maintained elsewhere in this codebase. If that schema changes // in future then this should change to match it. Schema: providers.Schema{ Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "input": {Type: cty.DynamicPseudoType, Optional: true}, "output": {Type: cty.DynamicPseudoType, Computed: true}, "triggers_replace": {Type: cty.DynamicPseudoType, Optional: true}, "id": {Type: cty.String, Computed: true}, }, }, Identity: &configschema.Object{ Attributes: map[string]*configschema.Attribute{ "id": { Type: cty.String, Description: "The unique identifier for the data store.", Required: true, }, }, Nesting: configschema.NestingSingle, }, }, }, } if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func TestPlanWithEphemeralInputVariables(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, "variable-ephemeral") t.Run("with variables set", func(t *testing.T) { changesCh := make(chan stackplan.PlannedChange, 8) diagsCh := make(chan tfdiags.Diagnostic, 2) fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z") if err != nil { t.Fatal(err) } req := PlanRequest{ Config: cfg, InputValues: map[stackaddrs.InputVariable]stackeval.ExternalInputValue{ {Name: "eph"}: {Value: cty.StringVal("eph value")}, {Name: "noneph"}: {Value: cty.StringVal("noneph value")}, }, ForcePlanTimestamp: &fakePlanTimestamp, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) gotChanges, diags := collectPlanOutput(changesCh, diagsCh) if len(diags) != 0 { t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error()) } wantChanges := []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{ Name: "eph", }, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.NullVal(cty.String), // ephemeral RequiredOnApply: true, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{ Name: "noneph", }, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.StringVal("noneph value"), }, } sort.SliceStable(gotChanges, func(i, j int) bool { return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) }) if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } }) t.Run("without variables set", func(t *testing.T) { changesCh := make(chan stackplan.PlannedChange, 8) diagsCh := make(chan tfdiags.Diagnostic, 2) fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z") if err != nil { t.Fatal(err) } req := PlanRequest{ InputValues: map[stackaddrs.InputVariable]stackeval.ExternalInputValue{ // Intentionally not set for this subtest. }, Config: cfg, ForcePlanTimestamp: &fakePlanTimestamp, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) gotChanges, diags := collectPlanOutput(changesCh, diagsCh) if len(diags) != 0 { t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error()) } wantChanges := []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{ Name: "eph", }, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.NullVal(cty.String), // ephemeral RequiredOnApply: false, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{ Name: "noneph", }, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.NullVal(cty.String), }, } sort.SliceStable(gotChanges, func(i, j int) bool { return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) }) if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } }) } func TestPlanVariableOutputRoundtripNested(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, "variable-output-roundtrip-nested") changesCh := make(chan stackplan.PlannedChange, 8) diagsCh := make(chan tfdiags.Diagnostic, 2) fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z") if err != nil { t.Fatal(err) } req := PlanRequest{ Config: cfg, ForcePlanTimestamp: &fakePlanTimestamp, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) gotChanges, diags := collectPlanOutput(changesCh, diagsCh) if len(diags) != 0 { t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error()) } wantChanges := []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangeOutputValue{ Addr: stackaddrs.OutputValue{Name: "msg"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.StringVal("default"), }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{ Name: "msg", }, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.StringVal("default"), }, } sort.SliceStable(gotChanges, func(i, j int) bool { return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) }) if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func TestPlanSensitiveOutput(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, "sensitive-output") fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") if err != nil { t.Fatal(err) } changesCh := make(chan stackplan.PlannedChange, 8) diagsCh := make(chan tfdiags.Diagnostic, 2) req := PlanRequest{ Config: cfg, ForcePlanTimestamp: &fakePlanTimestamp, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) gotChanges, diags := collectPlanOutput(changesCh, diagsCh) if len(diags) != 0 { t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error()) } wantChanges := []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: stackaddrs.Absolute( stackaddrs.RootStackInstance, stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "self"}, }, ), Action: plans.Create, PlanApplyable: true, PlanComplete: true, PlannedCheckResults: &states.CheckResults{}, PlannedInputValues: make(map[string]plans.DynamicValue), PlannedOutputValues: map[string]cty.Value{ "out": cty.StringVal("secret").Mark(marks.Sensitive), }, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangeOutputValue{ Addr: stackaddrs.OutputValue{Name: "result"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.StringVal("secret").Mark(marks.Sensitive), }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, } sort.SliceStable(gotChanges, func(i, j int) bool { return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) }) if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func TestPlanSensitiveOutputNested(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, "sensitive-output-nested") fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") if err != nil { t.Fatal(err) } changesCh := make(chan stackplan.PlannedChange, 8) diagsCh := make(chan tfdiags.Diagnostic, 2) req := PlanRequest{ Config: cfg, ForcePlanTimestamp: &fakePlanTimestamp, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) gotChanges, diags := collectPlanOutput(changesCh, diagsCh) if len(diags) != 0 { t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error()) } wantChanges := []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangeOutputValue{ Addr: stackaddrs.OutputValue{Name: "result"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.StringVal("secret").Mark(marks.Sensitive), }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeComponentInstance{ Addr: stackaddrs.Absolute( stackaddrs.RootStackInstance.Child("child", addrs.NoKey), stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "self"}, }, ), Action: plans.Create, PlanApplyable: true, PlanComplete: true, PlannedCheckResults: &states.CheckResults{}, PlannedInputValues: make(map[string]plans.DynamicValue), PlannedOutputValues: map[string]cty.Value{ "out": cty.StringVal("secret").Mark(marks.Sensitive), }, PlanTimestamp: fakePlanTimestamp, }, } sort.SliceStable(gotChanges, func(i, j int) bool { return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) }) if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func TestPlanSensitiveOutputAsInput(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, "sensitive-output-as-input") fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") if err != nil { t.Fatal(err) } changesCh := make(chan stackplan.PlannedChange, 8) diagsCh := make(chan tfdiags.Diagnostic, 2) req := PlanRequest{ Config: cfg, ForcePlanTimestamp: &fakePlanTimestamp, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) gotChanges, diags := collectPlanOutput(changesCh, diagsCh) if len(diags) != 0 { t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error()) } wantChanges := []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: stackaddrs.Absolute( stackaddrs.RootStackInstance, stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "self"}, }, ), Action: plans.Create, PlanApplyable: true, PlanComplete: true, RequiredComponents: collections.NewSet[stackaddrs.AbsComponent]( mustAbsComponent("stack.sensitive.component.self"), ), PlannedCheckResults: &states.CheckResults{}, PlannedInputValues: map[string]plans.DynamicValue{ "secret": mustPlanDynamicValueDynamicType(cty.StringVal("secret")), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "secret": { { Marks: cty.NewValueMarks(marks.Sensitive), }, }, }, PlannedOutputValues: map[string]cty.Value{ "result": cty.StringVal("SECRET").Mark(marks.Sensitive), }, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangeOutputValue{ Addr: stackaddrs.OutputValue{Name: "result"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), // MessagePack nil After: cty.StringVal("SECRET").Mark(marks.Sensitive), }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeComponentInstance{ Addr: stackaddrs.Absolute( stackaddrs.RootStackInstance.Child("sensitive", addrs.NoKey), stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "self"}, }, ), Action: plans.Create, PlanApplyable: true, PlanComplete: true, PlannedCheckResults: &states.CheckResults{}, PlannedInputValues: make(map[string]plans.DynamicValue), PlannedOutputValues: map[string]cty.Value{ "out": cty.StringVal("secret").Mark(marks.Sensitive), }, PlanTimestamp: fakePlanTimestamp, }, } sort.SliceStable(gotChanges, func(i, j int) bool { return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) }) if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func TestPlanWithProviderConfig(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, "with-provider-config") providerAddr := addrs.MustParseProviderSourceString("example.com/test/test") providerSchema := &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "name": { Type: cty.String, Required: true, }, }, }, }, } inputVarAddr := stackaddrs.InputVariable{Name: "name"} fakeSrcRng := tfdiags.SourceRange{ Filename: "fake-source", } lock := depsfile.NewLocks() lock.SetProvider( providerAddr, providerreqs.MustParseVersion("0.0.0"), providerreqs.MustParseVersionConstraints("=0.0.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) t.Run("valid", func(t *testing.T) { changesCh := make(chan stackplan.PlannedChange, 8) diagsCh := make(chan tfdiags.Diagnostic, 2) provider := &default_testing_provider.MockProvider{ GetProviderSchemaResponse: providerSchema, ValidateProviderConfigResponse: &providers.ValidateProviderConfigResponse{}, ConfigureProviderResponse: &providers.ConfigureProviderResponse{}, } req := PlanRequest{ Config: cfg, InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ inputVarAddr: { Value: cty.StringVal("Jackson"), DefRange: fakeSrcRng, }, }, ProviderFactories: map[addrs.Provider]providers.Factory{ providerAddr: func() (providers.Interface, error) { return provider, nil }, }, DependencyLocks: *lock, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) _, diags := collectPlanOutput(changesCh, diagsCh) if len(diags) != 0 { t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error()) } if !provider.ValidateProviderConfigCalled { t.Error("ValidateProviderConfig wasn't called") } else { req := provider.ValidateProviderConfigRequest if got, want := req.Config.GetAttr("name"), cty.StringVal("Jackson"); !got.RawEquals(want) { t.Errorf("wrong name in ValidateProviderConfig\ngot: %#v\nwant: %#v", got, want) } } if !provider.ConfigureProviderCalled { t.Error("ConfigureProvider wasn't called") } else { req := provider.ConfigureProviderRequest if got, want := req.Config.GetAttr("name"), cty.StringVal("Jackson"); !got.RawEquals(want) { t.Errorf("wrong name in ConfigureProvider\ngot: %#v\nwant: %#v", got, want) } } if !provider.CloseCalled { t.Error("provider wasn't closed") } }) } func TestPlanWithRemovedResource(t *testing.T) { fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z") if err != nil { t.Fatal(err) } attrs := map[string]interface{}{ "id": "FE1D5830765C", "input": map[string]interface{}{ "value": "hello", "type": "string", }, "output": map[string]interface{}{ "value": nil, "type": "string", }, "triggers_replace": nil, } attrsJSON, err := json.Marshal(attrs) if err != nil { t.Fatal(err) } // We want to see that it's adding the extra context for when a provider is // missing for a resource that's in state and not in config. expectedDiagnostic := "has resources in state that" tcs := make(map[string]*string) tcs["missing-providers"] = &expectedDiagnostic tcs["valid-providers"] = nil for name, diag := range tcs { t.Run(name, func(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, path.Join("empty-component", name)) req := PlanRequest{ Config: cfg, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) { return terraformProvider.NewProvider(), nil }, }, ForcePlanTimestamp: &fakePlanTimestamp, // PrevState specifies a state with a resource that is not present in // the current configuration. This is a common situation when a resource // is removed from the configuration but still exists in the state. PrevState: stackstate.NewStateBuilder(). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(stackaddrs.AbsResourceInstanceObject{ Component: stackaddrs.AbsComponentInstance{ Stack: stackaddrs.RootStackInstance, Item: stackaddrs.ComponentInstance{ Component: stackaddrs.Component{ Name: "self", }, Key: addrs.NoKey, }, }, Item: addrs.AbsResourceInstanceObject{ ResourceInstance: addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "terraform_data", Name: "main", }, Key: addrs.NoKey, }, }, DeposedKey: addrs.NotDeposed, }, }). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ SchemaVersion: 0, AttrsJSON: attrsJSON, Status: states.ObjectReady, }). SetProviderAddr(addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("terraform.io/builtin/terraform"), })). Build(), } changesCh := make(chan stackplan.PlannedChange) diagsCh := make(chan tfdiags.Diagnostic) resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) _, diags := collectPlanOutput(changesCh, diagsCh) if diag != nil { if len(diags) == 0 { t.Fatalf("expected diagnostics, got none") } if !strings.Contains(diags[0].Description().Detail, *diag) { t.Fatalf("expected diagnostic %q, got %q", *diag, diags[0].Description().Detail) } } else if len(diags) > 0 { t.Fatalf("unexpected diagnostics: %s", diags.ErrWithWarnings().Error()) } }) } } func TestPlanWithSensitivePropagation(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input", "sensitive-input")) fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") if err != nil { t.Fatal(err) } changesCh := make(chan stackplan.PlannedChange, 8) diagsCh := make(chan tfdiags.Diagnostic, 2) lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), providerreqs.MustParseVersion("0.0.0"), providerreqs.MustParseVersionConstraints("=0.0.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ Config: cfg, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, DependencyLocks: *lock, ForcePlanTimestamp: &fakePlanTimestamp, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) gotChanges, diags := collectPlanOutput(changesCh, diagsCh) if len(diags) != 0 { t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error()) } wantChanges := []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: stackaddrs.Absolute( stackaddrs.RootStackInstance, stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "self"}, }, ), PlanApplyable: true, PlanComplete: true, Action: plans.Create, RequiredComponents: collections.NewSet[stackaddrs.AbsComponent]( stackaddrs.AbsComponent{ Stack: stackaddrs.RootStackInstance, Item: stackaddrs.Component{Name: "sensitive"}, }, ), PlannedCheckResults: &states.CheckResults{}, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)), "input": mustPlanDynamicValueDynamicType(cty.StringVal("secret")), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "id": nil, "input": { { Marks: cty.NewValueMarks(marks.Sensitive), }, }, }, PlannedOutputValues: make(map[string]cty.Value), PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ Component: stackaddrs.Absolute( stackaddrs.RootStackInstance, stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "self"}, }, ), Item: addrs.AbsResourceInstanceObject{ ResourceInstance: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), }, }, ProviderConfigAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), PrevRunAddr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), ProviderAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("testing"), }, ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.StringVal("secret"), }), stacks_testing_provider.TestingResourceSchema.Body), AfterSensitivePaths: []cty.Path{ cty.GetAttrPath("value"), }, }, }, Schema: stacks_testing_provider.TestingResourceSchema, }, &stackplan.PlannedChangeComponentInstance{ Addr: stackaddrs.Absolute( stackaddrs.RootStackInstance, stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "sensitive"}, }, ), PlanApplyable: true, PlanComplete: true, Action: plans.Create, PlannedCheckResults: &states.CheckResults{}, PlannedInputValues: make(map[string]plans.DynamicValue), PlannedOutputValues: map[string]cty.Value{ "out": cty.StringVal("secret").Mark(marks.Sensitive), }, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "id"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.NullVal(cty.String), }, } sort.SliceStable(gotChanges, func(i, j int) bool { return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) }) if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func TestPlanWithSensitivePropagationNested(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input", "sensitive-input-nested")) fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") if err != nil { t.Fatal(err) } changesCh := make(chan stackplan.PlannedChange, 8) diagsCh := make(chan tfdiags.Diagnostic, 2) lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), providerreqs.MustParseVersion("0.0.0"), providerreqs.MustParseVersionConstraints("=0.0.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ Config: cfg, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, DependencyLocks: *lock, ForcePlanTimestamp: &fakePlanTimestamp, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) gotChanges, diags := collectPlanOutput(changesCh, diagsCh) if len(diags) != 0 { t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error()) } wantChanges := []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: stackaddrs.Absolute( stackaddrs.RootStackInstance, stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "self"}, }, ), Action: plans.Create, PlanApplyable: true, PlanComplete: true, RequiredComponents: collections.NewSet[stackaddrs.AbsComponent]( mustAbsComponent("stack.sensitive.component.self"), ), PlannedCheckResults: &states.CheckResults{}, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)), "input": mustPlanDynamicValueDynamicType(cty.StringVal("secret")), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "id": nil, "input": { { Marks: cty.NewValueMarks(marks.Sensitive), }, }, }, PlannedOutputValues: make(map[string]cty.Value), PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ Component: stackaddrs.Absolute( stackaddrs.RootStackInstance, stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "self"}, }, ), Item: addrs.AbsResourceInstanceObject{ ResourceInstance: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), }, }, ProviderConfigAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), PrevRunAddr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), ProviderAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("testing"), }, ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.StringVal("secret"), }), stacks_testing_provider.TestingResourceSchema.Body), AfterSensitivePaths: []cty.Path{ cty.GetAttrPath("value"), }, }, }, Schema: stacks_testing_provider.TestingResourceSchema, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeComponentInstance{ Addr: stackaddrs.Absolute( stackaddrs.RootStackInstance.Child("sensitive", addrs.NoKey), stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "self"}, }, ), Action: plans.Create, PlanApplyable: true, PlanComplete: true, PlannedCheckResults: &states.CheckResults{}, PlannedInputValues: make(map[string]plans.DynamicValue), PlannedOutputValues: map[string]cty.Value{ "out": cty.StringVal("secret").Mark(marks.Sensitive), }, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "id"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.NullVal(cty.String), }, } sort.SliceStable(gotChanges, func(i, j int) bool { return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) }) if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func TestPlanWithForEach(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input", "input-from-component-list")) fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") if err != nil { t.Fatal(err) } changesCh := make(chan stackplan.PlannedChange, 8) diagsCh := make(chan tfdiags.Diagnostic, 2) lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), providerreqs.MustParseVersion("0.0.0"), providerreqs.MustParseVersionConstraints("=0.0.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ Config: cfg, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, DependencyLocks: *lock, ForcePlanTimestamp: &fakePlanTimestamp, InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ {Name: "components"}: { Value: cty.ListVal([]cty.Value{cty.StringVal("one"), cty.StringVal("two"), cty.StringVal("three")}), DefRange: tfdiags.SourceRange{}, }, }, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) _, diags := collectPlanOutput(changesCh, diagsCh) reportDiagnosticsForTest(t, diags) if len(diags) != 0 { t.FailNow() // We reported the diags above/ } } func TestPlanWithCheckableObjects(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, "checkable-objects") fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z") if err != nil { t.Fatal(err) } changesCh := make(chan stackplan.PlannedChange, 8) diagsCh := make(chan tfdiags.Diagnostic, 2) lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), providerreqs.MustParseVersion("0.0.0"), providerreqs.MustParseVersionConstraints("=0.0.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ Config: cfg, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, DependencyLocks: *lock, ForcePlanTimestamp: &fakePlanTimestamp, InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ {Name: "foo"}: { Value: cty.StringVal("bar"), }, }, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } var wantDiags tfdiags.Diagnostics wantDiags = wantDiags.Append(&hcl.Diagnostic{ Severity: hcl.DiagWarning, Summary: "Check block assertion failed", Detail: `value must be 'baz'`, Subject: &hcl.Range{ Filename: mainBundleSourceAddrStr("checkable-objects/checkable-objects.tf"), Start: hcl.Pos{Line: 41, Column: 21, Byte: 716}, End: hcl.Pos{Line: 41, Column: 57, Byte: 752}, }, }) go Plan(ctx, &req, &resp) gotChanges, gotDiags := collectPlanOutput(changesCh, diagsCh) if diff := cmp.Diff(wantDiags.ForRPC(), gotDiags.ForRPC()); diff != "" { t.Errorf("wrong diagnostics\n%s", diff) } // The order of emission for our planned changes is unspecified since it // depends on how the various goroutines get scheduled, and so we'll // arbitrarily sort gotChanges lexically by the name of the change type // so that we have some dependable order to diff against below. sort.Slice(gotChanges, func(i, j int) bool { ic := gotChanges[i] jc := gotChanges[j] return fmt.Sprintf("%T", ic) < fmt.Sprintf("%T", jc) }) wantChanges := []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: stackaddrs.Absolute( stackaddrs.RootStackInstance, stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "single"}, }, ), Action: plans.Create, PlanApplyable: true, PlanComplete: true, PlannedInputValues: map[string]plans.DynamicValue{ "foo": mustPlanDynamicValueDynamicType(cty.StringVal("bar")), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{"foo": nil}, PlannedOutputValues: map[string]cty.Value{ "foo": cty.StringVal("bar"), }, PlannedCheckResults: &states.CheckResults{ ConfigResults: addrs.MakeMap( addrs.MakeMapElem[addrs.ConfigCheckable]( addrs.Check{ Name: "value_is_baz", }.InModule(addrs.RootModule), &states.CheckResultAggregate{ Status: checks.StatusFail, ObjectResults: addrs.MakeMap( addrs.MakeMapElem[addrs.Checkable]( addrs.Check{ Name: "value_is_baz", }.Absolute(addrs.RootModuleInstance), &states.CheckResultObject{ Status: checks.StatusFail, FailureMessages: []string{"value must be 'baz'"}, }, ), ), }, ), addrs.MakeMapElem[addrs.ConfigCheckable]( addrs.InputVariable{ Name: "foo", }.InModule(addrs.RootModule), &states.CheckResultAggregate{ Status: checks.StatusPass, ObjectResults: addrs.MakeMap( addrs.MakeMapElem[addrs.Checkable]( addrs.InputVariable{ Name: "foo", }.Absolute(addrs.RootModuleInstance), &states.CheckResultObject{ Status: checks.StatusPass, }, ), ), }, ), addrs.MakeMapElem[addrs.ConfigCheckable]( addrs.OutputValue{ Name: "foo", }.InModule(addrs.RootModule), &states.CheckResultAggregate{ Status: checks.StatusPass, ObjectResults: addrs.MakeMap( addrs.MakeMapElem[addrs.Checkable]( addrs.OutputValue{ Name: "foo", }.Absolute(addrs.RootModuleInstance), &states.CheckResultObject{ Status: checks.StatusPass, }, ), ), }, ), addrs.MakeMapElem[addrs.ConfigCheckable]( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "main", }.InModule(addrs.RootModule), &states.CheckResultAggregate{ Status: checks.StatusPass, ObjectResults: addrs.MakeMap( addrs.MakeMapElem[addrs.Checkable]( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "main", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.CheckResultObject{ Status: checks.StatusPass, }, ), ), }, ), ), }, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ Component: stackaddrs.Absolute( stackaddrs.RootStackInstance, stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "single"}, }, ), Item: addrs.AbsResourceInstanceObject{ ResourceInstance: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "main", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), }, }, ProviderConfigAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("testing"), }, ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "main", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), PrevRunAddr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "main", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), ProviderAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("testing"), }, ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("test"), "value": cty.StringVal("bar"), }), stacks_testing_provider.TestingResourceSchema.Body), }, }, Schema: stacks_testing_provider.TestingResourceSchema, }, } if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func TestPlanWithDeferredResource(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, "deferrable-component") fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z") if err != nil { t.Fatal(err) } changesCh := make(chan stackplan.PlannedChange) diagsCh := make(chan tfdiags.Diagnostic) lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), providerreqs.MustParseVersion("0.0.0"), providerreqs.MustParseVersionConstraints("=0.0.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ Config: cfg, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, DependencyLocks: *lock, ForcePlanTimestamp: &fakePlanTimestamp, InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ {Name: "id"}: { Value: cty.StringVal("62594ae3"), }, {Name: "defer"}: { Value: cty.BoolVal(true), }, }, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) gotChanges, diags := collectPlanOutput(changesCh, diagsCh) reportDiagnosticsForTest(t, diags) if len(diags) != 0 { t.FailNow() // We reported the diags above } sort.SliceStable(gotChanges, func(i, j int) bool { return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) }) wantChanges := []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: stackaddrs.Absolute( stackaddrs.RootStackInstance, stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "self"}, }, ), PlanComplete: false, PlanApplyable: false, // We don't have any resources to apply since they're deferred. Action: plans.Create, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.StringVal("62594ae3")), "defer": mustPlanDynamicValueDynamicType(cty.BoolVal(true)), }, PlannedOutputValues: map[string]cty.Value{}, PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "id": nil, "defer": nil, }, }, &stackplan.PlannedChangeDeferredResourceInstancePlanned{ ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ Component: stackaddrs.Absolute( stackaddrs.RootStackInstance, stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "self"}, }, ), Item: addrs.AbsResourceInstanceObject{ ResourceInstance: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_deferred_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), }, }, ProviderConfigAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_deferred_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), PrevRunAddr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_deferred_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), ProviderAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("62594ae3"), "value": cty.NullVal(cty.String), "deferred": cty.BoolVal(true), }), stacks_testing_provider.DeferredResourceSchema.Body), AfterSensitivePaths: nil, }, }, Schema: stacks_testing_provider.DeferredResourceSchema, }, DeferredReason: providers.DeferredReasonResourceConfigUnknown, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "defer"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.BoolVal(true), }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "id"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.StringVal("62594ae3"), }, } if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func TestPlanWithDeferredComponentForEach(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input-and-output", "deferred-component-for-each")) fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") if err != nil { t.Fatal(err) } changesCh := make(chan stackplan.PlannedChange, 8) diagsCh := make(chan tfdiags.Diagnostic, 2) lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), providerreqs.MustParseVersion("0.0.0"), providerreqs.MustParseVersionConstraints("=0.0.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ Config: cfg, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, DependencyLocks: *lock, ForcePlanTimestamp: &fakePlanTimestamp, InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ {Name: "components"}: { Value: cty.UnknownVal(cty.Set(cty.String)), DefRange: tfdiags.SourceRange{}, }, }, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) gotChanges, diags := collectPlanOutput(changesCh, diagsCh) reportDiagnosticsForTest(t, diags) if len(diags) != 0 { t.FailNow() // We reported the diags above/ } sort.SliceStable(gotChanges, func(i, j int) bool { return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) }) wantChanges := []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: stackaddrs.Absolute( stackaddrs.RootStackInstance, stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "child"}, }, ), PlanApplyable: true, PlanComplete: false, Action: plans.Create, RequiredComponents: collections.NewSet[stackaddrs.AbsComponent]( stackaddrs.AbsComponent{ Stack: stackaddrs.RootStackInstance, Item: stackaddrs.Component{ Name: "self", }, }, ), PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)), "input": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "id": nil, "input": nil, }, PlannedOutputValues: map[string]cty.Value{ "id": cty.UnknownVal(cty.String), }, PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeDeferredResourceInstancePlanned{ ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ Component: stackaddrs.AbsComponentInstance{ Stack: stackaddrs.RootStackInstance, Item: stackaddrs.ComponentInstance{ Component: stackaddrs.Component{ Name: "child", }, }, }, Item: addrs.AbsResourceInstanceObject{ ResourceInstance: addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }, Key: addrs.NoKey, }, }, }, }, ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), PrevRunAddr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), ProviderAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.UnknownVal(cty.String), }), stacks_testing_provider.TestingResourceSchema.Body), AfterSensitivePaths: nil, }, }, ProviderConfigAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, Schema: stacks_testing_provider.TestingResourceSchema, }, DeferredReason: providers.DeferredReasonDeferredPrereq, }, &stackplan.PlannedChangeComponentInstance{ Addr: stackaddrs.Absolute( stackaddrs.RootStackInstance, stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "self"}, Key: addrs.WildcardKey, }, ), PlanApplyable: true, // TODO: Questionable? We only have outputs. PlanComplete: false, Action: plans.Create, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)), "input": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)), }, PlannedOutputValues: map[string]cty.Value{ "id": cty.UnknownVal(cty.String), }, PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "id": nil, "input": nil, }, }, &stackplan.PlannedChangeDeferredResourceInstancePlanned{ DeferredReason: providers.DeferredReasonDeferredPrereq, ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ Component: stackaddrs.Absolute( stackaddrs.RootStackInstance, stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "self"}, Key: addrs.WildcardKey, }, ), Item: addrs.AbsResourceInstanceObject{ ResourceInstance: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), }, }, ProviderConfigAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), PrevRunAddr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), ProviderAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.UnknownVal(cty.String), }), stacks_testing_provider.TestingResourceSchema.Body), AfterSensitivePaths: nil, }, }, Schema: stacks_testing_provider.TestingResourceSchema, }, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "components"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.UnknownVal(cty.Set(cty.String)), }, } if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func TestPlanWithDeferredComponentReferences(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input-and-output", "deferred-component-references")) fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") if err != nil { t.Fatal(err) } changesCh := make(chan stackplan.PlannedChange, 8) diagsCh := make(chan tfdiags.Diagnostic, 2) lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), providerreqs.MustParseVersion("0.0.0"), providerreqs.MustParseVersionConstraints("=0.0.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ Config: cfg, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, DependencyLocks: *lock, ForcePlanTimestamp: &fakePlanTimestamp, InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ {Name: "known_components"}: { Value: cty.ListVal([]cty.Value{cty.StringVal("known")}), DefRange: tfdiags.SourceRange{}, }, {Name: "unknown_components"}: { Value: cty.UnknownVal(cty.Set(cty.String)), DefRange: tfdiags.SourceRange{}, }, }, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) gotChanges, diags := collectPlanOutput(changesCh, diagsCh) reportDiagnosticsForTest(t, diags) if len(diags) != 0 { t.FailNow() // We reported the diags above. } sort.SliceStable(gotChanges, func(i, j int) bool { return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) }) wantChanges := []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: stackaddrs.Absolute( stackaddrs.RootStackInstance, stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "children"}, Key: addrs.WildcardKey, }, ), PlanApplyable: true, // TODO: Questionable? We only have outputs. PlanComplete: false, Action: plans.Create, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)), "input": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "id": nil, "input": nil, }, PlannedOutputValues: map[string]cty.Value{ "id": cty.UnknownVal(cty.String), }, PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, RequiredComponents: collections.NewSet[stackaddrs.AbsComponent]( stackaddrs.AbsComponent{ Stack: stackaddrs.RootStackInstance, Item: stackaddrs.Component{ Name: "self", }, }, ), }, &stackplan.PlannedChangeDeferredResourceInstancePlanned{ DeferredReason: providers.DeferredReasonDeferredPrereq, ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ Component: stackaddrs.Absolute( stackaddrs.RootStackInstance, stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "children"}, Key: addrs.WildcardKey, }, ), Item: addrs.AbsResourceInstanceObject{ ResourceInstance: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), }, }, ProviderConfigAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), PrevRunAddr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), ProviderAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.UnknownVal(cty.String), }), stacks_testing_provider.TestingResourceSchema.Body), AfterSensitivePaths: nil, }, }, Schema: stacks_testing_provider.TestingResourceSchema, }, }, &stackplan.PlannedChangeComponentInstance{ Addr: stackaddrs.Absolute( stackaddrs.RootStackInstance, stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "self"}, Key: addrs.StringKey("known"), }), PlanApplyable: true, PlanComplete: true, Action: plans.Create, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)), "input": mustPlanDynamicValueDynamicType(cty.StringVal("known")), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "id": nil, "input": nil, }, PlannedOutputValues: map[string]cty.Value{ "id": cty.UnknownVal(cty.String), }, PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ Component: stackaddrs.AbsComponentInstance{ Stack: stackaddrs.RootStackInstance, Item: stackaddrs.ComponentInstance{ Component: stackaddrs.Component{ Name: "self", }, Key: addrs.StringKey("known"), }, }, Item: addrs.AbsResourceInstanceObject{ ResourceInstance: addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }, Key: addrs.NoKey, }, }, }, }, ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }, Key: addrs.NoKey, }, }, PrevRunAddr: addrs.AbsResourceInstance{ Module: addrs.RootModuleInstance, Resource: addrs.ResourceInstance{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }, Key: addrs.NoKey, }, }, ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.StringVal("known"), }), stacks_testing_provider.TestingResourceSchema.Body), }, ProviderAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, }, ProviderConfigAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, Schema: stacks_testing_provider.TestingResourceSchema, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "known_components"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.SetVal([]cty.Value{cty.StringVal("known")}), }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "unknown_components"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.UnknownVal(cty.Set(cty.String)), }, } if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func TestPlanWithDeferredComponentForEachOfInvalidType(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, "deferred-component-for-each-from-component-of-invalid-type") fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") if err != nil { t.Fatal(err) } changesCh := make(chan stackplan.PlannedChange, 8) diagsCh := make(chan tfdiags.Diagnostic, 2) lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), providerreqs.MustParseVersion("0.0.0"), providerreqs.MustParseVersionConstraints("=0.0.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ Config: cfg, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, DependencyLocks: *lock, ForcePlanTimestamp: &fakePlanTimestamp, InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ {Name: "components"}: { Value: cty.UnknownVal(cty.Set(cty.String)), DefRange: tfdiags.SourceRange{}, }, }, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) _, diags := collectPlanOutput(changesCh, diagsCh) if len(diags) != 1 { t.Fatalf("expected 1 diagnostic, got %d: %s", len(diags), diags) } if diags[0].Severity() != tfdiags.Error { t.Errorf("expected error diagnostic, got %q", diags[0].Severity()) } expectedSummary := "Invalid for_each value" if diags[0].Description().Summary != expectedSummary { t.Errorf("expected diagnostic with summary %q, got %q", expectedSummary, diags[0].Description().Summary) } expectedDetail := "The for_each expression must produce either a map of any type or a set of strings. The keys of the map or the set elements will serve as unique identifiers for multiple instances of this component." if diags[0].Description().Detail != expectedDetail { t.Errorf("expected diagnostic with detail %q, got %q", expectedDetail, diags[0].Description().Detail) } } func TestPlanWithDeferredProviderForEach(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input", "deferred-provider-for-each")) fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") if err != nil { t.Fatal(err) } changesCh := make(chan stackplan.PlannedChange) diagsCh := make(chan tfdiags.Diagnostic) lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), providerreqs.MustParseVersion("0.0.0"), providerreqs.MustParseVersionConstraints("=0.0.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ Config: cfg, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, DependencyLocks: *lock, ForcePlanTimestamp: &fakePlanTimestamp, InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ {Name: "providers"}: { Value: cty.UnknownVal(cty.Set(cty.String)), DefRange: tfdiags.SourceRange{}, }, }, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) gotChanges, diags := collectPlanOutput(changesCh, diagsCh) reportDiagnosticsForTest(t, diags) if len(diags) != 0 { t.FailNow() // We reported the diags above } sort.SliceStable(gotChanges, func(i, j int) bool { return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) }) wantChanges := []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: stackaddrs.Absolute( stackaddrs.RootStackInstance, stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "known"}, }), PlanComplete: false, PlanApplyable: false, Action: plans.Create, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)), "input": mustPlanDynamicValueDynamicType(cty.StringVal("primary")), }, PlannedOutputValues: map[string]cty.Value{}, PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "id": nil, "input": nil, }, }, &stackplan.PlannedChangeDeferredResourceInstancePlanned{ ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ Component: stackaddrs.Absolute( stackaddrs.RootStackInstance, stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "known"}, }, ), Item: addrs.AbsResourceInstanceObject{ ResourceInstance: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), }, }, ProviderConfigAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), PrevRunAddr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), ProviderAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.StringVal("primary"), }), stacks_testing_provider.TestingResourceSchema.Body), }, }, Schema: stacks_testing_provider.TestingResourceSchema, }, DeferredReason: providers.DeferredReasonProviderConfigUnknown, }, &stackplan.PlannedChangeComponentInstance{ Addr: stackaddrs.Absolute( stackaddrs.RootStackInstance, stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "unknown"}, Key: addrs.WildcardKey, }), PlanComplete: false, PlanApplyable: false, Action: plans.Create, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)), "input": mustPlanDynamicValueDynamicType(cty.StringVal("secondary")), }, PlannedOutputValues: map[string]cty.Value{}, PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "id": nil, "input": nil, }, }, &stackplan.PlannedChangeDeferredResourceInstancePlanned{ ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ Component: stackaddrs.Absolute( stackaddrs.RootStackInstance, stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "unknown"}, Key: addrs.WildcardKey, }, ), Item: addrs.AbsResourceInstanceObject{ ResourceInstance: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), }, }, ProviderConfigAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), PrevRunAddr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_resource", Name: "data", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), ProviderAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.StringVal("secondary"), }), stacks_testing_provider.TestingResourceSchema.Body), }, }, Schema: stacks_testing_provider.TestingResourceSchema, }, DeferredReason: providers.DeferredReasonProviderConfigUnknown, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "providers"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.UnknownVal(cty.Set(cty.String)), }, } if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func TestPlanInvalidProvidersFailGracefully(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, path.Join("invalid-providers")) fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") if err != nil { t.Fatal(err) } changesCh := make(chan stackplan.PlannedChange) diagsCh := make(chan tfdiags.Diagnostic) lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), providerreqs.MustParseVersion("0.0.0"), providerreqs.MustParseVersionConstraints("=0.0.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ Config: cfg, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, DependencyLocks: *lock, ForcePlanTimestamp: &fakePlanTimestamp, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) changes, diags := collectPlanOutput(changesCh, diagsCh) sort.SliceStable(diags, diagnosticSortFunc(diags)) expectDiagnosticsForTest(t, diags, expectDiagnostic(tfdiags.Error, "Provider configuration is invalid", "Cannot plan changes for this resource because its associated provider configuration is invalid."), expectDiagnostic(tfdiags.Error, "invalid configuration", "configure_error attribute was set")) sort.SliceStable(changes, func(i, j int) bool { return plannedChangeSortKey(changes[i]) < plannedChangeSortKey(changes[j]) }) wantChanges := []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{}, &stackplan.PlannedChangeComponentInstance{ Addr: stackaddrs.Absolute( stackaddrs.RootStackInstance, stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "self"}, }, ), Action: plans.Create, PlanTimestamp: fakePlanTimestamp, PlannedInputValues: make(map[string]plans.DynamicValue), PlannedOutputValues: make(map[string]cty.Value), PlannedCheckResults: &states.CheckResults{}, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, } if diff := cmp.Diff(wantChanges, changes, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func TestPlanWithStateManipulation(t *testing.T) { fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") if err != nil { t.Fatal(err) } lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), providerreqs.MustParseVersion("0.0.0"), providerreqs.MustParseVersionConstraints("=0.0.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) tcs := map[string]struct { state *stackstate.State store *stacks_testing_provider.ResourceStore inputs map[string]cty.Value changes []stackplan.PlannedChange counts collections.Map[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange] expectedWarnings []string }{ "moved": { state: stackstate.NewStateBuilder(). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.before")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "moved", "value": "moved", }), })). Build(), store: stacks_testing_provider.NewResourceStoreBuilder(). AddResource("moved", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("moved"), "value": cty.StringVal("moved"), })). Build(), changes: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.self"), PlanApplyable: true, PlanComplete: true, Action: plans.Update, PlannedInputValues: make(map[string]plans.DynamicValue), PlannedOutputValues: make(map[string]cty.Value), PlannedCheckResults: &states.CheckResults{}, RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](), PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.after"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource.after"), PrevRunAddr: mustAbsResourceInstance("testing_resource.before"), ProviderAddr: mustDefaultRootProvider("testing"), ChangeSrc: plans.ChangeSrc{ Action: plans.NoOp, Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("moved"), "value": cty.StringVal("moved"), })), After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("moved"), "value": cty.StringVal("moved"), })), }, }, PriorStateSrc: &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "moved", "value": "moved", }), Dependencies: make([]addrs.ConfigResource, 0), }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, }, counts: collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]( collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{ K: mustAbsComponentInstance("component.self"), V: &hooks.ComponentInstanceChange{ Addr: mustAbsComponentInstance("component.self"), Move: 1, }, }), }, "cross-type-moved": { state: stackstate.NewStateBuilder(). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.before")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "moved", "value": "moved", }), })). Build(), store: stacks_testing_provider.NewResourceStoreBuilder(). AddResource("moved", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("moved"), "value": cty.StringVal("moved"), })). Build(), changes: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.self"), PlanApplyable: true, PlanComplete: true, Action: plans.Update, PlannedInputValues: make(map[string]plans.DynamicValue), PlannedOutputValues: make(map[string]cty.Value), PlannedCheckResults: &states.CheckResults{}, RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](), PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_deferred_resource.after"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_deferred_resource.after"), PrevRunAddr: mustAbsResourceInstance("testing_resource.before"), ProviderAddr: mustDefaultRootProvider("testing"), ChangeSrc: plans.ChangeSrc{ Action: plans.NoOp, Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("moved"), "value": cty.StringVal("moved"), "deferred": cty.False, })), After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("moved"), "value": cty.StringVal("moved"), "deferred": cty.False, })), }, }, PriorStateSrc: &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "moved", "value": "moved", "deferred": false, }), Dependencies: make([]addrs.ConfigResource, 0), }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.DeferredResourceSchema, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, }, counts: collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]( collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{ K: mustAbsComponentInstance("component.self"), V: &hooks.ComponentInstanceChange{ Addr: mustAbsComponentInstance("component.self"), Move: 1, }, }), }, "import": { state: stackstate.NewStateBuilder().Build(), // We start with an empty state for this. store: stacks_testing_provider.NewResourceStoreBuilder(). AddResource("imported", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("imported"), "value": cty.StringVal("imported"), })). Build(), inputs: map[string]cty.Value{ "id": cty.StringVal("imported"), }, changes: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.self"), PlanApplyable: true, PlanComplete: true, // The component is still CREATE even though all the // instances are NoOps, because the component itself didn't // exist before even though all the resources might have. Action: plans.Create, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.StringVal("imported")), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "id": nil, }, PlannedOutputValues: make(map[string]cty.Value), PlannedCheckResults: &states.CheckResults{}, RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](), PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource.data"), PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), ProviderAddr: mustDefaultRootProvider("testing"), ChangeSrc: plans.ChangeSrc{ Action: plans.NoOp, Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("imported"), "value": cty.StringVal("imported"), })), After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("imported"), "value": cty.StringVal("imported"), })), Importing: &plans.ImportingSrc{ ID: "imported", }, }, }, PriorStateSrc: &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "imported", "value": "imported", }), Dependencies: make([]addrs.ConfigResource, 0), }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{ Name: "id", }, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.StringVal("imported"), RequiredOnApply: false, }, }, counts: collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]( collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{ K: mustAbsComponentInstance("component.self"), V: &hooks.ComponentInstanceChange{ Addr: mustAbsComponentInstance("component.self"), Import: 1, }, }), }, "removed": { state: stackstate.NewStateBuilder(). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.resource")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "removed", "value": "removed", }), })). Build(), store: stacks_testing_provider.NewResourceStoreBuilder(). AddResource("removed", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("removed"), "value": cty.StringVal("removed"), })). Build(), changes: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.self"), PlanApplyable: true, PlanComplete: true, Action: plans.Update, PlannedInputValues: make(map[string]plans.DynamicValue), PlannedOutputValues: make(map[string]cty.Value), PlannedCheckResults: &states.CheckResults{}, RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](), PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.resource"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource.resource"), PrevRunAddr: mustAbsResourceInstance("testing_resource.resource"), ProviderAddr: mustDefaultRootProvider("testing"), ChangeSrc: plans.ChangeSrc{ Action: plans.Forget, Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("removed"), "value": cty.StringVal("removed"), })), After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ "id": cty.String, "value": cty.String, }))), }, ActionReason: plans.ResourceInstanceDeleteBecauseNoResourceConfig, }, PriorStateSrc: &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "removed", "value": "removed", }), Dependencies: make([]addrs.ConfigResource, 0), }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, }, counts: collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]( collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{ K: mustAbsComponentInstance("component.self"), V: &hooks.ComponentInstanceChange{ Addr: mustAbsComponentInstance("component.self"), Forget: 1, }, }), expectedWarnings: []string{"Some objects will no longer be managed by Terraform"}, }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, path.Join("state-manipulation", name)) gotCounts := collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]() ctx = ContextWithHooks(ctx, &stackeval.Hooks{ ReportComponentInstancePlanned: func(ctx context.Context, span any, change *hooks.ComponentInstanceChange) any { gotCounts.Put(change.Addr, change) return span }, }) inputs := make(map[stackaddrs.InputVariable]ExternalInputValue, len(tc.inputs)) for name, input := range tc.inputs { inputs[stackaddrs.InputVariable{Name: name}] = ExternalInputValue{ Value: input, } } changesCh := make(chan stackplan.PlannedChange) diagsCh := make(chan tfdiags.Diagnostic) req := PlanRequest{ Config: cfg, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProviderWithData(t, tc.store), nil }, }, DependencyLocks: *lock, InputValues: inputs, ForcePlanTimestamp: &fakePlanTimestamp, PrevState: tc.state, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) changes, diags := collectPlanOutput(changesCh, diagsCh) reportDiagnosticsForTest(t, diags) if len(diags) > len(tc.expectedWarnings) { t.Fatalf("had unexpected warnings") } for i, diag := range diags { if diag.Description().Summary != tc.expectedWarnings[i] { t.Fatalf("expected diagnostic with summary %q, got %q", tc.expectedWarnings[i], diag.Description().Summary) } } sort.SliceStable(changes, func(i, j int) bool { return plannedChangeSortKey(changes[i]) < plannedChangeSortKey(changes[j]) }) if diff := cmp.Diff(tc.changes, changes, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } wantCounts := tc.counts for key, elem := range wantCounts.All() { // First, make sure everything we wanted is present. if !gotCounts.HasKey(key) { t.Errorf("wrong counts: wanted %s but didn't get it", key) } // And that the values actually match. got, want := gotCounts.Get(key), elem if diff := cmp.Diff(want, got); diff != "" { t.Errorf("wrong counts for %s: %s", want.Addr, diff) } } for key := range gotCounts.All() { // Then, make sure we didn't get anything we didn't want. if !wantCounts.HasKey(key) { t.Errorf("wrong counts: got %s but didn't want it", key) } } }) } } func TestPlan_plantimestamp_force_timestamp(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, "with-plantimestamp") forcedPlanTimestamp := "1991-08-25T20:57:08Z" fakePlanTimestamp, err := time.Parse(time.RFC3339, forcedPlanTimestamp) if err != nil { t.Fatal(err) } changesCh := make(chan stackplan.PlannedChange, 8) diagsCh := make(chan tfdiags.Diagnostic, 2) req := PlanRequest{ Config: cfg, ProviderFactories: map[addrs.Provider]providers.Factory{ // We support both hashicorp/testing and // terraform.io/builtin/testing as providers. This lets us // test the provider aliasing feature. Both providers // support the same set of resources and data sources. addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, addrs.NewBuiltInProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, InputValues: func() map[stackaddrs.InputVariable]ExternalInputValue { return map[stackaddrs.InputVariable]ExternalInputValue{} }(), ForcePlanTimestamp: &fakePlanTimestamp, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) gotChanges, diags := collectPlanOutput(changesCh, diagsCh) // The following will fail the test if there are any error // diagnostics. reportDiagnosticsForTest(t, diags) // We also want to fail if there are just warnings, since the // configurations here are supposed to be totally problem-free. if len(diags) != 0 { // reportDiagnosticsForTest already showed the diagnostics in // the log t.FailNow() } sort.SliceStable(gotChanges, func(i, j int) bool { return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) }) wantChanges := []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: stackaddrs.Absolute( stackaddrs.RootStackInstance, stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "second-self"}, }, ), Action: plans.Create, PlanApplyable: true, PlanComplete: true, PlannedCheckResults: &states.CheckResults{}, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "value": nil, }, PlannedInputValues: map[string]plans.DynamicValue{ "value": mustPlanDynamicValueDynamicType(cty.StringVal(forcedPlanTimestamp)), }, PlannedOutputValues: map[string]cty.Value{ "input": cty.StringVal(forcedPlanTimestamp), "out": cty.StringVal(fmt.Sprintf("module-output-%s", forcedPlanTimestamp)), }, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeComponentInstance{ Addr: stackaddrs.Absolute( stackaddrs.RootStackInstance, stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "self"}, }, ), Action: plans.Create, PlanApplyable: true, PlanComplete: true, PlannedCheckResults: &states.CheckResults{}, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "value": nil, }, PlannedInputValues: map[string]plans.DynamicValue{ "value": mustPlanDynamicValueDynamicType(cty.StringVal(forcedPlanTimestamp)), }, PlannedOutputValues: map[string]cty.Value{ "input": cty.StringVal(forcedPlanTimestamp), "out": cty.StringVal(fmt.Sprintf("module-output-%s", forcedPlanTimestamp)), }, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangeOutputValue{ Addr: stackaddrs.OutputValue{Name: "plantimestamp"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.StringVal(forcedPlanTimestamp), }, &stackplan.PlannedChangePlannedTimestamp{PlannedTimestamp: fakePlanTimestamp}, } if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func TestPlan_plantimestamp_later_than_when_writing_this_test(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, "with-plantimestamp") dayOfWritingThisTest := "2024-06-21T06:37:08Z" dayOfWritingThisTestTime, err := time.Parse(time.RFC3339, dayOfWritingThisTest) if err != nil { t.Fatal(err) } changesCh := make(chan stackplan.PlannedChange, 8) diagsCh := make(chan tfdiags.Diagnostic, 2) req := PlanRequest{ Config: cfg, ProviderFactories: map[addrs.Provider]providers.Factory{ // We support both hashicorp/testing and // terraform.io/builtin/testing as providers. This lets us // test the provider aliasing feature. Both providers // support the same set of resources and data sources. addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, addrs.NewBuiltInProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, InputValues: func() map[stackaddrs.InputVariable]ExternalInputValue { return map[stackaddrs.InputVariable]ExternalInputValue{} }(), ForcePlanTimestamp: nil, // This is what we want to test } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) changes, diags := collectPlanOutput(changesCh, diagsCh) output := expectOutput(t, "plantimestamp", changes) plantimestampValue := output.After plantimestamp, err := time.Parse(time.RFC3339, plantimestampValue.AsString()) if err != nil { t.Fatal(err) } if plantimestamp.Before(dayOfWritingThisTestTime) { t.Errorf("expected plantimestamp to be later than %q, got %q", dayOfWritingThisTest, plantimestampValue.AsString()) } // The following will fail the test if there are any error // diagnostics. reportDiagnosticsForTest(t, diags) // We also want to fail if there are just warnings, since the // configurations here are supposed to be totally problem-free. if len(diags) != 0 { // reportDiagnosticsForTest already showed the diagnostics in // the log t.FailNow() } } func TestPlan_DependsOnUpdatesRequirements(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input", "depends-on")) fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") if err != nil { t.Fatal(err) } lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), providerreqs.MustParseVersion("0.0.0"), providerreqs.MustParseVersionConstraints("=0.0.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) changesCh := make(chan stackplan.PlannedChange) diagsCh := make(chan tfdiags.Diagnostic) req := PlanRequest{ Config: cfg, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, DependencyLocks: *lock, ForcePlanTimestamp: &fakePlanTimestamp, InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ {Name: "input"}: { Value: cty.StringVal("hello, world!"), }, }, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) gotChanges, diags := collectPlanOutput(changesCh, diagsCh) reportDiagnosticsForTest(t, diags) if len(diags) != 0 { t.FailNow() } sort.SliceStable(gotChanges, func(i, j int) bool { return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) }) wantChanges := []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.first"), PlanApplyable: true, PlanComplete: true, Action: plans.Create, RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](), PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)), "input": mustPlanDynamicValueDynamicType(cty.StringVal("hello, world!")), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "id": nil, "input": nil, }, PlanTimestamp: fakePlanTimestamp, PlannedOutputValues: make(map[string]cty.Value), PlannedCheckResults: &states.CheckResults{}, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.first.testing_resource.data"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource.data"), PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), ProviderAddr: mustDefaultRootProvider("testing"), ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ "id": cty.String, "value": cty.String, }))), After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.StringVal("hello, world!"), })), }, }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.second"), PlanApplyable: true, PlanComplete: true, Action: plans.Create, RequiredComponents: collections.NewSet[stackaddrs.AbsComponent]( mustAbsComponent("component.first"), mustAbsComponent("stack.second.component.self"), ), PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)), "input": mustPlanDynamicValueDynamicType(cty.StringVal("hello, world!")), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "id": nil, "input": nil, }, PlanTimestamp: fakePlanTimestamp, PlannedOutputValues: make(map[string]cty.Value), PlannedCheckResults: &states.CheckResults{}, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.second.testing_resource.data"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource.data"), PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), ProviderAddr: mustDefaultRootProvider("testing"), ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ "id": cty.String, "value": cty.String, }))), After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.StringVal("hello, world!"), })), }, }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("stack.first.component.self"), PlanApplyable: true, PlanComplete: true, Action: plans.Create, RequiredComponents: collections.NewSet[stackaddrs.AbsComponent]( mustAbsComponent("component.first"), mustAbsComponent("component.empty"), ), PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)), "input": mustPlanDynamicValueDynamicType(cty.StringVal("hello, world!")), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "id": nil, "input": nil, }, PlanTimestamp: fakePlanTimestamp, PlannedOutputValues: make(map[string]cty.Value), PlannedCheckResults: &states.CheckResults{}, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.first.component.self.testing_resource.data"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource.data"), PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), ProviderAddr: mustDefaultRootProvider("testing"), ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ "id": cty.String, "value": cty.String, }))), After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.StringVal("hello, world!"), })), }, }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("stack.second.component.self"), PlanApplyable: true, PlanComplete: true, Action: plans.Create, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)), "input": mustPlanDynamicValueDynamicType(cty.StringVal("hello, world!")), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "id": nil, "input": nil, }, PlanTimestamp: fakePlanTimestamp, PlannedOutputValues: make(map[string]cty.Value), PlannedCheckResults: &states.CheckResults{}, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.second.component.self.testing_resource.data"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource.data"), PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), ProviderAddr: mustDefaultRootProvider("testing"), ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ "id": cty.String, "value": cty.String, }))), After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.StringVal("hello, world!"), })), }, }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{ Name: "empty", }, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.SetValEmpty(cty.String), }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{ Name: "input", }, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.StringVal("hello, world!"), }, } if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func TestPlan_RemovedBlocks(t *testing.T) { fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") if err != nil { t.Fatal(err) } lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), providerreqs.MustParseVersion("0.0.0"), providerreqs.MustParseVersionConstraints("=0.0.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) tcs := map[string]struct { source string initialState *stackstate.State store *stacks_testing_provider.ResourceStore inputs map[string]cty.Value wantPlanChanges []stackplan.PlannedChange wantPlanDiags []expectedDiagnostic }{ "unknown removed block with nothing to remove": { source: filepath.Join("with-single-input", "removed-component-instance"), initialState: stackstate.NewStateBuilder(). // we have a single component instance in state AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"a\"]")). AddInputVariable("id", cty.StringVal("a")). AddInputVariable("input", cty.StringVal("a"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "a", "value": "a", }), })). Build(), store: stacks_testing_provider.NewResourceStoreBuilder(). AddResource("a", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("a"), "value": cty.StringVal("a"), })). Build(), inputs: map[string]cty.Value{ "input": cty.SetVal([]cty.Value{ cty.StringVal("a"), }), "removed": cty.UnknownVal(cty.Set(cty.String)), }, wantPlanChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.self[\"a\"]"), PlanComplete: true, PlanApplyable: false, // all changes are no-ops Action: plans.Update, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.StringVal("a")), "input": mustPlanDynamicValueDynamicType(cty.StringVal("a")), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "input": nil, "id": nil, }, PlannedOutputValues: make(map[string]cty.Value), PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource.data"), PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), ChangeSrc: plans.ChangeSrc{ Action: plans.NoOp, Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("a"), "value": cty.StringVal("a"), })), After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("a"), "value": cty.StringVal("a"), })), }, ProviderAddr: mustDefaultRootProvider("testing"), }, PriorStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "a", "value": "a", }), Status: states.ObjectReady, Dependencies: make([]addrs.ConfigResource, 0), }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "input"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.SetVal([]cty.Value{ cty.StringVal("a"), }), }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "removed"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.UnknownVal(cty.Set(cty.String)), }, }, }, "unknown removed block with elements in state": { source: filepath.Join("with-single-input", "removed-component-instance"), initialState: stackstate.NewStateBuilder(). // we have a single component instance in state AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"a\"]")). AddInputVariable("id", cty.StringVal("a")). AddInputVariable("input", cty.StringVal("a"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "a", "value": "a", }), })). Build(), store: stacks_testing_provider.NewResourceStoreBuilder(). AddResource("a", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("a"), "value": cty.StringVal("a"), })). Build(), inputs: map[string]cty.Value{ "input": cty.SetValEmpty(cty.String), "removed": cty.UnknownVal(cty.Set(cty.String)), }, wantPlanChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.self[\"a\"]"), PlanComplete: false, // has deferred changes PlanApplyable: false, // only deferred changes Action: plans.Delete, Mode: plans.DestroyMode, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.StringVal("a")), "input": mustPlanDynamicValueDynamicType(cty.StringVal("a")), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "input": nil, "id": nil, }, PlannedOutputValues: make(map[string]cty.Value), PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeDeferredResourceInstancePlanned{ ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource.data"), PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), ChangeSrc: plans.ChangeSrc{ Action: plans.Delete, Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("a"), "value": cty.StringVal("a"), })), After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ "id": cty.String, "value": cty.String, }))), }, ProviderAddr: mustDefaultRootProvider("testing"), }, PriorStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "a", "value": "a", }), Status: states.ObjectReady, Dependencies: make([]addrs.ConfigResource, 0), }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, DeferredReason: providers.DeferredReasonDeferredPrereq, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "input"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.SetValEmpty(cty.String), }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "removed"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.UnknownVal(cty.Set(cty.String)), }, }, }, "unknown component block with element to remove": { source: filepath.Join("with-single-input", "removed-component-instance"), initialState: stackstate.NewStateBuilder(). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"a\"]")). AddInputVariable("id", cty.StringVal("a")). AddInputVariable("input", cty.StringVal("a"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "a", "value": "a", }), })). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"b\"]")). AddInputVariable("id", cty.StringVal("b")). AddInputVariable("input", cty.StringVal("b"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("component.self[\"b\"].testing_resource.data")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "b", "value": "b", }), })). Build(), store: stacks_testing_provider.NewResourceStoreBuilder(). AddResource("a", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("a"), "value": cty.StringVal("a"), })). AddResource("b", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("b"), "value": cty.StringVal("b"), })). Build(), inputs: map[string]cty.Value{ "input": cty.UnknownVal(cty.Set(cty.String)), "removed": cty.SetVal([]cty.Value{cty.StringVal("b")}), }, wantPlanChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.self[\"a\"]"), PlanComplete: false, // has deferred changes PlanApplyable: false, // only deferred changes Action: plans.Update, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.StringVal("a")), "input": mustPlanDynamicValueDynamicType(cty.StringVal("a")), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "input": nil, "id": nil, }, PlannedOutputValues: make(map[string]cty.Value), PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeDeferredResourceInstancePlanned{ ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource.data"), PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), ChangeSrc: plans.ChangeSrc{ Action: plans.NoOp, Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("a"), "value": cty.StringVal("a"), })), After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("a"), "value": cty.StringVal("a"), })), }, ProviderAddr: mustDefaultRootProvider("testing"), }, PriorStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "a", "value": "a", }), Status: states.ObjectReady, Dependencies: make([]addrs.ConfigResource, 0), }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, DeferredReason: providers.DeferredReasonDeferredPrereq, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.self[\"b\"]"), PlanComplete: true, PlanApplyable: true, Action: plans.Delete, Mode: plans.DestroyMode, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.StringVal("b")), "input": mustPlanDynamicValueDynamicType(cty.StringVal("b")), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "input": nil, "id": nil, }, PlannedOutputValues: make(map[string]cty.Value), PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"b\"].testing_resource.data"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource.data"), PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), ChangeSrc: plans.ChangeSrc{ Action: plans.Delete, Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("b"), "value": cty.StringVal("b"), })), After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ "id": cty.String, "value": cty.String, }))), }, ProviderAddr: mustDefaultRootProvider("testing"), }, PriorStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "b", "value": "b", }), Status: states.ObjectReady, Dependencies: make([]addrs.ConfigResource, 0), }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "input"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.UnknownVal(cty.Set(cty.String)), }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "removed"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.SetVal([]cty.Value{cty.StringVal("b")}), }, }, }, "unknown component and removed block with element in state": { source: filepath.Join("with-single-input", "removed-component-instance"), initialState: stackstate.NewStateBuilder(). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"a\"]")). AddInputVariable("id", cty.StringVal("a")). AddInputVariable("input", cty.StringVal("a"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "a", "value": "a", }), })). Build(), store: stacks_testing_provider.NewResourceStoreBuilder(). AddResource("a", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("a"), "value": cty.StringVal("a"), })). Build(), inputs: map[string]cty.Value{ "input": cty.UnknownVal(cty.Set(cty.String)), "removed": cty.UnknownVal(cty.Set(cty.String)), }, wantPlanChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.self[\"a\"]"), PlanComplete: false, // has deferred changes PlanApplyable: false, // only deferred changes Action: plans.Update, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.StringVal("a")), "input": mustPlanDynamicValueDynamicType(cty.StringVal("a")), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "input": nil, "id": nil, }, PlannedOutputValues: make(map[string]cty.Value), PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeDeferredResourceInstancePlanned{ ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource.data"), PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), ChangeSrc: plans.ChangeSrc{ Action: plans.NoOp, Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("a"), "value": cty.StringVal("a"), })), After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("a"), "value": cty.StringVal("a"), })), }, ProviderAddr: mustDefaultRootProvider("testing"), }, PriorStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "a", "value": "a", }), Status: states.ObjectReady, Dependencies: make([]addrs.ConfigResource, 0), }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, DeferredReason: providers.DeferredReasonDeferredPrereq, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "input"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.UnknownVal(cty.Set(cty.String)), }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "removed"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.UnknownVal(cty.Set(cty.String)), }, }, }, "absent component": { source: filepath.Join("with-single-input", "removed-component"), wantPlanChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, }, }, "absent component instance": { source: filepath.Join("with-single-input", "removed-component-instance"), initialState: stackstate.NewStateBuilder(). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"removed\"]")). AddInputVariable("id", cty.StringVal("a")). AddInputVariable("input", cty.StringVal("a"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "a", "value": "a", }), })). Build(), store: stacks_testing_provider.NewResourceStoreBuilder(). AddResource("a", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("a"), "value": cty.StringVal("a"), })). Build(), inputs: map[string]cty.Value{ "input": cty.SetVal([]cty.Value{ cty.StringVal("a"), }), "removed": cty.SetVal([]cty.Value{ cty.StringVal("b"), // Doesn't exist! }), }, wantPlanChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, // we're expecting the new component to be created &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.self[\"a\"]"), PlanComplete: true, PlanApplyable: false, // no changes Action: plans.Update, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.StringVal("a")), "input": mustPlanDynamicValueDynamicType(cty.StringVal("a")), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "input": nil, "id": nil, }, PlannedOutputValues: make(map[string]cty.Value), PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource.data"), PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), ChangeSrc: plans.ChangeSrc{ Action: plans.NoOp, Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("a"), "value": cty.StringVal("a"), })), After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("a"), "value": cty.StringVal("a"), })), }, ProviderAddr: mustDefaultRootProvider("testing"), }, PriorStateSrc: &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "a", "value": "a", }), Dependencies: make([]addrs.ConfigResource, 0), }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackplan.PlannedChangeComponentInstanceRemoved{ Addr: mustAbsComponentInstance("component.self[\"removed\"]"), }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "input"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.SetVal([]cty.Value{ cty.StringVal("a"), }), }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "removed"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.SetVal([]cty.Value{ cty.StringVal("b"), }), }, }, }, "orphaned component": { source: filepath.Join("with-single-input", "removed-component-instance"), initialState: stackstate.NewStateBuilder(). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"removed\"]")). AddInputVariable("id", cty.StringVal("removed")). AddInputVariable("input", cty.StringVal("removed"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("component.self[\"removed\"].testing_resource.data")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "removed", "value": "removed", }), })). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"orphaned\"]")). AddInputVariable("id", cty.StringVal("orphaned")). AddInputVariable("input", cty.StringVal("orphaned"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("component.self[\"orphaned\"].testing_resource.data")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "orphaned", "value": "orphaned", }), })). Build(), store: stacks_testing_provider.NewResourceStoreBuilder(). AddResource("removed", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("removed"), "value": cty.StringVal("removed"), })). AddResource("orphaned", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("orphaned"), "value": cty.StringVal("orphaned"), })). Build(), inputs: map[string]cty.Value{ "input": cty.SetVal([]cty.Value{ cty.StringVal("added"), }), "removed": cty.SetVal([]cty.Value{ cty.StringVal("removed"), }), }, wantPlanChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: false, // No! We have an unclaimed instance! }, // we're expecting the new component to be created &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.self[\"added\"]"), PlanComplete: true, PlanApplyable: true, Action: plans.Create, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.StringVal("added")), "input": mustPlanDynamicValueDynamicType(cty.StringVal("added")), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "input": nil, "id": nil, }, PlannedOutputValues: make(map[string]cty.Value), PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"added\"].testing_resource.data"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource.data"), PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ "id": cty.String, "value": cty.String, }))), After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("added"), "value": cty.StringVal("added"), })), }, ProviderAddr: mustDefaultRootProvider("testing"), }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.self[\"removed\"]"), PlanComplete: true, PlanApplyable: true, Mode: plans.DestroyMode, Action: plans.Delete, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.StringVal("removed")), "input": mustPlanDynamicValueDynamicType(cty.StringVal("removed")), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "input": nil, "id": nil, }, PlannedOutputValues: make(map[string]cty.Value), PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"removed\"].testing_resource.data"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource.data"), PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), ChangeSrc: plans.ChangeSrc{ Action: plans.Delete, Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("removed"), "value": cty.StringVal("removed"), })), After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ "id": cty.String, "value": cty.String, }))), }, ProviderAddr: mustDefaultRootProvider("testing"), }, PriorStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "removed", "value": "removed", }), Dependencies: make([]addrs.ConfigResource, 0), Status: states.ObjectReady, }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "input"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.SetVal([]cty.Value{ cty.StringVal("added"), }), }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "removed"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.SetVal([]cty.Value{ cty.StringVal("removed"), }), }, }, wantPlanDiags: []expectedDiagnostic{ { severity: tfdiags.Error, summary: "Unclaimed component instance", detail: "The component instance component.self[\"orphaned\"] is not claimed by any component or removed block in the configuration. Make sure it is instantiated by a component block, or targeted for removal by a removed block.", }, }, }, "duplicate component": { source: filepath.Join("with-single-input", "removed-component-instance"), initialState: stackstate.NewStateBuilder(). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"a\"]")). AddInputVariable("id", cty.StringVal("a")). AddInputVariable("input", cty.StringVal("a"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "a", "value": "a", }), })). Build(), store: stacks_testing_provider.NewResourceStoreBuilder(). AddResource("a", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("a"), "value": cty.StringVal("a"), })). Build(), inputs: map[string]cty.Value{ "input": cty.SetVal([]cty.Value{ cty.StringVal("a"), }), "removed": cty.SetVal([]cty.Value{ cty.StringVal("a"), }), }, wantPlanChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: false, // No! The removed block is a duplicate of the component! }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.self[\"a\"]"), PlanComplete: true, PlanApplyable: false, // no changes Action: plans.Update, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.StringVal("a")), "input": mustPlanDynamicValueDynamicType(cty.StringVal("a")), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "input": nil, "id": nil, }, PlannedOutputValues: make(map[string]cty.Value), PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource.data"), PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), ChangeSrc: plans.ChangeSrc{ Action: plans.NoOp, Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("a"), "value": cty.StringVal("a"), })), After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("a"), "value": cty.StringVal("a"), })), }, ProviderAddr: mustDefaultRootProvider("testing"), }, PriorStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "a", "value": "a", }), Dependencies: make([]addrs.ConfigResource, 0), Status: states.ObjectReady, }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "input"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.SetVal([]cty.Value{ cty.StringVal("a"), }), }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "removed"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.SetVal([]cty.Value{ cty.StringVal("a"), }), }, }, wantPlanDiags: []expectedDiagnostic{ { severity: tfdiags.Error, summary: "Cannot remove component instance", detail: "The component instance component.self[\"a\"] is targeted by a component block and cannot be removed. The relevant component is defined at git::https://example.com/test.git//with-single-input/removed-component-instance/removed-component-instance.tfcomponent.hcl:18,1-17.", }, }, }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, tc.source) inputs := make(map[stackaddrs.InputVariable]ExternalInputValue, len(tc.inputs)) for name, input := range tc.inputs { inputs[stackaddrs.InputVariable{Name: name}] = ExternalInputValue{ Value: input, } } providers := map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProviderWithData(t, tc.store), nil }, } planChangesCh := make(chan stackplan.PlannedChange) planDiagsCh := make(chan tfdiags.Diagnostic) planReq := PlanRequest{ Config: cfg, ProviderFactories: providers, InputValues: inputs, ForcePlanTimestamp: &fakePlanTimestamp, PrevState: tc.initialState, DependencyLocks: *lock, } planResp := PlanResponse{ PlannedChanges: planChangesCh, Diagnostics: planDiagsCh, } go Plan(ctx, &planReq, &planResp) gotPlanChanges, gotPlanDiags := collectPlanOutput(planChangesCh, planDiagsCh) sort.SliceStable(gotPlanChanges, func(i, j int) bool { return plannedChangeSortKey(gotPlanChanges[i]) < plannedChangeSortKey(gotPlanChanges[j]) }) sort.SliceStable(gotPlanDiags, diagnosticSortFunc(gotPlanDiags)) expectDiagnosticsForTest(t, gotPlanDiags, tc.wantPlanDiags...) if diff := cmp.Diff(tc.wantPlanChanges, gotPlanChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } }) } } func TestPlanWithResourceIdentities(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, "resource-identity") fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") if err != nil { t.Fatal(err) } lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), providerreqs.MustParseVersion("0.0.0"), providerreqs.MustParseVersionConstraints("=0.0.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) changesCh := make(chan stackplan.PlannedChange, 8) diagsCh := make(chan tfdiags.Diagnostic, 2) req := PlanRequest{ Config: cfg, ForcePlanTimestamp: &fakePlanTimestamp, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, DependencyLocks: *lock, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) gotChanges, diags := collectPlanOutput(changesCh, diagsCh) if len(diags) != 0 { t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error()) } wantChanges := []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: stackaddrs.Absolute( stackaddrs.RootStackInstance, stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "self"}, }, ), Action: plans.Create, PlanApplyable: true, PlanComplete: true, PlannedCheckResults: &states.CheckResults{}, PlannedInputValues: map[string]plans.DynamicValue{ "name": mustPlanDynamicValueDynamicType(cty.StringVal("example")), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{"name": nil}, PlannedOutputValues: map[string]cty.Value{}, PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeResourceInstancePlanned{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource_with_identity.hello"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource_with_identity.hello"), PrevRunAddr: mustAbsResourceInstance("testing_resource_with_identity.hello"), ProviderAddr: mustDefaultRootProvider("testing"), ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("example"), "value": cty.NullVal(cty.String), })), AfterIdentity: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("id:example"), })), }, }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceWithIdentitySchema, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, } sort.SliceStable(gotChanges, func(i, j int) bool { return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) }) if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func TestPlanInvalidLocalValue(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, "invalid-local") fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") if err != nil { t.Fatal(err) } lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), providerreqs.MustParseVersion("0.0.0"), providerreqs.MustParseVersionConstraints("=0.0.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) changesCh := make(chan stackplan.PlannedChange, 8) diagsCh := make(chan tfdiags.Diagnostic, 2) req := PlanRequest{ Config: cfg, ForcePlanTimestamp: &fakePlanTimestamp, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, DependencyLocks: *lock, InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ {Name: "in"}: { Value: cty.ObjectVal(map[string]cty.Value{"name": cty.StringVal("foo")}), }, }, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) gotChanges, diags := collectPlanOutput(changesCh, diagsCh) tfdiags.AssertDiagnosticsMatch(t, diags, tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid operand", Detail: "Unsuitable value for left operand: a number is required.", Subject: &hcl.Range{ Filename: "git::https://example.com/test.git//invalid-local/invalid-local.tfcomponent.hcl", Start: hcl.Pos{Line: 19, Column: 49, Byte: 377}, End: hcl.Pos{Line: 19, Column: 50, Byte: 378}, }, Context: &hcl.Range{ Filename: "git::https://example.com/test.git//invalid-local/invalid-local.tfcomponent.hcl", Start: hcl.Pos{Line: 19, Column: 49, Byte: 377}, End: hcl.Pos{Line: 19, Column: 54, Byte: 382}, }, })) // We don't really care about the precise content of the plan changes here, // we just want to ensure that the produced plan is not applyable sort.SliceStable(gotChanges, func(i, j int) bool { return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) }) pca, ok := gotChanges[0].(*stackplan.PlannedChangeApplyable) if !ok { t.Fatalf("expected first change to be PlannedChangeApplyable, got %T", gotChanges[0]) } if pca.Applyable { t.Fatalf("expected plan to be not applyable due to invalid local value, but it is applyable") } } // 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 expectOutput(t *testing.T, name string, changes []stackplan.PlannedChange) *stackplan.PlannedChangeOutputValue { t.Helper() for _, change := range changes { if v, ok := change.(*stackplan.PlannedChangeOutputValue); ok && v.Addr.Name == name { return v } } t.Fatalf("expected output value %q", name) return nil } func TestPlanWithActionInvocationHooks(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, "planning-action-lifecycle") fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") if err != nil { t.Fatal(err) } testCtx := TestContext{ config: cfg, providers: map[addrs.Provider]providers.Factory{ addrs.NewBuiltInProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, timestamp: &fakePlanTimestamp, } // Create dynamic values for resource change resourceBeforeVal := cty.NullVal(cty.Object(map[string]cty.Type{ "id": cty.String, "value": cty.String, })) resourceAfterVal := cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.StringVal("example"), }) resourceBeforeDynVal, err := plans.NewDynamicValue(resourceBeforeVal, resourceBeforeVal.Type()) if err != nil { t.Fatal(err) } resourceAfterDynVal, err := plans.NewDynamicValue(resourceAfterVal, resourceAfterVal.Type()) if err != nil { t.Fatal(err) } // Common addresses used throughout the test webComponentInstance := stackaddrs.AbsComponentInstance{ Stack: stackaddrs.RootStackInstance, Item: stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "web"}, }, } webComponent := stackaddrs.AbsComponent{ Stack: stackaddrs.RootStackInstance, Item: stackaddrs.Component{Name: "web"}, } testResourceInstance := addrs.RootModuleInstance.ResourceInstance(addrs.ManagedResourceMode, "testing_resource", "main", addrs.NoKey) testResourceObject := stackaddrs.AbsResourceInstanceObject{ Component: webComponentInstance, Item: addrs.AbsResourceInstanceObject{ ResourceInstance: testResourceInstance, }, } testActionInstance := addrs.RootModuleInstance.ActionInstance("testing_action", "notify", addrs.NoKey) testActionInvocationAddr := stackaddrs.AbsActionInvocationInstance{ Component: webComponentInstance, Item: testActionInstance, } testProviderConfig := addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewBuiltInProvider("testing"), } expectedHooks := ExpectedHooks{ ReportActionInvocationPlanned: []*hooks.ActionInvocation{ { Addr: testActionInvocationAddr, ProviderAddr: addrs.NewBuiltInProvider("testing"), Trigger: &plans.ResourceActionTrigger{ TriggeringResourceAddr: testResourceInstance, ActionTriggerEvent: configs.AfterCreate, ActionTriggerBlockIndex: 0, ActionsListIndex: 0, }, }, }, ComponentExpanded: []*hooks.ComponentInstances{ { ComponentAddr: webComponent, InstanceAddrs: []stackaddrs.AbsComponentInstance{webComponentInstance}, }, }, PendingComponentInstancePlan: collections.NewSet(webComponentInstance), BeginComponentInstancePlan: collections.NewSet(webComponentInstance), EndComponentInstancePlan: collections.NewSet(webComponentInstance), ReportResourceInstanceStatus: []*hooks.ResourceInstanceStatusHookData{ { Addr: testResourceObject, ProviderAddr: addrs.NewBuiltInProvider("testing"), Status: hooks.ResourceInstancePlanning, }, { Addr: testResourceObject, ProviderAddr: addrs.NewBuiltInProvider("testing"), Status: hooks.ResourceInstancePlanned, }, }, ReportResourceInstancePlanned: []*hooks.ResourceInstanceChange{ { Addr: testResourceObject, Change: &plans.ResourceInstanceChangeSrc{ Addr: testResourceInstance, PrevRunAddr: testResourceInstance, ProviderAddr: testProviderConfig, ChangeSrc: plans.ChangeSrc{ Action: plans.Create, Before: resourceBeforeDynVal, After: resourceAfterDynVal, }, }, }, }, ReportComponentInstancePlanned: []*hooks.ComponentInstanceChange{ { Addr: webComponentInstance, Add: 1, ActionInvocation: 1, }, }, } cycle := TestCycle{ planMode: plans.NormalMode, wantPlannedHooks: &expectedHooks, } testCtx.Plan(t, ctx, stackstate.NewState(), cycle) } func TestPlanWithDeferredActionInvocation(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, "deferred-action") fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z") if err != nil { t.Fatal(err) } changesCh := make(chan stackplan.PlannedChange) diagsCh := make(chan tfdiags.Diagnostic) lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), providerreqs.MustParseVersion("0.0.0"), providerreqs.MustParseVersionConstraints("=0.0.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) req := PlanRequest{ Config: cfg, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, DependencyLocks: *lock, ForcePlanTimestamp: &fakePlanTimestamp, InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ {Name: "id"}: { Value: cty.StringVal("test-id-123"), }, {Name: "defer"}: { Value: cty.BoolVal(true), }, }, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) gotChanges, diags := collectPlanOutput(changesCh, diagsCh) reportDiagnosticsForTest(t, diags) if len(diags) != 0 { t.FailNow() // We reported the diags above } sort.SliceStable(gotChanges, func(i, j int) bool { return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) }) // Find the deferred action invocation in the changes var foundDeferredAction bool for _, change := range gotChanges { if _, ok := change.(*stackplan.PlannedChangeDeferredActionInvocation); ok { foundDeferredAction = true break } } if !foundDeferredAction { t.Error("Expected to find a deferred action invocation in the plan changes, but none was found") t.Logf("Got %d changes:", len(gotChanges)) for i, change := range gotChanges { t.Logf(" [%d] %T", i, change) } } }