diff --git a/internal/stacks/stackruntime/apply_destroy_test.go b/internal/stacks/stackruntime/apply_destroy_test.go index 3555c665df21..0632bd1ebbd5 100644 --- a/internal/stacks/stackruntime/apply_destroy_test.go +++ b/internal/stacks/stackruntime/apply_destroy_test.go @@ -1121,7 +1121,7 @@ func TestApplyDestroy(t *testing.T) { PlanApplyable: false, PlannedInputValues: make(map[string]plans.DynamicValue), PlannedOutputValues: map[string]cty.Value{ - "value": cty.DynamicVal, + "value": cty.UnknownVal(cty.String), }, PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, @@ -1256,7 +1256,7 @@ func TestApplyDestroy(t *testing.T) { PlanApplyable: true, RequiredComponents: collections.NewSet(mustAbsComponent("component.two")), PlannedOutputValues: map[string]cty.Value{ - "value": cty.DynamicVal, + "value": cty.StringVal("foo"), }, PlanTimestamp: fakePlanTimestamp, }, @@ -1268,7 +1268,7 @@ func TestApplyDestroy(t *testing.T) { PlanApplyable: true, RequiredComponents: collections.NewSet(mustAbsComponent("component.one")), PlannedOutputValues: map[string]cty.Value{ - "value": cty.DynamicVal, + "value": cty.StringVal("foo"), }, PlanTimestamp: fakePlanTimestamp, }, @@ -1319,6 +1319,129 @@ func TestApplyDestroy(t *testing.T) { }, }, }, + "destroy-partial-state-with-module": { + path: "with-module", + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self")). + AddInputVariable("id", cty.StringVal("self")). + AddInputVariable("input", cty.StringVal("self"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.outside")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "self", + "value": "self", + }), + Status: states.ObjectReady, + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("self", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("self"), + "value": cty.StringVal("self"), + })). + Build(), + cycles: []TestCycle{ + { + planMode: plans.DestroyMode, + planInputs: map[string]cty.Value{ + "id": cty.StringVal("self"), + "input": cty.StringVal("self"), + }, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self"), + Action: plans.Delete, + Mode: plans.DestroyMode, + PlanApplyable: true, + PlanComplete: true, + PlannedInputValues: map[string]plans.DynamicValue{ + "create": mustPlanDynamicValueDynamicType(cty.True), + "id": mustPlanDynamicValueDynamicType(cty.StringVal("self")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("self")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "create": nil, + "id": nil, + "input": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: new(states.CheckResults), + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.outside"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.outside"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.outside"), + ProviderAddr: mustDefaultRootProvider("testing"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("self"), + "value": cty.StringVal("self"), + })), + After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }, + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "self", + "value": "self", + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: mustStackInputVariable("id"), + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("self"), + DeleteOnApply: true, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: mustStackInputVariable("input"), + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("self"), + DeleteOnApply: true, + }, + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.outside"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("id"), + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("input"), + }, + }, + }, + }, + }, "destroy-partial-state": { path: "destroy-partial-state", state: stackstate.NewStateBuilder(). @@ -1379,7 +1502,7 @@ func TestApplyDestroy(t *testing.T) { PlanComplete: true, PlannedInputValues: make(map[string]plans.DynamicValue), PlannedOutputValues: map[string]cty.Value{ - "deleted_id": cty.DynamicVal, + "deleted_id": cty.UnknownVal(cty.String), }, PlannedCheckResults: &states.CheckResults{}, PlanTimestamp: fakePlanTimestamp, diff --git a/internal/stacks/stackruntime/internal/stackeval/component_instance.go b/internal/stacks/stackruntime/internal/stackeval/component_instance.go index eb0e29ed049a..cff141573dca 100644 --- a/internal/stacks/stackruntime/internal/stackeval/component_instance.go +++ b/internal/stacks/stackruntime/internal/stackeval/component_instance.go @@ -249,34 +249,28 @@ func (c *ComponentInstance) CheckModuleTreePlan(ctx context.Context) (*plans.Pla return nil, diags } - if !refresh.Complete { - // If the refresh was deferred, then we'll defer the destroy - // plan as well. - opts.ExternalDependencyDeferred = true - } else { - // If we're destroying this instance, then the dependencies - // should be reversed. Unfortunately, we can't compute that - // easily so instead we'll use the dependents computed at the - // last apply operation. - Dependents: - for depAddr := range c.PlanPrevDependents(ctx).All() { - depStack := c.main.Stack(ctx, depAddr.Stack, PlanPhase) - if depStack == nil { - // something weird has happened, but this means that - // whatever thing we're depending on being deleted first - // doesn't exist so it's fine. - continue - } - depComponent, depRemoveds := depStack.ApplyableComponents(ctx, depAddr.Item) - if depComponent != nil && !depComponent.PlanIsComplete(ctx) { + // If we're destroying this instance, then the dependencies + // should be reversed. Unfortunately, we can't compute that + // easily so instead we'll use the dependents computed at the + // last apply operation. + Dependents: + for depAddr := range c.PlanPrevDependents(ctx).All() { + depStack := c.main.Stack(ctx, depAddr.Stack, PlanPhase) + if depStack == nil { + // something weird has happened, but this means that + // whatever thing we're depending on being deleted first + // doesn't exist so it's fine. + continue + } + depComponent, depRemoveds := depStack.ApplyableComponents(ctx, depAddr.Item) + if depComponent != nil && !depComponent.PlanIsComplete(ctx) { + opts.ExternalDependencyDeferred = true + break + } + for _, depRemoved := range depRemoveds { + if !depRemoved.PlanIsComplete(ctx) { opts.ExternalDependencyDeferred = true - break - } - for _, depRemoved := range depRemoveds { - if !depRemoved.PlanIsComplete(ctx) { - opts.ExternalDependencyDeferred = true - break Dependents - } + break Dependents } } } diff --git a/internal/stacks/stackruntime/internal/stackeval/refresh_instance.go b/internal/stacks/stackruntime/internal/stackeval/refresh_instance.go index a8cfbc648b6d..17e04fcd32ec 100644 --- a/internal/stacks/stackruntime/internal/stackeval/refresh_instance.go +++ b/internal/stacks/stackruntime/internal/stackeval/refresh_instance.go @@ -69,7 +69,7 @@ func (r *RefreshInstance) Result(ctx context.Context) map[string]cty.Value { func (r *RefreshInstance) Plan(ctx context.Context) (*plans.Plan, tfdiags.Diagnostics) { return doOnceWithDiags(ctx, &r.moduleTreePlan, r, func(ctx context.Context) (*plans.Plan, tfdiags.Diagnostics) { - opts, diags := r.component.PlanOpts(ctx, plans.RefreshOnlyMode, false) + opts, diags := r.component.PlanOpts(ctx, plans.NormalMode, false) if opts == nil { return nil, diags } diff --git a/internal/stacks/stackruntime/plan_test.go b/internal/stacks/stackruntime/plan_test.go index b732c809a229..63230d813214 100644 --- a/internal/stacks/stackruntime/plan_test.go +++ b/internal/stacks/stackruntime/plan_test.go @@ -222,7 +222,7 @@ func TestPlan(t *testing.T) { Action: plans.Delete, Mode: plans.DestroyMode, PlannedOutputValues: map[string]cty.Value{ - "id": cty.NullVal(cty.DynamicPseudoType), + "id": cty.StringVal("foo"), }, PlanTimestamp: fakePlanTimestamp, }, @@ -1186,10 +1186,10 @@ func TestPlanWithEphemeralInputVariables(t *testing.T) { t.Fatal(err) } req := PlanRequest{ - Config: cfg, InputValues: map[stackaddrs.InputVariable]stackeval.ExternalInputValue{ // Intentionally not set for this subtest. }, + Config: cfg, ForcePlanTimestamp: &fakePlanTimestamp, } resp := PlanResponse{ diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-module/module/module.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/with-module/module/module.tf new file mode 100644 index 000000000000..4da49727a5b0 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-module/module/module.tf @@ -0,0 +1,23 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "id" { + type = string + default = null + nullable = true # We'll generate an ID if none provided. +} + +variable "input" { + type = string +} + +resource "testing_resource" "data" { + id = var.id + value = var.input +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-module/with-module.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/with-module/with-module.tf new file mode 100644 index 000000000000..0bbca09ac7bd --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-module/with-module.tf @@ -0,0 +1,44 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "create" { + type = bool + default = true +} + +variable "id" { + type = string + default = null + nullable = true # We'll generate an ID if none provided. +} + +variable "input" { + type = string +} + +resource "testing_resource" "resource" { + count = var.create ? 1 : 0 +} + + +module "module" { + source = "./module" + + providers = { + testing = testing + } + + id = testing_resource.resource[0].id + input = var.input +} + +resource "testing_resource" "outside" { + id = var.id + value = var.input +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-module/with-module.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-module/with-module.tfstack.hcl new file mode 100644 index 000000000000..7d1db6d2abac --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-module/with-module.tfstack.hcl @@ -0,0 +1,30 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "input" { + type = string +} + +variable "id" { + type = string + default = null +} + +component "self" { + source = "./" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = var.id + input = var.input + } +}