stacks: support forgetting components

pull/35713/head
Daniel Schmidt 2 years ago
parent 0ae6bc34c4
commit ceb52e65bb
No known key found for this signature in database
GPG Key ID: 377C3A4D62FBBBE2

@ -75,6 +75,8 @@ func NewAction(action plans.Action) Action {
return Action_DELETE_THEN_CREATE
case plans.CreateThenDelete:
return Action_CREATE_THEN_DELETE
case plans.Forget:
return Action_FORGET
default:
// The above should be exhaustive for all possible actions
panic(fmt.Sprintf("unsupported change action %s", action))
@ -97,6 +99,8 @@ func FromAction(protoAction Action) (plans.Action, error) {
return plans.DeleteThenCreate, nil
case Action_CREATE_THEN_DELETE:
return plans.CreateThenDelete, nil
case Action_FORGET:
return plans.Forget, nil
default:
return plans.NoOp, fmt.Errorf("unsupported action %s", protoAction)
}

@ -3545,6 +3545,119 @@ func TestApply_RemovedBlocks(t *testing.T) {
},
wantApplyDiags: []expectedDiagnostic{},
},
"forgotten component": {
source: filepath.Join("with-single-input", "forgotten-component"),
initialState: 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(),
inputs: map[string]cty.Value{
"destroy": cty.BoolVal(false),
},
wantPlanChanges: []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,
},
},
wantPlanDiags: []expectedDiagnostic{
{
severity: tfdiags.Warning,
summary: "Some objects will no longer be managed by Terraform",
detail: `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.`,
},
},
wantApplyChanges: []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("removed"),
mustInputVariable("input"): cty.StringVal("removed"),
},
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
NewStateSrc: nil,
Schema: nil,
},
},
wantApplyDiags: []expectedDiagnostic{},
},
}
for name, tc := range tcs {

@ -136,12 +136,14 @@ func (r *RemovedInstance) ModuleTreePlan(ctx context.Context) (*plans.Plan, tfdi
}
plantimestamp := r.main.PlanTimestamp()
forget := !r.call.Config(ctx).config.Destroy
opts := &terraform.PlanOpts{
Mode: plans.DestroyMode,
SetVariables: r.PlanPrevInputs(ctx),
ExternalProviders: providerClients,
DeferralAllowed: true,
ExternalDependencyDeferred: deferred,
Forget: forget,
// We want the same plantimestamp between all components and the stacks language
ForcePlanTimestamp: &plantimestamp,
@ -256,7 +258,13 @@ func (r *RemovedInstance) PlanChanges(ctx context.Context) ([]stackplan.PlannedC
var changes []stackplan.PlannedChange
if plan != nil {
changes, moreDiags = stackplan.FromPlan(ctx, r.ModuleTree(ctx), plan, plans.Delete, r)
var action plans.Action
if r.call.Config(ctx).config.Destroy {
action = plans.Delete
} else {
action = plans.Forget
}
changes, moreDiags = stackplan.FromPlan(ctx, r.ModuleTree(ctx), plan, action, r)
diags = diags.Append(moreDiags)
}
return changes, diags

@ -0,0 +1,22 @@
required_providers {
testing = {
source = "hashicorp/testing"
version = "0.1.0"
}
}
provider "testing" "default" {}
removed {
from = component.self
source = "../"
lifecycle {
destroy = false
}
providers = {
testing = provider.testing.default
}
}

@ -2814,6 +2814,68 @@ removed {
checkStateString(t, state, `<no state>`)
}
// TestContext2Apply_destroy_and_forget tests that a destroy plan with the forget flag set to true.
// The expectation is that all resources should be forgotten and not destroyed.
func TestContext2Apply_destroy_and_forget(t *testing.T) {
addrA := mustResourceInstanceAddr("test_object.a")
addrB := mustResourceInstanceAddr("test_object.b")
m := testModuleInline(t, map[string]string{
"main.tf": `
resource "test_object" "a" {
test_string = "foo"
}
resource "test_object" "b" {
test_string = "foo"
}
`})
state := states.BuildState(func(s *states.SyncState) {
s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{
AttrsJSON: []byte(`{"foo":"bar"}`),
Status: states.ObjectReady,
}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`))
s.SetResourceInstanceCurrent(addrB, &states.ResourceInstanceObjectSrc{
AttrsJSON: []byte(`{"foo":"bar"}`),
Status: states.ObjectReady,
}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`))
})
p := simpleMockProvider()
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
Forget: true,
})
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
// We expect all actions to be forget
for i, change := range plan.Changes.Resources {
if change.Action != plans.Forget {
t.Fatalf("Expected all actions to be forget, but got %s at plan.Changes.Resources[%d]", change.Action, i)
}
}
state, diags = ctx.Apply(plan, m, nil)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
// check that the provider was not asked to destroy the resource
if p.ApplyResourceChangeCalled {
t.Fatalf("Expected ApplyResourceChange not to be called, but it was called")
}
checkStateString(t, state, `<no state>`)
}
func TestContext2Apply_sensitiveInputVariableValue(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `

@ -131,6 +131,10 @@ type PlanOpts struct {
// This is here only to allow producing fixed results for tests. Don't
// use it for main code.
ForcePlanTimestamp *time.Time
// Forget if set to true will cause the plan to forget all resources. This is
// only allowd in the context of a destroy plan.
Forget bool
}
// Plan generates an execution plan by comparing the given configuration
@ -230,6 +234,14 @@ func (c *Context) PlanAndEval(config *configs.Config, prevRunState *states.State
))
return nil, nil, diags
}
if opts.Forget && opts.Mode != plans.DestroyMode {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unsupported plan mode",
"Forgetting all resources is only allowed in the context of a destroy plan. This is a bug in Terraform, please report it.",
))
return nil, nil, diags
}
// By the time we get here, we should have values defined for all of
// the root module variables, even if some of them are "unknown". It's the
@ -711,6 +723,7 @@ func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, o
Overrides: opts.Overrides,
PlanTimeTimestamp: timestamp,
ProviderFuncResults: providerFuncResults,
Forget: opts.Forget,
})
diags = diags.Append(walker.NonFatalDiagnostics)
diags = diags.Append(walkDiags)

@ -73,6 +73,10 @@ type graphWalkOpts struct {
MoveResults refactoring.MoveResults
ProviderFuncResults *providers.FunctionResults
// Forget if set to true will cause the plan to forget all resources. This is
// only allowd in the context of a destroy plan.
Forget bool
}
func (c *Context) walk(graph *Graph, operation walkOperation, opts *graphWalkOpts) (*ContextGraphWalker, tfdiags.Diagnostics) {
@ -191,5 +195,6 @@ func (c *Context) graphWalker(graph *Graph, operation walkOperation, opts *graph
StopContext: c.runContext,
PlanTimestamp: opts.PlanTimeTimestamp,
providerFuncResults: opts.ProviderFuncResults,
Forget: opts.Forget,
}
}

@ -199,6 +199,10 @@ type EvalContext interface {
// withScope derives a new EvalContext that has all of the same global
// context, but a new evaluation scope.
withScope(scope evalContextScope) EvalContext
// Forget if set to true will cause the plan to forget all resources. This is
// only allowed in the context of a destroy plan.
Forget() bool
}
func evalContextForModuleInstance(baseCtx EvalContext, addr addrs.ModuleInstance) EvalContext {

@ -68,6 +68,10 @@ type BuiltinEvalContext struct {
// DeferralsValue is the object returned by [BuiltinEvalContext.Deferrals].
DeferralsValue *deferring.Deferred
// forget if set to true will cause the plan to forget all resources. This is
// only allowd in the context of a destroy plan.
forget bool
Hooks []Hook
InputValue UIInput
ProviderCache map[string]providers.Interface
@ -598,3 +602,7 @@ func (ctx *BuiltinEvalContext) MoveResults() refactoring.MoveResults {
func (ctx *BuiltinEvalContext) Overrides() *mocking.Overrides {
return ctx.OverrideValues
}
func (ctx *BuiltinEvalContext) Forget() bool {
return ctx.forget
}

@ -152,6 +152,9 @@ type MockEvalContext struct {
OverridesCalled bool
OverrideValues *mocking.Overrides
ForgetCalled bool
ForgetValues bool
}
// MockEvalContext implements EvalContext
@ -402,3 +405,8 @@ func (c *MockEvalContext) Overrides() *mocking.Overrides {
c.OverridesCalled = true
return c.OverrideValues
}
func (c *MockEvalContext) Forget() bool {
c.ForgetCalled = true
return c.ForgetValues
}

@ -48,6 +48,9 @@ type ContextGraphWalker struct {
Config *configs.Config
PlanTimestamp time.Time
Overrides *mocking.Overrides
// Forget if set to true will cause the plan to forget all resources. This is
// only allowd in the context of a destroy plan.
Forget bool
// This is an output. Do not set this, nor read it while a graph walk
// is in progress.
@ -131,6 +134,7 @@ func (w *ContextGraphWalker) EvalContext() EvalContext {
PrevRunStateValue: w.PrevRunState,
Evaluator: evaluator,
OverrideValues: w.Overrides,
forget: w.Forget,
}
return ctx

@ -398,6 +398,13 @@ func (n *NodeAbstractResourceInstance) planDestroy(ctx EvalContext, currentState
return noop, deferred, nil
}
// If we are in a context where we forget instead of destroying, we can
// just return the forget change without consulting the provider.
if ctx.Forget() {
forget, diags := n.planForget(ctx, currentState, deposedKey)
return forget, deferred, diags
}
unmarkedPriorVal, _ := currentState.Value.UnmarkDeep()
// The config and new value are null to signify that this is a destroy

Loading…
Cancel
Save