// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package stackruntime import ( "context" "fmt" "path" "path/filepath" "sort" "strconv" "strings" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty-debug/ctydebug" "github.com/zclconf/go-cty/cty" "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/depsfile" "github.com/hashicorp/terraform/internal/getproviders/providerreqs" "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" "github.com/hashicorp/terraform/internal/stacks/stackplan" "github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks" "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" ) var changesCmpOpts = cmp.Options{ ctydebug.CmpOptions, collections.CmpOptions, cmpopts.IgnoreUnexported(addrs.InputVariable{}), cmpopts.IgnoreUnexported(states.ResourceInstanceObjectSrc{}), } // TestApply uses a generic framework for running apply 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 TestApply(t *testing.T) { fakePlanTimestamp, err := time.Parse(time.RFC3339, "2021-01-01T00:00:00Z") if err != nil { t.Fatal(err) } tcs := map[string]struct { path string skip bool state *stackstate.State store *stacks_testing_provider.ResourceStore cycles []TestCycle }{ "built-in provider used not present in required": { path: "with-built-in-provider", cycles: []TestCycle{ {}, // plan, apply -> no diags }, }, "built-in provider used and explicitly defined in required providers": { path: "with-built-in-provider-explicitly-defined", cycles: []TestCycle{ {}, // plan, apply -> no diags }, }, "creating inputs and outputs": { path: "component-input-output", cycles: []TestCycle{ { planInputs: map[string]cty.Value{ "value": cty.StringVal("foo"), }, wantPlannedChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangeOutputValue{ Addr: mustStackOutputValue("value"), Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.StringVal("foo"), }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: mustStackInputVariable("value"), Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.StringVal("foo"), }, }, wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeOutputValue{ Addr: mustStackOutputValue("value"), Value: cty.StringVal("foo"), }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("value"), Value: cty.StringVal("foo"), }, }, }, }, }, "updating inputs and outputs": { path: "component-input-output", cycles: []TestCycle{ { planInputs: map[string]cty.Value{ "value": cty.StringVal("foo"), }, }, { planInputs: map[string]cty.Value{ "value": cty.StringVal("bar"), }, wantPlannedChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangeOutputValue{ Addr: mustStackOutputValue("value"), Action: plans.Update, Before: cty.StringVal("foo"), After: cty.StringVal("bar"), }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: mustStackInputVariable("value"), Action: plans.Update, Before: cty.StringVal("foo"), After: cty.StringVal("bar"), }, }, wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeOutputValue{ Addr: mustStackOutputValue("value"), Value: cty.StringVal("bar"), }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("value"), Value: cty.StringVal("bar"), }, }, }, }, }, "updating inputs and outputs (noop)": { path: "component-input-output", cycles: []TestCycle{ { planInputs: map[string]cty.Value{ "value": cty.StringVal("foo"), }, }, { planInputs: map[string]cty.Value{ "value": cty.StringVal("foo"), }, wantPlannedChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangeOutputValue{ Addr: mustStackOutputValue("value"), Action: plans.NoOp, Before: cty.StringVal("foo"), After: cty.StringVal("foo"), }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: mustStackInputVariable("value"), Action: plans.NoOp, Before: cty.StringVal("foo"), After: cty.StringVal("foo"), }, }, wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeOutputValue{ Addr: mustStackOutputValue("value"), Value: cty.StringVal("foo"), }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("value"), Value: cty.StringVal("foo"), }, }, }, }, }, "deleting inputs and outputs": { path: "component-input-output", state: stackstate.NewStateBuilder(). AddInput("removed", cty.StringVal("bar")). AddOutput("removed", cty.StringVal("bar")). Build(), cycles: []TestCycle{ { planInputs: map[string]cty.Value{ "value": cty.StringVal("foo"), }, wantPlannedChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangeOutputValue{ Addr: mustStackOutputValue("removed"), Action: plans.Delete, Before: cty.StringVal("bar"), After: cty.NullVal(cty.DynamicPseudoType), }, &stackplan.PlannedChangeOutputValue{ Addr: mustStackOutputValue("value"), Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.StringVal("foo"), }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: mustStackInputVariable("removed"), Action: plans.Delete, Before: cty.StringVal("bar"), After: cty.NullVal(cty.DynamicPseudoType), }, &stackplan.PlannedChangeRootInputValue{ Addr: mustStackInputVariable("value"), Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.StringVal("foo"), }, }, wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeOutputValue{ Addr: mustStackOutputValue("removed"), }, &stackstate.AppliedChangeOutputValue{ Addr: mustStackOutputValue("value"), Value: cty.StringVal("foo"), }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("removed"), Value: cty.NilVal, // destroyed }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("value"), Value: cty.StringVal("foo"), }, }, }, }, }, "checkable objects": { path: "checkable-objects", cycles: []TestCycle{ { planInputs: map[string]cty.Value{ "foo": cty.StringVal("bar"), }, wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { return diags.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}, }, }) }), wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.single"), ComponentInstanceAddr: mustAbsComponentInstance("component.single"), OutputValues: map[addrs.OutputValue]cty.Value{ addrs.OutputValue{Name: "foo"}: cty.StringVal("bar"), }, InputVariables: map[addrs.InputVariable]cty.Value{ mustInputVariable("foo"): cty.StringVal("bar"), }, }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.single.testing_resource.main"), NewStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "test", "value": "bar", }), Status: states.ObjectReady, Dependencies: make([]addrs.ConfigResource, 0), }, ProviderConfigAddr: addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("testing"), }, Schema: stacks_testing_provider.TestingResourceSchema, }, }, wantAppliedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { return diags.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}, }, }) }), }, { planMode: plans.DestroyMode, planInputs: map[string]cty.Value{ "foo": cty.StringVal("bar"), }, wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstanceRemoved{ ComponentAddr: mustAbsComponent("component.single"), ComponentInstanceAddr: mustAbsComponentInstance("component.single"), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.single.testing_resource.main"), ProviderConfigAddr: mustDefaultRootProvider("testing"), }, }, }, }, }, "removed component": { path: filepath.Join("with-single-input", "removed-component"), state: stackstate.NewStateBuilder(). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self")). AddInputVariable("id", cty.StringVal("removed")). AddInputVariable("input", cty.StringVal("removed"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.data")). 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(), cycles: []TestCycle{ { planInputs: map[string]cty.Value{}, wantPlannedChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.self"), 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.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, }, }, wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstanceRemoved{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self"), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), ProviderConfigAddr: mustDefaultRootProvider("testing"), NewStateSrc: nil, Schema: providers.Schema{}, }, }, }, }, }, "removed component instance": { path: filepath.Join("with-single-input", "removed-component-instance"), state: 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", }), })). Build(), store: stacks_testing_provider.NewResourceStoreBuilder(). AddResource("removed", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("removed"), "value": cty.StringVal("removed"), })). Build(), cycles: []TestCycle{ { planInputs: map[string]cty.Value{ "input": cty.SetVal([]cty.Value{ cty.StringVal("added"), }), "removed": cty.SetVal([]cty.Value{ cty.StringVal("removed"), }), }, wantPlannedChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, // 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"), }), }, }, wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"added\"]"), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: map[addrs.InputVariable]cty.Value{ mustInputVariable("id"): cty.StringVal("added"), mustInputVariable("input"): cty.StringVal("added"), }, }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"added\"].testing_resource.data"), NewStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "added", "value": "added", }), Status: states.ObjectReady, Dependencies: make([]addrs.ConfigResource, 0), }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackstate.AppliedChangeComponentInstanceRemoved{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"removed\"]"), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"removed\"].testing_resource.data"), ProviderConfigAddr: mustDefaultRootProvider("testing"), NewStateSrc: nil, Schema: providers.Schema{}, }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("input"), Value: cty.SetVal([]cty.Value{ cty.StringVal("added"), }), }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("removed"), Value: cty.SetVal([]cty.Value{ cty.StringVal("removed"), }), }, }, }, }, }, "duplicate removed blocks": { path: path.Join("with-single-input", "removed-component-duplicate"), state: stackstate.NewStateBuilder(). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"one\"]")). AddInputVariable("id", cty.StringVal("one")). AddInputVariable("input", cty.StringVal("one"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("component.self[\"one\"].testing_resource.data")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "one", "value": "one", }), })). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"two\"]")). AddInputVariable("id", cty.StringVal("two")). AddInputVariable("input", cty.StringVal("two"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("component.self[\"two\"].testing_resource.data")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "two", "value": "two", }), })). Build(), store: stacks_testing_provider.NewResourceStoreBuilder(). AddResource("one", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("one"), "value": cty.StringVal("one"), })). AddResource("two", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("two"), "value": cty.StringVal("two"), })). Build(), cycles: []TestCycle{ { planMode: plans.NormalMode, planInputs: map[string]cty.Value{ "input": cty.SetValEmpty(cty.String), "removed_one": cty.SetVal([]cty.Value{ cty.StringVal("one"), }), "removed_two": cty.SetVal([]cty.Value{ cty.StringVal("two"), }), }, wantPlannedChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.self[\"one\"]"), PlanComplete: true, PlanApplyable: true, Mode: plans.DestroyMode, Action: plans.Delete, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.StringVal("one")), "input": mustPlanDynamicValueDynamicType(cty.StringVal("one")), }, 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[\"one\"].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("one"), "value": cty.StringVal("one"), })), 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": "one", "value": "one", }), Dependencies: make([]addrs.ConfigResource, 0), Status: states.ObjectReady, }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.self[\"two\"]"), PlanComplete: true, PlanApplyable: true, Mode: plans.DestroyMode, Action: plans.Delete, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.StringVal("two")), "input": mustPlanDynamicValueDynamicType(cty.StringVal("two")), }, 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[\"two\"].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("two"), "value": cty.StringVal("two"), })), 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": "two", "value": "two", }), 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.SetValEmpty(cty.String), }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "removed_one"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.SetVal([]cty.Value{ cty.StringVal("one"), }), }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "removed_two"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.SetVal([]cty.Value{ cty.StringVal("two"), }), }, }, wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstanceRemoved{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"one\"]"), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"one\"].testing_resource.data"), ProviderConfigAddr: mustDefaultRootProvider("testing"), NewStateSrc: nil, Schema: providers.Schema{}, }, &stackstate.AppliedChangeComponentInstanceRemoved{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"two\"]"), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"two\"].testing_resource.data"), ProviderConfigAddr: mustDefaultRootProvider("testing"), NewStateSrc: nil, Schema: providers.Schema{}, }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("input"), Value: cty.SetValEmpty(cty.String), }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("removed_one"), Value: cty.SetVal([]cty.Value{ cty.StringVal("one"), }), }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("removed_two"), Value: cty.SetVal([]cty.Value{ cty.StringVal("two"), }), }, }, }, }, }, "removed component instance direct": { path: filepath.Join("with-single-input", "removed-component-instance-direct"), state: 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", }), })). Build(), store: stacks_testing_provider.NewResourceStoreBuilder(). AddResource("removed", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("removed"), "value": cty.StringVal("removed"), })). Build(), cycles: []TestCycle{ { planInputs: map[string]cty.Value{ "input": cty.SetVal([]cty.Value{ cty.StringVal("added"), }), }, wantPlannedChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, // 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"), }), }, }, wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"added\"]"), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: map[addrs.InputVariable]cty.Value{ mustInputVariable("id"): cty.StringVal("added"), mustInputVariable("input"): cty.StringVal("added"), }, }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"added\"].testing_resource.data"), NewStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "added", "value": "added", }), Status: states.ObjectReady, Dependencies: make([]addrs.ConfigResource, 0), }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackstate.AppliedChangeComponentInstanceRemoved{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"removed\"]"), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"removed\"].testing_resource.data"), ProviderConfigAddr: mustDefaultRootProvider("testing"), NewStateSrc: nil, Schema: providers.Schema{}, }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("input"), Value: cty.SetVal([]cty.Value{ cty.StringVal("added"), }), }, }, }, }, }, "removed stack instance": { path: filepath.Join("with-single-input", "removed-stack-instance-dynamic"), state: stackstate.NewStateBuilder(). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.simple[\"removed\"].component.self")). AddInputVariable("id", cty.StringVal("removed")). AddInputVariable("input", cty.StringVal("removed"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("stack.simple[\"removed\"].component.self.testing_resource.data")). 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(), cycles: []TestCycle{ { planInputs: map[string]cty.Value{ "input": cty.MapVal(map[string]cty.Value{ "added": cty.StringVal("added"), }), "removed": cty.MapVal(map[string]cty.Value{ "removed": cty.StringVal("removed"), }), }, wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("stack.simple[\"added\"].component.self"), ComponentInstanceAddr: mustAbsComponentInstance("stack.simple[\"added\"].component.self"), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: map[addrs.InputVariable]cty.Value{ mustInputVariable("id"): cty.StringVal("added"), mustInputVariable("input"): cty.StringVal("added"), }, }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.simple[\"added\"].component.self.testing_resource.data"), NewStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "added", "value": "added", }), Status: states.ObjectReady, Dependencies: make([]addrs.ConfigResource, 0), }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackstate.AppliedChangeComponentInstanceRemoved{ ComponentAddr: mustAbsComponent("stack.simple[\"removed\"].component.self"), ComponentInstanceAddr: mustAbsComponentInstance("stack.simple[\"removed\"].component.self"), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.simple[\"removed\"].component.self.testing_resource.data"), ProviderConfigAddr: mustDefaultRootProvider("testing"), NewStateSrc: nil, Schema: providers.Schema{}, }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("input"), Value: cty.MapVal(map[string]cty.Value{ "added": cty.StringVal("added"), }), }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("removed"), Value: cty.MapVal(map[string]cty.Value{ "removed": cty.StringVal("removed"), }), }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("removed-direct"), Value: cty.SetValEmpty(cty.String), }, }, }, }, }, "removed embedded dynamic component from stack": { path: filepath.Join("with-single-input", "removed-component-from-stack-dynamic"), state: stackstate.NewStateBuilder(). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.for_each.component.self[\"removed\"]")). AddInputVariable("id", cty.StringVal("removed")). AddInputVariable("input", cty.StringVal("removed"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("stack.for_each.component.self[\"removed\"].testing_resource.data")). 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(), cycles: []TestCycle{ { planInputs: map[string]cty.Value{ "for_each_input": cty.MapVal(map[string]cty.Value{ "added": cty.StringVal("added"), }), "for_each_removed": cty.SetVal([]cty.Value{ cty.StringVal("removed"), }), }, wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("stack.for_each.component.self"), ComponentInstanceAddr: mustAbsComponentInstance("stack.for_each.component.self[\"added\"]"), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: map[addrs.InputVariable]cty.Value{ mustInputVariable("id"): cty.StringVal("added"), mustInputVariable("input"): cty.StringVal("added"), }, }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.for_each.component.self[\"added\"].testing_resource.data"), NewStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "added", "value": "added", }), Status: states.ObjectReady, Dependencies: make([]addrs.ConfigResource, 0), }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackstate.AppliedChangeComponentInstanceRemoved{ ComponentAddr: mustAbsComponent("stack.for_each.component.self[\"removed\"]"), ComponentInstanceAddr: mustAbsComponentInstance("stack.for_each.component.self[\"removed\"]"), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.for_each.component.self[\"removed\"].testing_resource.data"), ProviderConfigAddr: mustDefaultRootProvider("testing"), NewStateSrc: nil, Schema: providers.Schema{}, }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("for_each_input"), Value: cty.MapVal(map[string]cty.Value{ "added": cty.StringVal("added"), }), }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("for_each_removed"), Value: cty.SetVal([]cty.Value{ cty.StringVal("removed"), }), }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("simple_input"), Value: cty.MapValEmpty(cty.String), }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("simple_removed"), Value: cty.SetValEmpty(cty.String), }, }, }, }, }, "removed embedded component relative": { path: filepath.Join("with-single-input", "removed-component-from-stack"), state: stackstate.NewStateBuilder(). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.nested.component.self[\"foo\"]")). AddInputVariable("id", cty.StringVal("removed")). AddInputVariable("input", cty.StringVal("removed"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("stack.nested.component.self[\"foo\"].testing_resource.data")). 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(), cycles: []TestCycle{ { wantPlannedChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("stack.nested.component.self[\"foo\"]"), 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("stack.nested.component.self[\"foo\"].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, }, }, wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstanceRemoved{ ComponentAddr: mustAbsComponent("stack.nested.component.self[\"foo\"]"), ComponentInstanceAddr: mustAbsComponentInstance("stack.nested.component.self[\"foo\"]"), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.nested.component.self[\"foo\"].testing_resource.data"), ProviderConfigAddr: mustDefaultRootProvider("testing"), NewStateSrc: nil, Schema: providers.Schema{}, }, }, }, }, }, "removed embedded component local": { path: filepath.Join("with-single-input", "removed-embedded-component"), state: stackstate.NewStateBuilder(). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.a.component.self")). AddInputVariable("id", cty.StringVal("removed")). AddInputVariable("input", cty.StringVal("removed"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("stack.a.component.self.testing_resource.data")). 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(), cycles: []TestCycle{ { wantPlannedChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("stack.a.component.self"), 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("stack.a.component.self.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, }, }, wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstanceRemoved{ ComponentAddr: mustAbsComponent("stack.a.component.self"), ComponentInstanceAddr: mustAbsComponentInstance("stack.a.component.self"), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.a.component.self.testing_resource.data"), ProviderConfigAddr: mustDefaultRootProvider("testing"), NewStateSrc: nil, Schema: providers.Schema{}, }, }, }, }, }, "forgotten component": { path: filepath.Join("with-single-input", "forgotten-component"), state: stackstate.NewStateBuilder(). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self")). AddInputVariable("id", cty.StringVal("removed")). AddInputVariable("input", cty.StringVal("removed"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.data")). 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(), cycles: []TestCycle{ { planInputs: map[string]cty.Value{ "destroy": cty.BoolVal(false), }, wantPlannedChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.self"), PlanComplete: true, PlanApplyable: true, Mode: plans.DestroyMode, Action: plans.Forget, 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.testing_resource.data"), ChangeSrc: &plans.ResourceInstanceChangeSrc{ Addr: mustAbsResourceInstance("testing_resource.data"), PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), 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, }))), }, 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, }, }, wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { return diags.Append(tfdiags.Sourceless( tfdiags.Warning, "Some objects will no longer be managed by Terraform", `If you apply this plan, Terraform will discard its tracking information for the following objects, but it will not delete them: - testing_resource.data After applying this plan, Terraform will no longer manage these objects. You will need to import them into Terraform to manage them again.`, )) }), wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstanceRemoved{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self"), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), ProviderConfigAddr: mustDefaultRootProvider("testing"), NewStateSrc: nil, Schema: providers.Schema{}, }, }, }, }, }, "orphaned component": { path: filepath.Join("with-single-input", "valid"), state: stackstate.NewStateBuilder(). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.orphan"))). Build(), cycles: []TestCycle{ { planInputs: map[string]cty.Value{ "id": cty.StringVal("foo"), "input": cty.StringVal("bar"), }, wantPlannedChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstanceRemoved{ // The orphaned component is just silently being removed. Addr: mustAbsComponentInstance("component.orphan"), }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.self"), PlanApplyable: true, PlanComplete: true, Action: plans.Create, PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.StringVal("foo")), "input": mustPlanDynamicValueDynamicType(cty.StringVal("bar")), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "id": nil, "input": nil, }, PlannedOutputValues: make(map[string]cty.Value), PlannedCheckResults: &states.CheckResults{}, 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.Create, Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("foo"), "value": cty.StringVal("bar"), })), }, }, 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("foo"), }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{ Name: "input", }, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.StringVal("bar"), }, }, wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstanceRemoved{ // The orphaned component is just silently being removed. ComponentAddr: mustAbsComponent("component.orphan"), ComponentInstanceAddr: mustAbsComponentInstance("component.orphan"), }, &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self"), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: map[addrs.InputVariable]cty.Value{ mustInputVariable("id"): cty.StringVal("foo"), mustInputVariable("input"): cty.StringVal("bar"), }, }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), NewStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "foo", "value": "bar", }), Dependencies: make([]addrs.ConfigResource, 0), Status: states.ObjectReady, }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("id"), Value: cty.StringVal("foo"), }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("input"), Value: cty.StringVal("bar"), }, }, }, }, }, "forget with dependency": { path: "forget_with_dependency", state: stackstate.NewStateBuilder(). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.one")). AddDependent(mustAbsComponent("component.two")). AddInputVariable("value", cty.StringVal("bar")). AddOutputValue("id", cty.StringVal("foo"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("component.one.testing_resource.resource")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "foo", "value": "bar", }), Status: states.ObjectReady, })). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.two")). AddDependency(mustAbsComponent("component.one")). AddInputVariable("value", cty.StringVal("foo")). AddOutputValue("id", cty.StringVal("baz"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("component.two.testing_resource.resource")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "baz", "value": "foo", }), Status: states.ObjectReady, })). Build(), store: stacks_testing_provider.NewResourceStoreBuilder(). AddResource("foo", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("foo"), "value": cty.StringVal("bar"), })). AddResource("baz", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("baz"), "value": cty.StringVal("foo"), })). Build(), cycles: []TestCycle{ { planMode: plans.NormalMode, wantPlannedDiags: tfdiags.Diagnostics{ tfdiags.Sourceless(tfdiags.Warning, "Some objects will no longer be managed by Terraform", `If you apply this plan, Terraform will discard its tracking information for the following objects, but it will not delete them: - testing_resource.resource After applying this plan, Terraform will no longer manage these objects. You will need to import them into Terraform to manage them again.`), }, wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.one"), ComponentInstanceAddr: mustAbsComponentInstance("component.one"), OutputValues: map[addrs.OutputValue]cty.Value{ addrs.OutputValue{Name: "id"}: cty.StringVal("foo"), }, InputVariables: map[addrs.InputVariable]cty.Value{ addrs.InputVariable{Name: "value"}: cty.StringVal("bar"), }, Dependents: collections.NewSet(mustAbsComponent("component.two")), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.one.testing_resource.resource"), NewStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "foo", "value": "bar", }), Status: states.ObjectReady, AttrSensitivePaths: make([]cty.Path, 0), }, ProviderConfigAddr: addrs.AbsProviderConfig{ Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, Schema: stacks_testing_provider.TestingResourceSchema, }, &stackstate.AppliedChangeComponentInstanceRemoved{ ComponentAddr: mustAbsComponent("component.two"), ComponentInstanceAddr: mustAbsComponentInstance("component.two"), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.two.testing_resource.resource"), NewStateSrc: nil, // Resource is forgotten ProviderConfigAddr: addrs.AbsProviderConfig{ Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, }, }, }, }, }, "forget with dependency on component to forget": { path: "forget_with_dependency_to_forget", state: stackstate.NewStateBuilder(). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.one")). AddDependent(mustAbsComponent("component.two")). AddInputVariable("value", cty.StringVal("bar")). AddOutputValue("id", cty.StringVal("foo"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("component.one.testing_resource.resource")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "foo", "value": "bar", }), Status: states.ObjectReady, })). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.two")). AddDependency(mustAbsComponent("component.one")). AddInputVariable("value", cty.StringVal("foo")). AddOutputValue("id", cty.StringVal("baz"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("component.two.testing_resource.resource")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "baz", "value": "foo", }), Status: states.ObjectReady, })). Build(), store: stacks_testing_provider.NewResourceStoreBuilder(). AddResource("foo", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("foo"), "value": cty.StringVal("bar"), })). AddResource("baz", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("baz"), "value": cty.StringVal("foo"), })). Build(), cycles: []TestCycle{ { planMode: plans.NormalMode, wantPlannedDiags: tfdiags.Diagnostics{ tfdiags.Sourceless(tfdiags.Warning, "Some objects will no longer be managed by Terraform", `If you apply this plan, Terraform will discard its tracking information for the following objects, but it will not delete them: - testing_resource.resource After applying this plan, Terraform will no longer manage these objects. You will need to import them into Terraform to manage them again.`), tfdiags.Sourceless(tfdiags.Warning, "Some objects will no longer be managed by Terraform", `If you apply this plan, Terraform will discard its tracking information for the following objects, but it will not delete them: - testing_resource.resource After applying this plan, Terraform will no longer manage these objects. You will need to import them into Terraform to manage them again.`), }, wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstanceRemoved{ ComponentAddr: mustAbsComponent("component.one"), ComponentInstanceAddr: mustAbsComponentInstance("component.one"), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.one.testing_resource.resource"), NewStateSrc: nil, // Resource is forgotten ProviderConfigAddr: addrs.AbsProviderConfig{ Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, }, &stackstate.AppliedChangeComponentInstanceRemoved{ ComponentAddr: mustAbsComponent("component.two"), ComponentInstanceAddr: mustAbsComponentInstance("component.two"), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.two.testing_resource.resource"), NewStateSrc: nil, // Resource is forgotten ProviderConfigAddr: addrs.AbsProviderConfig{ Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, }, }, }, }, }, "removed block with provider-to-component dep": { path: path.Join("auth-provider-w-data", "removed"), state: stackstate.NewStateBuilder(). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.load")). AddDependent(mustAbsComponent("component.create")). AddOutputValue("credentials", cty.StringVal("wrong"))). // must reload the credentials AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.create")). AddDependency(mustAbsComponent("component.load"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("component.create.testing_resource.resource")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "resource", "value": nil, }), Status: states.ObjectReady, }). SetProviderAddr(mustDefaultRootProvider("testing"))). Build(), store: stacks_testing_provider.NewResourceStoreBuilder().AddResource("credentials", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("credentials"), // we have the wrong value in state, so this correct value must // be loaded for this test to work. "value": cty.StringVal("authn"), })).Build(), cycles: []TestCycle{ { planMode: plans.NormalMode, wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstanceRemoved{ ComponentAddr: mustAbsComponent("component.create"), ComponentInstanceAddr: mustAbsComponentInstance("component.create"), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.create.testing_resource.resource"), ProviderConfigAddr: mustDefaultRootProvider("testing"), NewStateSrc: nil, // deleted }, &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.load"), ComponentInstanceAddr: mustAbsComponentInstance("component.load"), OutputValues: map[addrs.OutputValue]cty.Value{ addrs.OutputValue{Name: "credentials"}: cty.StringVal("authn").Mark(marks.Sensitive), }, InputVariables: make(map[addrs.InputVariable]cty.Value), Dependents: collections.NewSet(mustAbsComponent("component.create")), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.load.data.testing_data_source.credentials"), NewStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "credentials", "value": "authn", }), AttrSensitivePaths: make([]cty.Path, 0), Status: states.ObjectReady, }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingDataSourceSchema, }, }, }, }, }, "ephemeral": { path: path.Join("with-single-input", "ephemeral"), cycles: []TestCycle{ { planMode: plans.NormalMode, planInputs: map[string]cty.Value{ "input": cty.StringVal("hello"), "ephemeral": cty.StringVal("planning"), }, applyInputs: map[string]cty.Value{ "ephemeral": cty.StringVal("applying"), }, wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self"), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: map[addrs.InputVariable]cty.Value{ mustInputVariable("id"): cty.StringVal("2f9f3b84"), mustInputVariable("input"): cty.StringVal("hello"), }, }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), NewStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "2f9f3b84", "value": "hello", }), Status: states.ObjectReady, Dependencies: make([]addrs.ConfigResource, 0), }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("ephemeral"), Value: cty.NullVal(cty.String), // ephemeral }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("input"), Value: cty.StringVal("hello"), }, }, }, }, }, "missing-ephemeral": { path: path.Join("with-single-input", "ephemeral"), cycles: []TestCycle{ { planMode: plans.NormalMode, planInputs: map[string]cty.Value{ "input": cty.StringVal("hello"), "ephemeral": cty.StringVal("planning"), }, applyInputs: make(map[string]cty.Value), // deliberately omitting ephemeral wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self"), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: map[addrs.InputVariable]cty.Value{ mustInputVariable("id"): cty.StringVal("2f9f3b84"), mustInputVariable("input"): cty.StringVal("hello"), }, }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), NewStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "2f9f3b84", "value": "hello", }), Status: states.ObjectReady, Dependencies: make([]addrs.ConfigResource, 0), }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("input"), Value: cty.StringVal("hello"), }, }, wantAppliedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { return diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "No value for required variable", Detail: "The root input variable \"var.ephemeral\" is not set, and has no default value.", Subject: &hcl.Range{ Filename: "git::https://example.com/test.git//with-single-input/ephemeral/ephemeral.tfcomponent.hcl", Start: hcl.Pos{ Line: 14, Column: 1, Byte: 175, }, End: hcl.Pos{ Line: 14, Column: 21, Byte: 195, }, }, }) }), }, }, }, "ephemeral-default": { path: path.Join("with-single-input", "ephemeral-default"), cycles: []TestCycle{ { planMode: plans.NormalMode, planInputs: map[string]cty.Value{ "input": cty.StringVal("hello"), // deliberately omitting ephemeral }, applyInputs: make(map[string]cty.Value), // deliberately omitting ephemeral wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self"), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: map[addrs.InputVariable]cty.Value{ mustInputVariable("id"): cty.StringVal("2f9f3b84"), mustInputVariable("input"): cty.StringVal("hello"), }, }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), NewStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "2f9f3b84", "value": "hello", }), Status: states.ObjectReady, Dependencies: make([]addrs.ConfigResource, 0), }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("ephemeral"), Value: cty.NullVal(cty.String), // ephemeral }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("input"), Value: cty.StringVal("hello"), }, }, }, }, }, "deferred-components": { 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(), cycles: []TestCycle{ { planMode: plans.NormalMode, planInputs: map[string]cty.Value{ "providers": cty.UnknownVal(cty.Set(cty.String)), }, wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.const"), ComponentInstanceAddr: mustAbsComponentInstance("component.const"), Dependencies: collections.NewSet[stackaddrs.AbsComponent](), Dependents: collections.NewSet[stackaddrs.AbsComponent](), InputVariables: map[addrs.InputVariable]cty.Value{ mustInputVariable("id"): cty.StringVal("data_known"), mustInputVariable("resource"): cty.StringVal("resource_known"), }, OutputValues: make(map[addrs.OutputValue]cty.Value), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.const.data.testing_data_source.data"), NewStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "data_known", "value": "known", }), AttrSensitivePaths: make([]cty.Path, 0), Status: states.ObjectReady, }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingDataSourceSchema, }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.const.testing_resource.data"), NewStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "resource_known", "value": "known", }), Dependencies: []addrs.ConfigResource{ mustAbsResourceInstance("data.testing_data_source.data").ConfigResource(), }, Status: states.ObjectReady, }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("providers"), Value: cty.UnknownVal(cty.Set(cty.String)), }, }, }, { planMode: plans.DestroyMode, planInputs: map[string]cty.Value{ "providers": cty.UnknownVal(cty.Set(cty.String)), }, wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstanceRemoved{ ComponentAddr: mustAbsComponent("component.const"), ComponentInstanceAddr: mustAbsComponentInstance("component.const"), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.const.data.testing_data_source.data"), ProviderConfigAddr: mustDefaultRootProvider("testing"), NewStateSrc: nil, }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.const.testing_resource.data"), ProviderConfigAddr: mustDefaultRootProvider("testing"), NewStateSrc: nil, }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("providers"), Value: cty.NilVal, // destroyed }, }, }, }, }, "unknown-component-input": { path: path.Join("map-object-input", "for-each-input"), cycles: []TestCycle{ { planMode: plans.NormalMode, planInputs: map[string]cty.Value{ "inputs": cty.UnknownVal(cty.Map(cty.String)), }, wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.main"), ComponentInstanceAddr: mustAbsComponentInstance("component.main"), Dependencies: collections.NewSet(mustAbsComponent("component.self")), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: map[addrs.InputVariable]cty.Value{ mustInputVariable("input"): cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{ "output": cty.String, }))), }, }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("inputs"), Value: cty.UnknownVal(cty.Map(cty.String)), }, }, }, { planMode: plans.DestroyMode, planInputs: map[string]cty.Value{ "inputs": cty.MapValEmpty(cty.String), }, wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstanceRemoved{ ComponentAddr: mustAbsComponent("component.main"), ComponentInstanceAddr: mustAbsComponentInstance("component.main"), }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("inputs"), Value: cty.NilVal, // destroyed }, }, }, }, }, "unknown-component": { path: path.Join("with-single-input", "removed-component-instance"), state: stackstate.NewStateBuilder(). AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"main\"]")). AddInputVariable("id", cty.StringVal("main")). AddInputVariable("input", cty.StringVal("main"))). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("component.self[\"main\"].testing_resource.data")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: mustMarshalJSONAttrs(map[string]any{ "id": "main", "value": "main", }), })). Build(), store: stacks_testing_provider.NewResourceStoreBuilder(). AddResource("main", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("main"), "value": cty.StringVal("main"), })). Build(), cycles: []TestCycle{ { planInputs: map[string]cty.Value{ "input": cty.UnknownVal(cty.Set(cty.String)), "removed": cty.SetValEmpty(cty.String), }, wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"main\"]"), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: map[addrs.InputVariable]cty.Value{ mustInputVariable("id"): cty.StringVal("main"), mustInputVariable("input"): cty.StringVal("main"), }, }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("input"), Value: cty.UnknownVal(cty.Set(cty.String)), }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("removed"), Value: cty.SetValEmpty(cty.String), }, }, }, }, }, "ephemeral-module-outputs": { path: "ephemeral-module-output", skip: true, // TODO(issues/37822): Enable this. cycles: []TestCycle{ { wantPlannedChanges: []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.ephemeral_in"), PlanApplyable: false, PlanComplete: true, Action: plans.Create, RequiredComponents: collections.NewSet(mustAbsComponent("component.ephemeral_out")), PlannedInputValues: make(map[string]plans.DynamicValue), PlannedOutputValues: make(map[string]cty.Value), PlannedCheckResults: new(states.CheckResults), PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.ephemeral_out"), PlanApplyable: false, PlanComplete: true, Action: plans.Create, PlannedInputValues: make(map[string]plans.DynamicValue), PlannedOutputValues: map[string]cty.Value{ "value": cty.DynamicVal, // ephemeral }, PlannedCheckResults: new(states.CheckResults), PlanTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, }, wantAppliedChanges: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.ephemeral_in"), ComponentInstanceAddr: mustAbsComponentInstance("component.ephemeral_in"), Dependencies: collections.NewSet[stackaddrs.AbsComponent]( mustAbsComponent("component.ephemeral_out"), ), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: map[addrs.InputVariable]cty.Value{ mustInputVariable("input"): cty.UnknownVal(cty.String), // ephemeral }, }, &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.ephemeral_out"), ComponentInstanceAddr: mustAbsComponentInstance("component.ephemeral_out"), Dependents: collections.NewSet[stackaddrs.AbsComponent]( mustAbsComponent("component.ephemeral_in"), ), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: make(map[addrs.InputVariable]cty.Value), }, }, }, }, }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { if tc.skip { t.Skip() } 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) { provider := stacks_testing_provider.NewProviderWithData(t, store) provider.Authentication = "authn" return provider, nil }, }, dependencyLocks: *lock, } state := tc.state for ix, cycle := range tc.cycles { t.Run(strconv.FormatInt(int64(ix), 10), func(t *testing.T) { var plan *stackplan.Plan t.Run("plan", func(t *testing.T) { plan = testContext.Plan(t, ctx, state, cycle) }) t.Run("apply", func(t *testing.T) { state = testContext.Apply(t, ctx, plan, cycle) }) }) } }) } } func TestApplyWithRemovedResource(t *testing.T) { fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z") if err != nil { t.Fatal(err) } ctx := context.Background() cfg := loadMainBundleConfigForTest(t, path.Join("empty-component", "valid-providers")) lock := depsfile.NewLocks() planReq := PlanRequest{ Config: cfg, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) { return terraformProvider.NewProvider(), nil }, }, DependencyLocks: *lock, 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: mustMarshalJSONAttrs(map[string]interface{}{ "id": "FE1D5830765C", "input": map[string]interface{}{ "value": "hello", "type": "string", }, "output": map[string]interface{}{ "value": nil, "type": "string", }, "triggers_replace": nil, }), Status: states.ObjectReady, }). SetProviderAddr(addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("terraform.io/builtin/terraform"), })). Build(), } planChangesCh := make(chan stackplan.PlannedChange) diagsCh := make(chan tfdiags.Diagnostic) planResp := PlanResponse{ PlannedChanges: planChangesCh, Diagnostics: diagsCh, } go Plan(ctx, &planReq, &planResp) planChanges, diags := collectPlanOutput(planChangesCh, diagsCh) if len(diags) > 0 { t.Fatalf("expected no diagnostics, go %s", diags.ErrWithWarnings()) } planLoader := stackplan.NewLoader() for _, change := range planChanges { proto, err := change.PlannedChangeProto() if err != nil { t.Fatal(err) } for _, rawMsg := range proto.Raw { err = planLoader.AddRaw(rawMsg) if err != nil { t.Fatal(err) } } } plan, err := planLoader.Plan() if err != nil { t.Fatal(err) } applyReq := ApplyRequest{ Config: cfg, Plan: plan, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) { return terraformProvider.NewProvider(), nil }, }, } applyChangesCh := make(chan stackstate.AppliedChange) diagsCh = make(chan tfdiags.Diagnostic) applyResp := ApplyResponse{ AppliedChanges: applyChangesCh, Diagnostics: diagsCh, } go Apply(ctx, &applyReq, &applyResp) applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh) if len(applyDiags) > 0 { t.Fatalf("expected no diagnostics, got %s", applyDiags.ErrWithWarnings()) } wantChanges := []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self"), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: make(map[addrs.InputVariable]cty.Value), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.terraform_data.main"), NewStateSrc: nil, // Deleted, so is nil. ProviderConfigAddr: addrs.AbsProviderConfig{ Provider: addrs.Provider{ Type: "terraform", Namespace: "builtin", Hostname: "terraform.io", }, }, }, } sort.SliceStable(applyChanges, func(i, j int) bool { return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j]) }) if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func TestApplyWithMovedResource(t *testing.T) { fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z") if err != nil { t.Fatal(err) } ctx := context.Background() cfg := loadMainBundleConfigForTest(t, path.Join("state-manipulation", "moved")) lock := depsfile.NewLocks() lock.SetProvider( addrs.NewDefaultProvider("testing"), providerreqs.MustParseVersion("0.0.0"), providerreqs.MustParseVersionConstraints("=0.0.0"), providerreqs.PreferredHashes([]providerreqs.Hash{}), ) planReq := PlanRequest{ Config: cfg, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProviderWithData(t, stacks_testing_provider.NewResourceStoreBuilder(). AddResource("moved", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("moved"), "value": cty.StringVal("moved"), })). Build()), nil }, }, DependencyLocks: *lock, 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: "testing_resource", Name: "before", }, Key: addrs.NoKey, }, }, DeposedKey: addrs.NotDeposed, }, }). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ SchemaVersion: 0, AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "moved", "value": "moved", }), Status: states.ObjectReady, }). SetProviderAddr(addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), })). Build(), } planChangesCh := make(chan stackplan.PlannedChange) diagsCh := make(chan tfdiags.Diagnostic) planResp := PlanResponse{ PlannedChanges: planChangesCh, Diagnostics: diagsCh, } go Plan(ctx, &planReq, &planResp) planChanges, diags := collectPlanOutput(planChangesCh, diagsCh) if len(diags) > 0 { t.Fatalf("expected no diagnostics, go %s", diags.ErrWithWarnings()) } planLoader := stackplan.NewLoader() for _, change := range planChanges { proto, err := change.PlannedChangeProto() if err != nil { t.Fatal(err) } for _, rawMsg := range proto.Raw { err = planLoader.AddRaw(rawMsg) if err != nil { t.Fatal(err) } } } plan, err := planLoader.Plan() if err != nil { t.Fatal(err) } applyReq := ApplyRequest{ Config: cfg, Plan: plan, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProviderWithData(t, stacks_testing_provider.NewResourceStoreBuilder(). AddResource("moved", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("moved"), "value": cty.StringVal("moved"), })). Build()), nil }, }, DependencyLocks: *lock, } applyChangesCh := make(chan stackstate.AppliedChange) diagsCh = make(chan tfdiags.Diagnostic) applyResp := ApplyResponse{ AppliedChanges: applyChangesCh, Diagnostics: diagsCh, } go Apply(ctx, &applyReq, &applyResp) applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh) if len(applyDiags) > 0 { t.Fatalf("expected no diagnostics, got %s", applyDiags.ErrWithWarnings()) } expectedPreviousAddr := mustAbsResourceInstanceObject("component.self.testing_resource.before") wantChanges := []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self"), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: make(map[addrs.InputVariable]cty.Value), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.after"), PreviousResourceInstanceObjectAddr: &expectedPreviousAddr, NewStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "moved", "value": "moved", }), Status: states.ObjectReady, AttrSensitivePaths: make([]cty.Path, 0), }, ProviderConfigAddr: addrs.AbsProviderConfig{ Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), }, Schema: stacks_testing_provider.TestingResourceSchema, }, } sort.SliceStable(applyChanges, func(i, j int) bool { return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j]) }) if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func TestApplyWithSensitivePropagation(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) 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{ stackaddrs.InputVariable{Name: "id"}: { Value: cty.StringVal("bb5cf32312ec"), }, }, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) planChanges, diags := collectPlanOutput(changesCh, diagsCh) if len(diags) > 0 { t.Fatalf("expected no diagnostics, got %s", diags.ErrWithWarnings()) } planLoader := stackplan.NewLoader() for _, change := range planChanges { proto, err := change.PlannedChangeProto() if err != nil { t.Fatal(err) } for _, rawMsg := range proto.Raw { err = planLoader.AddRaw(rawMsg) if err != nil { t.Fatal(err) } } } plan, err := planLoader.Plan() if err != nil { t.Fatal(err) } applyReq := ApplyRequest{ Config: cfg, Plan: plan, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, DependencyLocks: *lock, } applyChangesCh := make(chan stackstate.AppliedChange) diagsCh = make(chan tfdiags.Diagnostic) applyResp := ApplyResponse{ AppliedChanges: applyChangesCh, Diagnostics: diagsCh, } go Apply(ctx, &applyReq, &applyResp) applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh) if len(applyDiags) > 0 { t.Fatalf("expected no diagnostics, got %s", applyDiags.ErrWithWarnings()) } wantChanges := []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self"), Dependencies: collections.NewSet(mustAbsComponent("component.sensitive")), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: map[addrs.InputVariable]cty.Value{ mustInputVariable("id"): cty.StringVal("bb5cf32312ec"), mustInputVariable("input"): cty.StringVal("secret").Mark(marks.Sensitive), }, }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), NewStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "bb5cf32312ec", "value": "secret", }), AttrSensitivePaths: []cty.Path{ cty.GetAttrPath("value"), }, Status: states.ObjectReady, Dependencies: make([]addrs.ConfigResource, 0), }, ProviderConfigAddr: addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("testing"), }, Schema: stacks_testing_provider.TestingResourceSchema, }, &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.sensitive"), ComponentInstanceAddr: mustAbsComponentInstance("component.sensitive"), Dependents: collections.NewSet(mustAbsComponent("component.self")), OutputValues: map[addrs.OutputValue]cty.Value{ addrs.OutputValue{Name: "out"}: cty.StringVal("secret").Mark(marks.Sensitive), }, InputVariables: make(map[addrs.InputVariable]cty.Value), }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("id"), Value: cty.StringVal("bb5cf32312ec"), }, } sort.SliceStable(applyChanges, func(i, j int) bool { return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j]) }) if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func TestApplyWithForcePlanTimestamp(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) 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 }, }, ForcePlanTimestamp: &fakePlanTimestamp, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) planChanges, diags := collectPlanOutput(changesCh, diagsCh) if len(diags) > 0 { t.Fatalf("expected no diagnostics, got %s", diags.ErrWithWarnings()) } // Sanity check that the plan timestamp was set correctly output := expectOutput(t, "plantimestamp", planChanges) plantimestampValue := output.After if plantimestampValue.AsString() != forcedPlanTimestamp { t.Errorf("expected plantimestamp to be %q, got %q", forcedPlanTimestamp, plantimestampValue.AsString()) } planLoader := stackplan.NewLoader() for _, change := range planChanges { proto, err := change.PlannedChangeProto() if err != nil { t.Fatal(err) } for _, rawMsg := range proto.Raw { err = planLoader.AddRaw(rawMsg) if err != nil { t.Fatal(err) } } } plan, err := planLoader.Plan() if err != nil { t.Fatal(err) } applyReq := ApplyRequest{ Config: cfg, Plan: plan, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, } applyChangesCh := make(chan stackstate.AppliedChange) diagsCh = make(chan tfdiags.Diagnostic) applyResp := ApplyResponse{ AppliedChanges: applyChangesCh, Diagnostics: diagsCh, } go Apply(ctx, &applyReq, &applyResp) applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh) if len(applyDiags) > 0 { t.Fatalf("expected no diagnostics, got %s", applyDiags.ErrWithWarnings()) } wantChanges := []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.second-self"), ComponentInstanceAddr: mustAbsComponentInstance("component.second-self"), OutputValues: map[addrs.OutputValue]cty.Value{ // We want to make sure the plantimestamp is set correctly {Name: "input"}: cty.StringVal(forcedPlanTimestamp), // plantimestamp should also be set for the module runtime used in the components {Name: "out"}: cty.StringVal(fmt.Sprintf("module-output-%s", forcedPlanTimestamp)), }, InputVariables: map[addrs.InputVariable]cty.Value{ mustInputVariable("value"): cty.StringVal(forcedPlanTimestamp), }, }, &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self"), OutputValues: map[addrs.OutputValue]cty.Value{ // We want to make sure the plantimestamp is set correctly {Name: "input"}: cty.StringVal(forcedPlanTimestamp), // plantimestamp should also be set for the module runtime used in the components {Name: "out"}: cty.StringVal(fmt.Sprintf("module-output-%s", forcedPlanTimestamp)), }, InputVariables: map[addrs.InputVariable]cty.Value{ mustInputVariable("value"): cty.StringVal(forcedPlanTimestamp), }, }, &stackstate.AppliedChangeOutputValue{ Addr: stackaddrs.OutputValue{Name: "plantimestamp"}, Value: cty.StringVal(forcedPlanTimestamp), }, } sort.SliceStable(applyChanges, func(i, j int) bool { return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j]) }) if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func TestApplyWithDefaultPlanTimestamp(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) 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 }, }, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) planChanges, diags := collectPlanOutput(changesCh, diagsCh) if len(diags) > 0 { t.Fatalf("expected no diagnostics, got %s", diags.ErrWithWarnings()) } // Sanity check that the plan timestamp was set correctly output := expectOutput(t, "plantimestamp", planChanges) 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()) } planLoader := stackplan.NewLoader() for _, change := range planChanges { proto, err := change.PlannedChangeProto() if err != nil { t.Fatal(err) } for _, rawMsg := range proto.Raw { err = planLoader.AddRaw(rawMsg) if err != nil { t.Fatal(err) } } } plan, err := planLoader.Plan() if err != nil { t.Fatal(err) } applyReq := ApplyRequest{ Config: cfg, Plan: plan, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, } applyChangesCh := make(chan stackstate.AppliedChange) diagsCh = make(chan tfdiags.Diagnostic) applyResp := ApplyResponse{ AppliedChanges: applyChangesCh, Diagnostics: diagsCh, } go Apply(ctx, &applyReq, &applyResp) applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh) if len(applyDiags) > 0 { t.Fatalf("expected no diagnostics, got %s", applyDiags.ErrWithWarnings()) } for _, x := range applyChanges { if v, ok := x.(*stackstate.AppliedChangeComponentInstance); ok { if actualTimestampValue, ok := v.OutputValues[addrs.OutputValue{ Name: "input", }]; ok { actualTimestamp, err := time.Parse(time.RFC3339, actualTimestampValue.AsString()) if err != nil { t.Fatalf("Could not parse component output value: %q", err) } if actualTimestamp.Before(dayOfWritingThisTestTime) { t.Error("Timestamp is before day of writing this test, that should be incorrect.") } } if actualTimestampValue, ok := v.OutputValues[addrs.OutputValue{ Name: "out", }]; ok { actualTimestamp, err := time.Parse(time.RFC3339, strings.ReplaceAll(actualTimestampValue.AsString(), "module-output-", "")) if err != nil { t.Fatalf("Could not parse component output value: %q", err) } if actualTimestamp.Before(dayOfWritingThisTestTime) { t.Error("Timestamp is before day of writing this test, that should be incorrect.") } } } } } func TestApplyWithFailedComponent(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, filepath.Join("with-single-input", "failed-parent")) 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) planChanges, diags := collectPlanOutput(changesCh, diagsCh) if len(diags) > 0 { t.Fatalf("expected no diagnostics, got %s", diags.ErrWithWarnings()) } planLoader := stackplan.NewLoader() for _, change := range planChanges { proto, err := change.PlannedChangeProto() if err != nil { t.Fatal(err) } for _, rawMsg := range proto.Raw { err = planLoader.AddRaw(rawMsg) if err != nil { t.Fatal(err) } } } plan, err := planLoader.Plan() if err != nil { t.Fatal(err) } applyReq := ApplyRequest{ Config: cfg, Plan: plan, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, DependencyLocks: *lock, } applyChangesCh := make(chan stackstate.AppliedChange) diagsCh = make(chan tfdiags.Diagnostic) applyResp := ApplyResponse{ AppliedChanges: applyChangesCh, Diagnostics: diagsCh, } go Apply(ctx, &applyReq, &applyResp) applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh) expectDiagnosticsForTest(t, applyDiags, // This is the expected failure, from our testing_failed_resource. expectDiagnostic(tfdiags.Error, "failedResource error", "failed during apply")) wantChanges := []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.parent"), ComponentInstanceAddr: mustAbsComponentInstance("component.parent"), Dependents: collections.NewSet(mustAbsComponent("component.self")), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: map[addrs.InputVariable]cty.Value{ mustInputVariable("input"): cty.StringVal("Hello, world!"), mustInputVariable("id"): cty.NullVal(cty.String), mustInputVariable("fail_plan"): cty.NullVal(cty.Bool), mustInputVariable("fail_apply"): cty.BoolVal(true), }, }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_failed_resource.data"), ProviderConfigAddr: mustDefaultRootProvider("testing"), }, &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self"), Dependencies: collections.NewSet(mustAbsComponent("component.parent")), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: map[addrs.InputVariable]cty.Value{ mustInputVariable("id"): cty.NullVal(cty.String), mustInputVariable("input"): cty.UnknownVal(cty.String), }, }, } sort.SliceStable(applyChanges, func(i, j int) bool { return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j]) }) if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func TestApplyWithFailedProviderLinkedComponent(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, filepath.Join("with-single-input", "failed-component-to-provider")) 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) planChanges, diags := collectPlanOutput(changesCh, diagsCh) if len(diags) > 0 { t.Fatalf("expected no diagnostics, got %s", diags.ErrWithWarnings()) } planLoader := stackplan.NewLoader() for _, change := range planChanges { proto, err := change.PlannedChangeProto() if err != nil { t.Fatal(err) } for _, rawMsg := range proto.Raw { err = planLoader.AddRaw(rawMsg) if err != nil { t.Fatal(err) } } } plan, err := planLoader.Plan() if err != nil { t.Fatal(err) } applyReq := ApplyRequest{ Config: cfg, Plan: plan, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, DependencyLocks: *lock, } applyChangesCh := make(chan stackstate.AppliedChange) diagsCh = make(chan tfdiags.Diagnostic) applyResp := ApplyResponse{ AppliedChanges: applyChangesCh, Diagnostics: diagsCh, } go Apply(ctx, &applyReq, &applyResp) applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh) expectDiagnosticsForTest(t, applyDiags, // This is the expected failure, from our testing_failed_resource. expectDiagnostic(tfdiags.Error, "failedResource error", "failed during apply")) wantChanges := []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.parent"), ComponentInstanceAddr: mustAbsComponentInstance("component.parent"), Dependents: collections.NewSet(mustAbsComponent("component.self")), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: map[addrs.InputVariable]cty.Value{ mustInputVariable("input"): cty.NullVal(cty.String), mustInputVariable("id"): cty.NullVal(cty.String), mustInputVariable("fail_plan"): cty.NullVal(cty.Bool), mustInputVariable("fail_apply"): cty.BoolVal(true), }, }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_failed_resource.data"), ProviderConfigAddr: mustDefaultRootProvider("testing"), }, &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self"), Dependencies: collections.NewSet(mustAbsComponent("component.parent")), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: map[addrs.InputVariable]cty.Value{ mustInputVariable("id"): cty.NullVal(cty.String), mustInputVariable("input"): cty.StringVal("Hello, world!"), }, }, } sort.SliceStable(applyChanges, func(i, j int) bool { return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j]) }) if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func TestApplyWithStateManipulation(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 []stackstate.AppliedChange counts collections.Map[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange] planDiags []expectedDiagnostic applyDiags []expectedDiagnostic }{ "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: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self"), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: make(map[addrs.InputVariable]cty.Value), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.after"), NewStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "moved", "value": "moved", }), Status: states.ObjectReady, AttrSensitivePaths: make([]cty.Path, 0), }, ProviderConfigAddr: mustDefaultRootProvider("testing"), PreviousResourceInstanceObjectAddr: mustAbsResourceInstanceObjectPtr("component.self.testing_resource.before"), Schema: stacks_testing_provider.TestingResourceSchema, }, }, 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, }, }), }, "moved-failed-dep": { 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: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self"), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: make(map[addrs.InputVariable]cty.Value), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_failed_resource.resource"), ProviderConfigAddr: mustDefaultRootProvider("testing"), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.after"), NewStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "moved", "value": "moved", }), Status: states.ObjectReady, AttrSensitivePaths: make([]cty.Path, 0), Dependencies: []addrs.ConfigResource{ { Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_failed_resource", Name: "resource", }, }, }, }, ProviderConfigAddr: mustDefaultRootProvider("testing"), PreviousResourceInstanceObjectAddr: mustAbsResourceInstanceObjectPtr("component.self.testing_resource.before"), Schema: stacks_testing_provider.TestingResourceSchema, }, }, 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, }, }), applyDiags: []expectedDiagnostic{ // This error comes from the testing_failed_resource expectDiagnostic(tfdiags.Error, "failedResource error", "failed during apply"), }, }, "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: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self"), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: map[addrs.InputVariable]cty.Value{ mustInputVariable("id"): cty.StringVal("imported"), }, }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), NewStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "imported", "value": "imported", }), Status: states.ObjectReady, AttrSensitivePaths: make([]cty.Path, 0), }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("id"), Value: cty.StringVal("imported"), }, }, 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, }, }), }, "import-failed-dep": { 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: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self"), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: map[addrs.InputVariable]cty.Value{ mustInputVariable("id"): cty.StringVal("imported"), }, }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_failed_resource.resource"), ProviderConfigAddr: mustDefaultRootProvider("testing"), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), NewStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "imported", "value": "imported", }), Status: states.ObjectReady, AttrSensitivePaths: make([]cty.Path, 0), Dependencies: []addrs.ConfigResource{ { Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "testing_failed_resource", Name: "resource", }, }, }, }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("id"), Value: cty.StringVal("imported"), }, }, 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, }, }), applyDiags: []expectedDiagnostic{ // This error comes from the testing_failed_resource expectDiagnostic(tfdiags.Error, "failedResource error", "failed during apply"), }, }, "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: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self"), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: make(map[addrs.InputVariable]cty.Value), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.resource"), NewStateSrc: nil, // Deleted, so is nil. ProviderConfigAddr: mustDefaultRootProvider("testing"), }, }, 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, }, }), planDiags: []expectedDiagnostic{ expectDiagnostic(tfdiags.Warning, "Some objects will no longer be managed by Terraform", "If you apply this plan, Terraform will discard its tracking information for the following objects, but it will not delete them:\n - testing_resource.resource\n\nAfter applying this plan, Terraform will no longer manage these objects. You will need to import them into Terraform to manage them again."), }, }, "removed-failed-dep": { 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: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self"), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: make(map[addrs.InputVariable]cty.Value), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_failed_resource.resource"), ProviderConfigAddr: mustDefaultRootProvider("testing"), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.resource"), NewStateSrc: nil, // Deleted, so is nil. ProviderConfigAddr: mustDefaultRootProvider("testing"), }, }, 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, }, }), planDiags: []expectedDiagnostic{ expectDiagnostic(tfdiags.Warning, "Some objects will no longer be managed by Terraform", "If you apply this plan, Terraform will discard its tracking information for the following objects, but it will not delete them:\n - testing_resource.resource\n\nAfter applying this plan, Terraform will no longer manage these objects. You will need to import them into Terraform to manage them again."), }, applyDiags: []expectedDiagnostic{ // This error comes from the testing_failed_resource expectDiagnostic(tfdiags.Error, "failedResource error", "failed during apply"), }, }, "deferred": { store: stacks_testing_provider.NewResourceStoreBuilder(). AddResource("self", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("deferred"), "value": cty.UnknownVal(cty.String), })). Build(), changes: []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.deferred"), ComponentInstanceAddr: mustAbsComponentInstance("component.deferred"), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: make(map[addrs.InputVariable]cty.Value), }, &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.ok"), ComponentInstanceAddr: mustAbsComponentInstance("component.ok"), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: make(map[addrs.InputVariable]cty.Value), }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.ok.testing_resource.self"), NewStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "ok", "value": "ok", }), Status: states.ObjectReady, AttrSensitivePaths: nil, Dependencies: []addrs.ConfigResource{}, }, ProviderConfigAddr: mustDefaultRootProvider("testing"), PreviousResourceInstanceObjectAddr: nil, Schema: stacks_testing_provider.TestingResourceSchema, }, }, counts: collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]( collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{ K: mustAbsComponentInstance("component.ok"), V: &hooks.ComponentInstanceChange{ Addr: mustAbsComponentInstance("component.ok"), Add: 1, Defer: 0, }, }, collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{ K: mustAbsComponentInstance("component.deferred"), V: &hooks.ComponentInstanceChange{ Addr: mustAbsComponentInstance("component.deferred"), Defer: 1, }, }, ), }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, path.Join("state-manipulation", name)) 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 }, } planChangeCh := make(chan stackplan.PlannedChange) diagsCh := make(chan tfdiags.Diagnostic) planReq := PlanRequest{ Config: cfg, ProviderFactories: providers, InputValues: inputs, ForcePlanTimestamp: &fakePlanTimestamp, PrevState: tc.state, DependencyLocks: *lock, } planResp := PlanResponse{ PlannedChanges: planChangeCh, Diagnostics: diagsCh, } go Plan(ctx, &planReq, &planResp) planChanges, diags := collectPlanOutput(planChangeCh, diagsCh) sort.SliceStable(diags, diagnosticSortFunc(diags)) expectDiagnosticsForTest(t, diags, tc.planDiags...) // Check the counts during the apply for this test. gotCounts := collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]() ctx = ContextWithHooks(ctx, &stackeval.Hooks{ ReportComponentInstanceApplied: func(ctx context.Context, span any, change *hooks.ComponentInstanceChange) any { gotCounts.Put(change.Addr, change) return span }, }) planLoader := stackplan.NewLoader() for _, change := range planChanges { proto, err := change.PlannedChangeProto() if err != nil { t.Fatal(err) } for _, rawMsg := range proto.Raw { err = planLoader.AddRaw(rawMsg) if err != nil { t.Fatal(err) } } } plan, err := planLoader.Plan() if err != nil { t.Fatal(err) } applyReq := ApplyRequest{ Config: cfg, Plan: plan, ProviderFactories: providers, DependencyLocks: *lock, } applyChangesCh := make(chan stackstate.AppliedChange) diagsCh = make(chan tfdiags.Diagnostic) applyResp := ApplyResponse{ AppliedChanges: applyChangesCh, Diagnostics: diagsCh, } go Apply(ctx, &applyReq, &applyResp) applyChanges, diags := collectApplyOutput(applyChangesCh, diagsCh) sort.SliceStable(diags, diagnosticSortFunc(diags)) expectDiagnosticsForTest(t, diags, tc.applyDiags...) sort.SliceStable(applyChanges, func(i, j int) bool { return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j]) }) if diff := cmp.Diff(tc.changes, applyChanges, 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 TestApplyWithChangedInputValues(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, filepath.Join("with-single-input", "valid")) 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{ stackaddrs.InputVariable{Name: "input"}: { Value: cty.StringVal("hello"), }, }, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) planChanges, diags := collectPlanOutput(changesCh, diagsCh) if len(diags) > 0 { t.Fatalf("expected no diagnostics, got %s", diags.ErrWithWarnings()) } planLoader := stackplan.NewLoader() for _, change := range planChanges { proto, err := change.PlannedChangeProto() if err != nil { t.Fatal(err) } for _, rawMsg := range proto.Raw { err = planLoader.AddRaw(rawMsg) if err != nil { t.Fatal(err) } } } plan, err := planLoader.Plan() if err != nil { t.Fatal(err) } applyReq := ApplyRequest{ Config: cfg, Plan: plan, 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{ // This time we're deliberately changing the values we're giving // to the apply operation. We expect this to fail earlier than // the previous test. stackaddrs.InputVariable{Name: "input"}: { Value: cty.StringVal("world"), }, }, } applyChangesCh := make(chan stackstate.AppliedChange) diagsCh = make(chan tfdiags.Diagnostic) applyResp := ApplyResponse{ AppliedChanges: applyChangesCh, Diagnostics: diagsCh, } go Apply(ctx, &applyReq, &applyResp) applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh) sort.SliceStable(applyDiags, diagnosticSortFunc(applyDiags)) expectDiagnosticsForTest(t, applyDiags, expectDiagnostic( tfdiags.Error, "Inconsistent value for input variable during apply", "The value for non-ephemeral input variable \"input\" was set to a different value during apply than was set during plan. Only ephemeral input variables can change between the plan and apply phases."), expectDiagnostic(tfdiags.Error, "Invalid inputs for component", "Input variable \"input\" could not be evaluated, additional diagnostics elsewhere should provide mode detail."), ) wantChanges := []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self"), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: make(map[addrs.InputVariable]cty.Value), }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("id"), Value: cty.NullVal(cty.String), }, // no resources should have been created because the input variable was // invalid. } sort.SliceStable(applyChanges, func(i, j int) bool { return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j]) }) if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func TestApplyAutomaticInputConversion(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, filepath.Join("with-single-input", "for-each-component")) 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{ stackaddrs.InputVariable{Name: "input"}: { // The stack expects a map of strings, but we're giving it // an object. Terraform should automatically convert this to // the expected type. Value: cty.ObjectVal(map[string]cty.Value{ "hello": cty.StringVal("hello"), "world": cty.StringVal("world"), }), }, }, } resp := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &req, &resp) planChanges, planDiags := collectPlanOutput(changesCh, diagsCh) if len(planDiags) > 0 { t.Fatalf("expected no diagnostics, got %s", planDiags.ErrWithWarnings()) } planLoader := stackplan.NewLoader() for _, change := range planChanges { proto, err := change.PlannedChangeProto() if err != nil { t.Fatal(err) } for _, rawMsg := range proto.Raw { err = planLoader.AddRaw(rawMsg) if err != nil { t.Fatal(err) } } } plan, err := planLoader.Plan() if err != nil { t.Fatal(err) } applyReq := ApplyRequest{ Config: cfg, Plan: plan, 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{ stackaddrs.InputVariable{Name: "input"}: { // The stack expects a map of strings, but we're giving it // an object. Terraform should automatically convert this to // the expected type. Value: cty.ObjectVal(map[string]cty.Value{ "hello": cty.StringVal("hello"), "world": cty.StringVal("world"), }), }, }, } applyChangesCh := make(chan stackstate.AppliedChange) diagsCh = make(chan tfdiags.Diagnostic) applyResp := ApplyResponse{ AppliedChanges: applyChangesCh, Diagnostics: diagsCh, } go Apply(ctx, &applyReq, &applyResp) applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh) if len(applyDiags) > 0 { t.Fatalf("expected no diagnostics, got %s", applyDiags.ErrWithWarnings()) } sort.SliceStable(applyChanges, func(i, j int) bool { return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j]) }) wantChanges := []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"hello\"]"), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: map[addrs.InputVariable]cty.Value{ mustInputVariable("id"): cty.StringVal("hello"), mustInputVariable("input"): cty.StringVal("hello"), }, }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"hello\"].testing_resource.data"), NewStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "hello", "value": "hello", }), Status: states.ObjectReady, Dependencies: make([]addrs.ConfigResource, 0), }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"world\"]"), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: map[addrs.InputVariable]cty.Value{ mustInputVariable("id"): cty.StringVal("world"), mustInputVariable("input"): cty.StringVal("world"), }, }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"world\"].testing_resource.data"), NewStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "world", "value": "world", }), Status: states.ObjectReady, Dependencies: make([]addrs.ConfigResource, 0), }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("input"), Value: cty.MapVal(map[string]cty.Value{ "hello": cty.StringVal("hello"), "world": cty.StringVal("world"), }), }, } if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func TestApply_DependsOnComponentWithNoInstances(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) planRequest := 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!"), }, }, } planResponse := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &planRequest, &planResponse) planChanges, planDiags := collectPlanOutput(changesCh, diagsCh) reportDiagnosticsForTest(t, planDiags) if len(planDiags) != 0 { t.FailNow() } planLoader := stackplan.NewLoader() for _, change := range planChanges { proto, err := change.PlannedChangeProto() if err != nil { t.Fatal(err) } for _, rawMsg := range proto.Raw { err = planLoader.AddRaw(rawMsg) if err != nil { t.Fatal(err) } } } plan, err := planLoader.Plan() if err != nil { t.Fatal(err) } applyReq := ApplyRequest{ Config: cfg, Plan: plan, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, DependencyLocks: *lock, } applyChangesCh := make(chan stackstate.AppliedChange) diagsCh = make(chan tfdiags.Diagnostic) applyResp := ApplyResponse{ AppliedChanges: applyChangesCh, Diagnostics: diagsCh, } go Apply(ctx, &applyReq, &applyResp) _, applyDiags := collectApplyOutput(applyChangesCh, diagsCh) reportDiagnosticsForTest(t, applyDiags) if len(applyDiags) != 0 { t.FailNow() } // don't care about the changes - just want to make sure that depends_on // reference to a component with zero instances doesn't break anything } func TestApply_WithProviderFunctions(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, filepath.Join("with-provider-functions")) 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) planRequest := 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!"), }, }, } planResponse := PlanResponse{ PlannedChanges: changesCh, Diagnostics: diagsCh, } go Plan(ctx, &planRequest, &planResponse) planChanges, planDiags := collectPlanOutput(changesCh, diagsCh) reportDiagnosticsForTest(t, planDiags) if len(planDiags) != 0 { t.FailNow() } sort.SliceStable(planChanges, func(i, j int) bool { return plannedChangeSortKey(planChanges[i]) < plannedChangeSortKey(planChanges[j]) }) wantPlanChanges := []stackplan.PlannedChange{ &stackplan.PlannedChangeApplyable{ Applyable: true, }, &stackplan.PlannedChangeComponentInstance{ Addr: mustAbsComponentInstance("component.self"), PlanApplyable: true, PlanComplete: true, Action: plans.Create, RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](), PlannedInputValues: map[string]plans.DynamicValue{ "id": mustPlanDynamicValueDynamicType(cty.StringVal("2f9f3b84")), "input": mustPlanDynamicValueDynamicType(cty.StringVal("hello, world!")), }, PlannedInputValueMarks: map[string][]cty.PathValueMarks{ "id": nil, "input": nil, }, PlannedOutputValues: map[string]cty.Value{ "value": cty.StringVal("hello, world!"), }, PlannedCheckResults: &states.CheckResults{}, PlannedProviderFunctionResults: []lang.FunctionResultHash{ { Key: providerFunctionHashArgs(mustDefaultRootProvider("testing").Provider, "echo", cty.StringVal("hello, world!")), Result: providerFunctionHashResult(cty.StringVal("hello, world!")), }, }, 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.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("2f9f3b84"), "value": cty.StringVal("hello, world!"), })), }, }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackplan.PlannedChangeProviderFunctionResults{ Results: []lang.FunctionResultHash{ { Key: providerFunctionHashArgs(mustDefaultRootProvider("testing").Provider, "echo", cty.StringVal("hello, world!")), Result: providerFunctionHashResult(cty.StringVal("hello, world!")), }, }, }, &stackplan.PlannedChangeHeader{ TerraformVersion: version.SemVer, }, &stackplan.PlannedChangeOutputValue{ Addr: stackaddrs.OutputValue{Name: "value"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.StringVal("hello, world!"), }, &stackplan.PlannedChangePlannedTimestamp{ PlannedTimestamp: fakePlanTimestamp, }, &stackplan.PlannedChangeRootInputValue{ Addr: stackaddrs.InputVariable{Name: "input"}, Action: plans.Create, Before: cty.NullVal(cty.DynamicPseudoType), After: cty.StringVal("hello, world!"), }, } if diff := cmp.Diff(wantPlanChanges, planChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } planLoader := stackplan.NewLoader() for _, change := range planChanges { proto, err := change.PlannedChangeProto() if err != nil { t.Fatal(err) } for _, rawMsg := range proto.Raw { err = planLoader.AddRaw(rawMsg) if err != nil { t.Fatal(err) } } } plan, err := planLoader.Plan() if err != nil { t.Fatal(err) } // just verify the plan is correctly loading the provider function results // as well if len(plan.FunctionResults) == 0 { t.Errorf("expected provider function results, got none") if len(plan.GetComponent(mustAbsComponentInstance("component.self")).PlannedFunctionResults) == 0 { t.Errorf("expected component function results, got none") } } applyReq := ApplyRequest{ Config: cfg, Plan: plan, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, DependencyLocks: *lock, } applyChangesCh := make(chan stackstate.AppliedChange) diagsCh = make(chan tfdiags.Diagnostic) applyResp := ApplyResponse{ AppliedChanges: applyChangesCh, Diagnostics: diagsCh, } go Apply(ctx, &applyReq, &applyResp) applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh) reportDiagnosticsForTest(t, applyDiags) if len(applyDiags) != 0 { t.FailNow() } sort.SliceStable(applyChanges, func(i, j int) bool { return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j]) }) wantApplyChanges := []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self"), OutputValues: map[addrs.OutputValue]cty.Value{ {Name: "value"}: cty.StringVal("hello, world!"), }, InputVariables: map[addrs.InputVariable]cty.Value{ mustInputVariable("id"): cty.StringVal("2f9f3b84"), mustInputVariable("input"): cty.StringVal("hello, world!"), }, }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), NewStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "2f9f3b84", "value": "hello, world!", }), Status: states.ObjectReady, Dependencies: make([]addrs.ConfigResource, 0), }, ProviderConfigAddr: mustDefaultRootProvider("testing"), Schema: stacks_testing_provider.TestingResourceSchema, }, &stackstate.AppliedChangeOutputValue{ Addr: stackaddrs.OutputValue{Name: "value"}, Value: cty.StringVal("hello, world!"), }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("input"), Value: cty.StringVal("hello, world!"), }, } if diff := cmp.Diff(wantApplyChanges, applyChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func TestApplyFailedDependencyWithResourceInState(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, "failed-dependency") fakePlanTimestamp, err := time.Parse(time.RFC3339, "2021-01-01T00:00:00Z") 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{}), ) store := stacks_testing_provider.NewResourceStoreBuilder(). AddResource("resource", cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("resource"), "value": cty.NullVal(cty.String), })). Build() planReq := PlanRequest{ PlanMode: plans.NormalMode, Config: cfg, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProviderWithData(t, store), nil }, }, DependencyLocks: *lock, ForcePlanTimestamp: &fakePlanTimestamp, InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ stackaddrs.InputVariable{Name: "fail_apply"}: { Value: cty.True, }, }, // We have a resource in the state from a previous run. We shouldn't // emit any state changes to this resource as a result of the dependency // failing. PrevState: stackstate.NewStateBuilder(). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.data")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ SchemaVersion: 0, AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "resource", "value": nil, }), Status: states.ObjectReady, })). Build(), } planChangesCh := make(chan stackplan.PlannedChange) planDiagsCh := make(chan tfdiags.Diagnostic) planResp := PlanResponse{ PlannedChanges: planChangesCh, Diagnostics: planDiagsCh, } go Plan(ctx, &planReq, &planResp) planChanges, planDiags := collectPlanOutput(planChangesCh, planDiagsCh) if len(planDiags) > 0 { t.Fatalf("unexpected diagnostics during planning: %s", planDiags) } planLoader := stackplan.NewLoader() for _, change := range planChanges { proto, err := change.PlannedChangeProto() if err != nil { t.Fatal(err) } for _, rawMsg := range proto.Raw { err = planLoader.AddRaw(rawMsg) if err != nil { t.Fatal(err) } } } plan, err := planLoader.Plan() if err != nil { t.Fatal(err) } applyReq := ApplyRequest{ Config: cfg, Plan: plan, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProviderWithData(t, store), nil }, }, DependencyLocks: *lock, } applyChangesCh := make(chan stackstate.AppliedChange) applyDiagsCh := make(chan tfdiags.Diagnostic) applyResp := ApplyResponse{ AppliedChanges: applyChangesCh, Diagnostics: applyDiagsCh, } go Apply(ctx, &applyReq, &applyResp) applyChanges, applyDiags := collectApplyOutput(applyChangesCh, applyDiagsCh) expectDiagnosticsForTest(t, applyDiags, expectDiagnostic(tfdiags.Error, "failedResource error", "failed during apply")) wantChanges := []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self"), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: map[addrs.InputVariable]cty.Value{ mustInputVariable("resource_id"): cty.StringVal("resource"), mustInputVariable("failed_id"): cty.StringVal("failed"), mustInputVariable("fail_apply"): cty.True, mustInputVariable("fail_plan"): cty.False, mustInputVariable("input"): cty.NullVal(cty.String), }, }, &stackstate.AppliedChangeResourceInstanceObject{ // This has no state as the apply operation failed and it wasn't // in the state before. ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_failed_resource.data"), ProviderConfigAddr: mustDefaultRootProvider("testing"), }, &stackstate.AppliedChangeResourceInstanceObject{ // This emits the state from the previous run, as it was not // changed during this run. ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), ProviderConfigAddr: mustDefaultRootProvider("testing"), NewStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "resource", "value": nil, }), AttrSensitivePaths: make([]cty.Path, 0), Status: states.ObjectReady, Dependencies: []addrs.ConfigResource{mustAbsResourceInstance("testing_failed_resource.data").ConfigResource()}, }, Schema: stacks_testing_provider.TestingResourceSchema, }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("fail_apply"), Value: cty.True, }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("fail_plan"), Value: cty.False, }, } sort.SliceStable(applyChanges, func(i, j int) bool { return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j]) }) if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func TestApplyManuallyRemovedResource(t *testing.T) { ctx := context.Background() cfg := loadMainBundleConfigForTest(t, filepath.Join("with-single-input", "valid")) fakePlanTimestamp, err := time.Parse(time.RFC3339, "2021-01-01T00:00:00Z") 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{}), ) planReq := PlanRequest{ PlanMode: plans.NormalMode, 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{ stackaddrs.InputVariable{Name: "id"}: { Value: cty.StringVal("foo"), }, stackaddrs.InputVariable{Name: "input"}: { Value: cty.StringVal("hello"), }, }, // We have in the previous state a resource that is not in our // underlying data store. This simulates the case where someone went // in and manually deleted a resource that Terraform is managing. // // Some providers will return an error in this case, but some will // not. We need to ensure that we handle the second case gracefully. PrevState: stackstate.NewStateBuilder(). AddResourceInstance(stackstate.NewResourceInstanceBuilder(). SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.missing")). SetProviderAddr(mustDefaultRootProvider("testing")). SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ SchemaVersion: 0, AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "e84b59f2", "value": "hello", }), Status: states.ObjectReady, })). Build(), } planChangesCh := make(chan stackplan.PlannedChange) planDiagsCh := make(chan tfdiags.Diagnostic) planResp := PlanResponse{ PlannedChanges: planChangesCh, Diagnostics: planDiagsCh, } go Plan(ctx, &planReq, &planResp) planChanges, planDiags := collectPlanOutput(planChangesCh, planDiagsCh) if len(planDiags) > 0 { t.Fatalf("unexpected diagnostics during planning: %s", planDiags) } planLoader := stackplan.NewLoader() for _, change := range planChanges { proto, err := change.PlannedChangeProto() if err != nil { t.Fatal(err) } for _, rawMsg := range proto.Raw { err = planLoader.AddRaw(rawMsg) if err != nil { t.Fatal(err) } } } plan, err := planLoader.Plan() if err != nil { t.Fatal(err) } applyReq := ApplyRequest{ Config: cfg, Plan: plan, ProviderFactories: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { return stacks_testing_provider.NewProvider(t), nil }, }, DependencyLocks: *lock, } applyChangesCh := make(chan stackstate.AppliedChange) applyDiagsCh := make(chan tfdiags.Diagnostic) applyResp := ApplyResponse{ AppliedChanges: applyChangesCh, Diagnostics: applyDiagsCh, } go Apply(ctx, &applyReq, &applyResp) applyChanges, applyDiags := collectApplyOutput(applyChangesCh, applyDiagsCh) if len(applyDiags) > 0 { t.Fatalf("unexpected diagnostics during apply: %s", applyDiags) } wantChanges := []stackstate.AppliedChange{ &stackstate.AppliedChangeComponentInstance{ ComponentAddr: mustAbsComponent("component.self"), ComponentInstanceAddr: mustAbsComponentInstance("component.self"), OutputValues: make(map[addrs.OutputValue]cty.Value), InputVariables: map[addrs.InputVariable]cty.Value{ mustInputVariable("id"): cty.StringVal("foo"), mustInputVariable("input"): cty.StringVal("hello"), }, }, // The resource in our configuration has been updated, so that is // present as normal. &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), ProviderConfigAddr: mustDefaultRootProvider("testing"), NewStateSrc: &states.ResourceInstanceObjectSrc{ AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ "id": "foo", "value": "hello", }), Status: states.ObjectReady, Dependencies: make([]addrs.ConfigResource, 0), }, Schema: stacks_testing_provider.TestingResourceSchema, }, // The resource that was in state but not in the configuration should // be removed from state. &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.missing"), ProviderConfigAddr: mustDefaultRootProvider("testing"), NewStateSrc: nil, // We should be removing this from the state file. Schema: providers.Schema{}, }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("id"), Value: cty.StringVal("foo"), }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("input"), Value: cty.StringVal("hello"), }, } sort.SliceStable(applyChanges, func(i, j int) bool { return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j]) }) if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" { t.Errorf("wrong changes\n%s", diff) } } func collectApplyOutput(changesCh <-chan stackstate.AppliedChange, diagsCh <-chan tfdiags.Diagnostic) ([]stackstate.AppliedChange, tfdiags.Diagnostics) { var changes []stackstate.AppliedChange 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) } } }