| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package stackruntime | |
| import ( | |
| "context" | |
| "fmt" | |
| "path" | |
| "path/filepath" | |
| "sort" | |
| "strconv" | |
| "strings" | |
| "sync" | |
| "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/policy" | |
| "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.1.0"), | |
| providerreqs.MustParseVersionConstraints("0.1.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.1.0"), | |
| providerreqs.MustParseVersionConstraints("0.1.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.1.0"), | |
| providerreqs.MustParseVersionConstraints("0.1.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.1.0"), | |
| providerreqs.MustParseVersionConstraints("0.1.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.1.0"), | |
| providerreqs.MustParseVersionConstraints("0.1.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.1.0"), | |
| providerreqs.MustParseVersionConstraints("0.1.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]() | |
| var gotCountsMu sync.Mutex | |
| ctx = ContextWithHooks(ctx, &stackeval.Hooks{ | |
| ReportComponentInstanceApplied: func(ctx context.Context, span any, change *hooks.ComponentInstanceChange) any { | |
| gotCountsMu.Lock() | |
| defer gotCountsMu.Unlock() | |
| 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 | |
| gotCountsMu.Lock() | |
| defer gotCountsMu.Unlock() | |
| 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.1.0"), | |
| providerreqs.MustParseVersionConstraints("0.1.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.1.0"), | |
| providerreqs.MustParseVersionConstraints("0.1.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.1.0"), | |
| providerreqs.MustParseVersionConstraints("0.1.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.1.0"), | |
| providerreqs.MustParseVersionConstraints("0.1.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.1.0"), | |
| providerreqs.MustParseVersionConstraints("0.1.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.1.0"), | |
| providerreqs.MustParseVersionConstraints("0.1.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 TestApply_WithPolicyResults(t *testing.T) { | |
| ctx := context.Background() | |
| cfg := loadMainBundleConfigForTest(t, "policy-evaluation") | |
| lock := depsfile.NewLocks() | |
| lock.SetProvider( | |
| addrs.NewDefaultProvider("testing"), | |
| providerreqs.MustParseVersion("0.1.0"), | |
| providerreqs.MustParseVersionConstraints("0.1.0"), | |
| providerreqs.PreferredHashes([]providerreqs.Hash{}), | |
| ) | |
| plan := planForApplyTest(t, ctx, PlanRequest{ | |
| PlanMode: plans.NormalMode, | |
| // Omit policy client as we're not asserting policy results for the plan phase in this test | |
| PolicyClient: nil, | |
| Config: cfg, | |
| ProviderFactories: map[addrs.Provider]providers.Factory{ | |
| addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { | |
| return stacks_testing_provider.NewProvider(t), nil | |
| }, | |
| }, | |
| DependencyLocks: *lock, | |
| }) | |
| gotPolicyResults := applyAndCollectPolicyResults(t, ctx, 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, | |
| PolicyClient: policyEvaluationTestClient(t), | |
| }) | |
| wantPolicyResults := map[string]map[string]policy.EvaluationResponse{ | |
| `component.simple_component["comp1"]`: createExpectedComponentInstancePolicyEvaluation("policy-evaluation"), | |
| `component.simple_component["comp2"]`: createExpectedComponentInstancePolicyEvaluation("policy-evaluation"), | |
| `provider["registry.terraform.io/hashicorp/testing"].default["comp1"]`: createExpectedProviderInstancePolicyEvaluation("policy-evaluation"), | |
| `provider["registry.terraform.io/hashicorp/testing"].default["comp2"]`: createExpectedProviderInstancePolicyEvaluation("policy-evaluation"), | |
| } | |
| if diff := cmp.Diff(gotPolicyResults, wantPolicyResults, cmp.Comparer(simplePolicyDiagCompare)); diff != "" { | |
| t.Errorf("wrong policy results\n%s", diff) | |
| } | |
| } | |
| func TestApply_WithPolicyResults_EmbeddedStack(t *testing.T) { | |
| ctx := context.Background() | |
| cfg := loadMainBundleConfigForTest(t, "policy-evaluation-embedded-stack") | |
| lock := depsfile.NewLocks() | |
| lock.SetProvider( | |
| addrs.NewDefaultProvider("testing"), | |
| providerreqs.MustParseVersion("0.1.0"), | |
| providerreqs.MustParseVersionConstraints("0.1.0"), | |
| providerreqs.PreferredHashes([]providerreqs.Hash{}), | |
| ) | |
| plan := planForApplyTest(t, ctx, PlanRequest{ | |
| PlanMode: plans.NormalMode, | |
| // Omit policy client as we're not asserting policy results for the plan phase in this test | |
| PolicyClient: nil, | |
| Config: cfg, | |
| ProviderFactories: map[addrs.Provider]providers.Factory{ | |
| addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { | |
| return stacks_testing_provider.NewProvider(t), nil | |
| }, | |
| }, | |
| DependencyLocks: *lock, | |
| }) | |
| gotPolicyResults := applyAndCollectPolicyResults(t, ctx, 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, | |
| PolicyClient: policyEvaluationTestClient(t), | |
| }) | |
| wantPolicyResults := map[string]map[string]policy.EvaluationResponse{ | |
| `stack.embedded.component.simple_component["comp1"]`: createExpectedComponentInstancePolicyEvaluation("policy-evaluation-embedded-stack/embedded"), | |
| `stack.embedded.component.simple_component["comp2"]`: createExpectedComponentInstancePolicyEvaluation("policy-evaluation-embedded-stack/embedded"), | |
| `stack.embedded.provider["registry.terraform.io/hashicorp/testing"].default["comp1"]`: createExpectedProviderInstancePolicyEvaluation("policy-evaluation-embedded-stack/embedded"), | |
| `stack.embedded.provider["registry.terraform.io/hashicorp/testing"].default["comp2"]`: createExpectedProviderInstancePolicyEvaluation("policy-evaluation-embedded-stack/embedded"), | |
| } | |
| if diff := cmp.Diff(gotPolicyResults, wantPolicyResults, cmp.Comparer(simplePolicyDiagCompare)); diff != "" { | |
| t.Errorf("wrong policy results\n%s", diff) | |
| } | |
| } | |
| func TestApply_WithPolicyResultsOnRefresh(t *testing.T) { | |
| ctx := context.Background() | |
| cfg := loadMainBundleConfigForTest(t, "policy-evaluation") | |
| lock := depsfile.NewLocks() | |
| lock.SetProvider( | |
| addrs.NewDefaultProvider("testing"), | |
| providerreqs.MustParseVersion("0.1.0"), | |
| providerreqs.MustParseVersionConstraints("0.1.0"), | |
| providerreqs.PreferredHashes([]providerreqs.Hash{}), | |
| ) | |
| // Create a provider data store to indicate drift so the plan is applyable | |
| dataStore := stacks_testing_provider.NewResourceStoreBuilder(). | |
| AddResource("comp1-parent", cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("comp1-parent"), | |
| "value": cty.StringVal("drifted value"), | |
| })). | |
| AddResource("comp1-child", cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("comp1-child"), | |
| "value": cty.StringVal("drifted value"), | |
| })). | |
| AddResource("comp2-parent", cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("comp2-parent"), | |
| "value": cty.StringVal("drifted value"), | |
| })). | |
| AddResource("comp2-child", cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("comp2-child"), | |
| "value": cty.StringVal("drifted value"), | |
| })). | |
| Build() | |
| plan := planForApplyTest(t, ctx, PlanRequest{ | |
| PlanMode: plans.RefreshOnlyMode, | |
| // Omit policy client as we're not asserting policy results for the plan phase in this test | |
| PolicyClient: nil, | |
| Config: cfg, | |
| PrevState: policyEvaluationPriorState(t), | |
| ProviderFactories: map[addrs.Provider]providers.Factory{ | |
| addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { | |
| return stacks_testing_provider.NewProviderWithData(t, dataStore), nil | |
| }, | |
| }, | |
| DependencyLocks: *lock, | |
| }) | |
| gotPolicyResults := applyAndCollectPolicyResults(t, ctx, ApplyRequest{ | |
| Config: cfg, | |
| Plan: plan, | |
| ProviderFactories: map[addrs.Provider]providers.Factory{ | |
| addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { | |
| return stacks_testing_provider.NewProviderWithData(t, dataStore), nil | |
| }, | |
| }, | |
| DependencyLocks: *lock, | |
| PolicyClient: policyEvaluationTestClient(t), | |
| }) | |
| wantPolicyResults := map[string]map[string]policy.EvaluationResponse{ | |
| // The module runtime currently does not evaluate resource policies during refresh, only module policies | |
| `component.simple_component["comp1"]`: createExpectedComponentInstancePolicyEvaluationForModules("policy-evaluation"), | |
| `component.simple_component["comp2"]`: createExpectedComponentInstancePolicyEvaluationForModules("policy-evaluation"), | |
| `provider["registry.terraform.io/hashicorp/testing"].default["comp1"]`: createExpectedProviderInstancePolicyEvaluation("policy-evaluation"), | |
| `provider["registry.terraform.io/hashicorp/testing"].default["comp2"]`: createExpectedProviderInstancePolicyEvaluation("policy-evaluation"), | |
| } | |
| if diff := cmp.Diff(gotPolicyResults, wantPolicyResults, cmp.Comparer(simplePolicyDiagCompare)); diff != "" { | |
| t.Errorf("wrong policy results\n%s", diff) | |
| } | |
| } | |
| func TestApply_WithPolicyResultsOnDestroy(t *testing.T) { | |
| ctx := context.Background() | |
| cfg := loadMainBundleConfigForTest(t, "policy-evaluation") | |
| lock := depsfile.NewLocks() | |
| lock.SetProvider( | |
| addrs.NewDefaultProvider("testing"), | |
| providerreqs.MustParseVersion("0.1.0"), | |
| providerreqs.MustParseVersionConstraints("0.1.0"), | |
| providerreqs.PreferredHashes([]providerreqs.Hash{}), | |
| ) | |
| plan := planForApplyTest(t, ctx, PlanRequest{ | |
| PlanMode: plans.DestroyMode, | |
| // Omit policy client as we're not asserting policy results for the plan phase in this test | |
| PolicyClient: nil, | |
| Config: cfg, | |
| PrevState: policyEvaluationPriorState(t), | |
| ProviderFactories: map[addrs.Provider]providers.Factory{ | |
| addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { | |
| return stacks_testing_provider.NewProviderWithData(t, policyEvaluationResourceStore(t)), nil | |
| }, | |
| }, | |
| DependencyLocks: *lock, | |
| }) | |
| gotPolicyResults := applyAndCollectPolicyResults(t, ctx, ApplyRequest{ | |
| Config: cfg, | |
| Plan: plan, | |
| ProviderFactories: map[addrs.Provider]providers.Factory{ | |
| addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { | |
| return stacks_testing_provider.NewProviderWithData(t, policyEvaluationResourceStore(t)), nil | |
| }, | |
| }, | |
| DependencyLocks: *lock, | |
| PolicyClient: policyEvaluationTestClient(t), | |
| }) | |
| wantPolicyResults := map[string]map[string]policy.EvaluationResponse{ | |
| // Module policies are not evaluated during the destroy apply (only during pre-destroy refresh) | |
| `component.simple_component["comp1"]`: createExpectedComponentInstancePolicyEvaluationForResources("policy-evaluation"), | |
| `component.simple_component["comp2"]`: createExpectedComponentInstancePolicyEvaluationForResources("policy-evaluation"), | |
| `provider["registry.terraform.io/hashicorp/testing"].default["comp1"]`: createExpectedProviderInstancePolicyEvaluation("policy-evaluation"), | |
| `provider["registry.terraform.io/hashicorp/testing"].default["comp2"]`: createExpectedProviderInstancePolicyEvaluation("policy-evaluation"), | |
| } | |
| if diff := cmp.Diff(gotPolicyResults, wantPolicyResults, cmp.Comparer(simplePolicyDiagCompare)); diff != "" { | |
| t.Errorf("wrong policy results\n%s", diff) | |
| } | |
| } | |
| func TestApply_WithPolicyResultsOnRemovedComponent(t *testing.T) { | |
| ctx := context.Background() | |
| removedCfg := loadMainBundleConfigForTest(t, "policy-evaluation-removed") | |
| lock := depsfile.NewLocks() | |
| lock.SetProvider( | |
| addrs.NewDefaultProvider("testing"), | |
| providerreqs.MustParseVersion("0.1.0"), | |
| providerreqs.MustParseVersionConstraints("0.1.0"), | |
| providerreqs.PreferredHashes([]providerreqs.Hash{}), | |
| ) | |
| // Create comp1, remove/destroy comp2 | |
| priorState := stackstate.NewStateBuilder(). | |
| AddInput("component_names", cty.SetVal([]cty.Value{cty.StringVal("comp1"), cty.StringVal("comp2")})). | |
| AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance(`component.simple_component["comp2"]`)). | |
| AddInputVariable("name", cty.StringVal("comp2"))). | |
| AddResourceInstance(stackstate.NewResourceInstanceBuilder(). | |
| SetAddr(mustAbsResourceInstanceObject(`component.simple_component["comp2"].testing_resource.parent_resource`)). | |
| SetProviderAddr(mustDefaultRootProvider("testing")). | |
| SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ | |
| Status: states.ObjectReady, | |
| AttrsJSON: mustMarshalJSONAttrs(map[string]any{ | |
| "id": "comp2-parent", | |
| "value": "hello from the root of comp2", | |
| }), | |
| })). | |
| AddResourceInstance(stackstate.NewResourceInstanceBuilder(). | |
| SetAddr(mustAbsResourceInstanceObject(`component.simple_component["comp2"].module.child.testing_resource.child_resource`)). | |
| SetProviderAddr(mustDefaultRootProvider("testing")). | |
| SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ | |
| Status: states.ObjectReady, | |
| AttrsJSON: mustMarshalJSONAttrs(map[string]any{ | |
| "id": "comp2-child", | |
| "value": "hello from child module in comp2", | |
| }), | |
| })). | |
| Build() | |
| plan := planForApplyTest(t, ctx, PlanRequest{ | |
| PlanMode: plans.NormalMode, | |
| // Omit policy client as we're not asserting policy results for the plan phase in this test | |
| PolicyClient: nil, | |
| Config: removedCfg, | |
| PrevState: priorState, | |
| ProviderFactories: map[addrs.Provider]providers.Factory{ | |
| addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { | |
| return stacks_testing_provider.NewProviderWithData(t, policyEvaluationResourceStore(t)), nil | |
| }, | |
| }, | |
| DependencyLocks: *lock, | |
| }) | |
| gotPolicyResults := applyAndCollectPolicyResults(t, ctx, ApplyRequest{ | |
| Config: removedCfg, | |
| Plan: plan, | |
| ProviderFactories: map[addrs.Provider]providers.Factory{ | |
| addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { | |
| return stacks_testing_provider.NewProviderWithData(t, policyEvaluationResourceStore(t)), nil | |
| }, | |
| }, | |
| DependencyLocks: *lock, | |
| PolicyClient: policyEvaluationTestClient(t), | |
| }) | |
| wantPolicyResults := map[string]map[string]policy.EvaluationResponse{ | |
| `component.simple_component["comp1"]`: createExpectedComponentInstancePolicyEvaluation("policy-evaluation-removed"), | |
| // Module policies are not evaluated during the destroy apply (only during pre-destroy refresh) | |
| `component.simple_component["comp2"]`: createExpectedComponentInstancePolicyEvaluationForResources("policy-evaluation-removed"), | |
| `provider["registry.terraform.io/hashicorp/testing"].default["comp1"]`: createExpectedProviderInstancePolicyEvaluation("policy-evaluation-removed"), | |
| `provider["registry.terraform.io/hashicorp/testing"].default["comp2"]`: createExpectedProviderInstancePolicyEvaluation("policy-evaluation-removed"), | |
| } | |
| if diff := cmp.Diff(gotPolicyResults, wantPolicyResults, cmp.Comparer(simplePolicyDiagCompare)); diff != "" { | |
| t.Errorf("wrong policy results\n%s", diff) | |
| } | |
| } | |
| func planForApplyTest(t *testing.T, ctx context.Context, req PlanRequest) *stackplan.Plan { | |
| t.Helper() | |
| changesCh := make(chan stackplan.PlannedChange) | |
| diagsCh := make(chan tfdiags.Diagnostic) | |
| resp := PlanResponse{ | |
| PlannedChanges: changesCh, | |
| Diagnostics: diagsCh, | |
| } | |
| go Plan(ctx, &req, &resp) | |
| planChanges, diags := collectPlanOutput(changesCh, diagsCh) | |
| if diags.HasErrors() { | |
| t.Fatalf("unexpected errors during plan\n%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 { | |
| if err := planLoader.AddRaw(rawMsg); err != nil { | |
| t.Fatal(err) | |
| } | |
| } | |
| } | |
| plan, err := planLoader.Plan() | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| return plan | |
| } | |
| func applyAndCollectPolicyResults(t *testing.T, ctx context.Context, req ApplyRequest) map[string]map[string]policy.EvaluationResponse { | |
| t.Helper() | |
| applyChangesCh := make(chan stackstate.AppliedChange) | |
| diagsCh := make(chan tfdiags.Diagnostic) | |
| resp := ApplyResponse{ | |
| AppliedChanges: applyChangesCh, | |
| Diagnostics: diagsCh, | |
| } | |
| var mu sync.Mutex | |
| gotPolicyResults := make(map[string]map[string]policy.EvaluationResponse) | |
| applyHooks := &Hooks{ | |
| ReportComponentInstancePolicyResult: func(ctx context.Context, a any, data *hooks.ComponentInstancePolicyResult) any { | |
| mu.Lock() | |
| defer mu.Unlock() | |
| existingResults, ok := gotPolicyResults[data.ComponentAddr.String()] | |
| if !ok { | |
| gotPolicyResults[data.ComponentAddr.String()] = map[string]policy.EvaluationResponse{ | |
| data.ResourceAddr: data.Result, | |
| } | |
| return a | |
| } | |
| // Merge the two results together | |
| existingResults[data.ResourceAddr] = data.Result | |
| return a | |
| }, | |
| ReportProviderInstancePolicyResult: func(ctx context.Context, data *hooks.ProviderInstancePolicyResults) { | |
| mu.Lock() | |
| defer mu.Unlock() | |
| gotPolicyResults[data.Addr.String()] = map[string]policy.EvaluationResponse{ | |
| data.ProviderAddr: data.Result, | |
| } | |
| }, | |
| } | |
| go Apply(ContextWithHooks(ctx, applyHooks), &req, &resp) | |
| _, diags := collectApplyOutput(applyChangesCh, diagsCh) | |
| if diags.HasErrors() { | |
| t.Fatalf("unexpected errors during apply\n%s", diags.ErrWithWarnings()) | |
| } | |
| return gotPolicyResults | |
| } | |
| 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) | |
| } | |
| } | |
| } |