Merge pull request #27408 from hashicorp/jbardin/destroy-plan-refresh

Refresh during destroy
This commit is contained in:
James Bardin 2021-01-11 14:24:28 -05:00 committed by GitHub
commit d26c149174
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 154 additions and 40 deletions

View File

@ -559,7 +559,7 @@ func TestLocal_planDestroy(t *testing.T) {
b, cleanup := TestLocal(t) b, cleanup := TestLocal(t)
defer cleanup() defer cleanup()
p := TestLocalProvider(t, b, "test", planFixtureSchema()) TestLocalProvider(t, b, "test", planFixtureSchema())
testStateFile(t, b.StatePath, testPlanState()) testStateFile(t, b.StatePath, testPlanState())
outDir := testTempDir(t) outDir := testTempDir(t)
@ -593,10 +593,6 @@ func TestLocal_planDestroy(t *testing.T) {
t.Fatalf("plan operation failed") t.Fatalf("plan operation failed")
} }
if p.ReadResourceCalled {
t.Fatal("ReadResource should not be called")
}
if run.PlanEmpty { if run.PlanEmpty {
t.Fatal("plan should not be empty") t.Fatal("plan should not be empty")
} }
@ -613,7 +609,7 @@ func TestLocal_planDestroy_withDataSources(t *testing.T) {
b, cleanup := TestLocal(t) b, cleanup := TestLocal(t)
defer cleanup() defer cleanup()
p := TestLocalProvider(t, b, "test", planFixtureSchema()) TestLocalProvider(t, b, "test", planFixtureSchema())
testStateFile(t, b.StatePath, testPlanState_withDataSource()) testStateFile(t, b.StatePath, testPlanState_withDataSource())
b.CLI = cli.NewMockUi() b.CLI = cli.NewMockUi()
@ -649,14 +645,6 @@ func TestLocal_planDestroy_withDataSources(t *testing.T) {
t.Fatalf("plan operation failed") t.Fatalf("plan operation failed")
} }
if p.ReadResourceCalled {
t.Fatal("ReadResource should not be called")
}
if p.ReadDataSourceCalled {
t.Fatal("ReadDataSourceCalled should not be called")
}
if run.PlanEmpty { if run.PlanEmpty {
t.Fatal("plan should not be empty") t.Fatal("plan should not be empty")
} }

View File

@ -1,4 +1,5 @@
resource "test_instance" "foo" { resource "test_instance" "foo" {
count = 1
ami = "bar" ami = "bar"
} }

View File

@ -530,6 +530,20 @@ The -target option is not for routine use, and is provided only for exceptional
)) ))
} }
var plan *plans.Plan
var planDiags tfdiags.Diagnostics
switch {
case c.destroy:
plan, planDiags = c.destroyPlan()
default:
plan, planDiags = c.plan()
}
diags = diags.Append(planDiags)
if diags.HasErrors() {
return nil, diags
}
// convert the variables into the format expected for the plan
varVals := make(map[string]plans.DynamicValue, len(c.variables)) varVals := make(map[string]plans.DynamicValue, len(c.variables))
for k, iv := range c.variables { for k, iv := range c.variables {
// We use cty.DynamicPseudoType here so that we'll save both the // We use cty.DynamicPseudoType here so that we'll save both the
@ -547,44 +561,85 @@ The -target option is not for routine use, and is provided only for exceptional
varVals[k] = dv varVals[k] = dv
} }
p := &plans.Plan{ // insert the run-specific data from the context into the plan; variables,
VariableValues: varVals, // targets and provider SHAs.
TargetAddrs: c.targets, plan.VariableValues = varVals
ProviderSHA256s: c.providerSHA256s, plan.TargetAddrs = c.targets
} plan.ProviderSHA256s = c.providerSHA256s
operation := walkPlan return plan, diags
graphType := GraphTypePlan }
if c.destroy {
operation = walkPlanDestroy
graphType = GraphTypePlanDestroy
}
graph, graphDiags := c.Graph(graphType, nil) func (c *Context) plan() (*plans.Plan, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
graph, graphDiags := c.Graph(GraphTypePlan, nil)
diags = diags.Append(graphDiags) diags = diags.Append(graphDiags)
if graphDiags.HasErrors() { if graphDiags.HasErrors() {
return nil, diags return nil, diags
} }
// Do the walk // Do the walk
walker, walkDiags := c.walk(graph, operation) walker, walkDiags := c.walk(graph, walkPlan)
diags = diags.Append(walker.NonFatalDiagnostics) diags = diags.Append(walker.NonFatalDiagnostics)
diags = diags.Append(walkDiags) diags = diags.Append(walkDiags)
if walkDiags.HasErrors() { if walkDiags.HasErrors() {
return nil, diags return nil, diags
} }
p.Changes = c.changes plan := &plans.Plan{
Changes: c.changes,
}
c.refreshState.SyncWrapper().RemovePlannedResourceInstanceObjects() c.refreshState.SyncWrapper().RemovePlannedResourceInstanceObjects()
refreshedState := c.refreshState.DeepCopy() refreshedState := c.refreshState.DeepCopy()
p.State = refreshedState plan.State = refreshedState
// replace the working state with the updated state, so that immediate calls // replace the working state with the updated state, so that immediate calls
// to Apply work as expected. // to Apply work as expected.
c.state = refreshedState c.state = refreshedState
return p, diags return plan, diags
}
func (c *Context) destroyPlan() (*plans.Plan, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
destroyPlan := &plans.Plan{}
c.changes = plans.NewChanges()
// A destroy plan starts by running Refresh to read any pending data
// sources, and remove missing managed resources. This is required because
// a "destroy plan" is only creating delete changes, and is essentially a
// local operation.
if !c.skipRefresh {
refreshPlan, refreshDiags := c.plan()
diags = diags.Append(refreshDiags)
if diags.HasErrors() {
return nil, diags
}
// insert the refreshed state into the destroy plan result, and discard
// the changes recorded from the refresh.
destroyPlan.State = refreshPlan.State
c.changes = plans.NewChanges()
}
graph, graphDiags := c.Graph(GraphTypePlanDestroy, nil)
diags = diags.Append(graphDiags)
if graphDiags.HasErrors() {
return nil, diags
}
// Do the walk
walker, walkDiags := c.walk(graph, walkPlan)
diags = diags.Append(walker.NonFatalDiagnostics)
diags = diags.Append(walkDiags)
if walkDiags.HasErrors() {
return nil, diags
}
destroyPlan.Changes = c.changes
return destroyPlan, diags
} }
// Refresh goes through all the resources in the state and refreshes them // Refresh goes through all the resources in the state and refreshes them

View File

@ -1275,6 +1275,7 @@ func TestContext2Apply_destroyDependsOnStateOnly(t *testing.T) {
} }
func testContext2Apply_destroyDependsOnStateOnly(t *testing.T, state *states.State) { func testContext2Apply_destroyDependsOnStateOnly(t *testing.T, state *states.State) {
state = state.DeepCopy()
m := testModule(t, "empty") m := testModule(t, "empty")
p := testProvider("aws") p := testProvider("aws")
p.ApplyResourceChangeFn = testApplyFn p.ApplyResourceChangeFn = testApplyFn
@ -1371,6 +1372,7 @@ func TestContext2Apply_destroyDependsOnStateOnlyModule(t *testing.T) {
} }
func testContext2Apply_destroyDependsOnStateOnlyModule(t *testing.T, state *states.State) { func testContext2Apply_destroyDependsOnStateOnlyModule(t *testing.T, state *states.State) {
state = state.DeepCopy()
m := testModule(t, "empty") m := testModule(t, "empty")
p := testProvider("aws") p := testProvider("aws")
p.ApplyResourceChangeFn = testApplyFn p.ApplyResourceChangeFn = testApplyFn
@ -1451,6 +1453,12 @@ func TestContext2Apply_destroyData(t *testing.T) {
p := testProvider("null") p := testProvider("null")
p.ApplyResourceChangeFn = testApplyFn p.ApplyResourceChangeFn = testApplyFn
p.PlanResourceChangeFn = testDiffFn p.PlanResourceChangeFn = testDiffFn
p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
return providers.ReadDataSourceResponse{
State: req.Config,
}
}
state := states.NewState() state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance) root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent( root.SetResourceInstanceCurrent(
@ -1493,6 +1501,8 @@ func TestContext2Apply_destroyData(t *testing.T) {
} }
wantHookCalls := []*testHookCall{ wantHookCalls := []*testHookCall{
{"PreDiff", "data.null_data_source.testing"},
{"PostDiff", "data.null_data_source.testing"},
{"PreDiff", "data.null_data_source.testing"}, {"PreDiff", "data.null_data_source.testing"},
{"PostDiff", "data.null_data_source.testing"}, {"PostDiff", "data.null_data_source.testing"},
{"PostStateUpdate", ""}, {"PostStateUpdate", ""},
@ -9561,6 +9571,18 @@ func TestContext2Apply_destroyDataCycle(t *testing.T) {
p := testProvider("null") p := testProvider("null")
p.ApplyResourceChangeFn = testApplyFn p.ApplyResourceChangeFn = testApplyFn
p.PlanResourceChangeFn = testDiffFn p.PlanResourceChangeFn = testDiffFn
p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
return providers.ReadDataSourceResponse{
State: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("new"),
"foo": cty.NullVal(cty.String),
}),
}
}
tp := testProvider("test")
tp.ApplyResourceChangeFn = testApplyFn
tp.PlanResourceChangeFn = testDiffFn
state := states.NewState() state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance) root := state.EnsureModule(addrs.RootModuleInstance)
@ -9579,6 +9601,31 @@ func TestContext2Apply_destroyDataCycle(t *testing.T) {
Module: addrs.RootModule, Module: addrs.RootModule,
}, },
) )
root.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "a",
}.Instance(addrs.IntKey(0)),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"a"}`),
Dependencies: []addrs.ConfigResource{
addrs.ConfigResource{
Resource: addrs.Resource{
Mode: addrs.DataResourceMode,
Type: "null_data_source",
Name: "d",
},
Module: addrs.RootModule,
},
},
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
root.SetResourceInstanceCurrent( root.SetResourceInstanceCurrent(
addrs.Resource{ addrs.Resource{
Mode: addrs.DataResourceMode, Mode: addrs.DataResourceMode,
@ -9587,7 +9634,7 @@ func TestContext2Apply_destroyDataCycle(t *testing.T) {
}.Instance(addrs.NoKey), }.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{ &states.ResourceInstanceObjectSrc{
Status: states.ObjectReady, Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"data"}`), AttrsJSON: []byte(`{"id":"old"}`),
}, },
addrs.AbsProviderConfig{ addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("null"), Provider: addrs.NewDefaultProvider("null"),
@ -9597,15 +9644,14 @@ func TestContext2Apply_destroyDataCycle(t *testing.T) {
Providers := map[addrs.Provider]providers.Factory{ Providers := map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("null"): testProviderFuncFixed(p), addrs.NewDefaultProvider("null"): testProviderFuncFixed(p),
addrs.NewDefaultProvider("test"): testProviderFuncFixed(tp),
} }
hook := &testHook{}
ctx := testContext2(t, &ContextOpts{ ctx := testContext2(t, &ContextOpts{
Config: m, Config: m,
Providers: Providers, Providers: Providers,
State: state, State: state,
Destroy: true, Destroy: true,
Hooks: []Hook{hook},
}) })
plan, diags := ctx.Plan() plan, diags := ctx.Plan()
@ -9627,6 +9673,19 @@ func TestContext2Apply_destroyDataCycle(t *testing.T) {
t.Fatalf("failed to create context for plan: %s", diags.Err()) t.Fatalf("failed to create context for plan: %s", diags.Err())
} }
tp.ConfigureFn = func(req providers.ConfigureRequest) (resp providers.ConfigureResponse) {
foo := req.Config.GetAttr("foo")
if !foo.IsKnown() {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown config value foo"))
return resp
}
if foo.AsString() != "new" {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("wrong config value: %q", foo.AsString()))
}
return resp
}
_, diags = ctx.Apply() _, diags = ctx.Apply()
if diags.HasErrors() { if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err()) t.Fatalf("diags: %s", diags.Err())

View File

@ -12,7 +12,10 @@ import (
// planning a pure-destroy. // planning a pure-destroy.
// //
// Planning a pure destroy operation is simple because we can ignore most // Planning a pure destroy operation is simple because we can ignore most
// ordering configuration and simply reverse the state. // ordering configuration and simply reverse the state. This graph mainly
// exists for targeting, because we need to walk the destroy dependencies to
// ensure we plan the required resources. Without the requirement for
// targeting, the plan could theoretically be created directly from the state.
type DestroyPlanGraphBuilder struct { type DestroyPlanGraphBuilder struct {
// Config is the configuration tree to build the plan from. // Config is the configuration tree to build the plan from.
Config *configs.Config Config *configs.Config
@ -72,6 +75,7 @@ func (b *DestroyPlanGraphBuilder) Steps() []GraphTransformer {
State: b.State, State: b.State,
}, },
// Create the delete changes for root module outputs.
&OutputTransformer{ &OutputTransformer{
Config: b.Config, Config: b.Config,
Destroy: true, Destroy: true,
@ -93,8 +97,6 @@ func (b *DestroyPlanGraphBuilder) Steps() []GraphTransformer {
Schemas: b.Schemas, Schemas: b.Schemas,
}, },
// Target. Note we don't set "Destroy: true" here since we already
// created proper destroy ordering.
&TargetsTransformer{Targets: b.Targets}, &TargetsTransformer{Targets: b.Targets},
// Close opened plugin connections // Close opened plugin connections

View File

@ -1401,6 +1401,13 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, currentSt
return plannedChange, plannedNewState, diags return plannedChange, plannedNewState, diags
} }
// While this isn't a "diff", continue to call this for data sources.
diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) {
return h.PreDiff(n.Addr, states.CurrentGen, priorVal, configVal)
}))
if diags.HasErrors() {
return nil, nil, diags
}
// We have a complete configuration with no dependencies to wait on, so we // We have a complete configuration with no dependencies to wait on, so we
// can read the data source into the state. // can read the data source into the state.
newVal, readDiags := n.readDataSource(ctx, configVal) newVal, readDiags := n.readDataSource(ctx, configVal)

View File

@ -8,3 +8,7 @@ data "null_data_source" "d" {
resource "null_resource" "a" { resource "null_resource" "a" {
count = local.l == "NONE" ? 1 : 0 count = local.l == "NONE" ? 1 : 0
} }
provider "test" {
foo = data.null_data_source.d.id
}

View File

@ -1,5 +1,3 @@
data "null_data_source" "testing" { data "null_data_source" "testing" {
inputs = { foo = "yes"
test = "yes"
}
} }

View File

@ -1,5 +1,5 @@
variable "a_id" {} variable "a_id" {}
resource "aws_instance" "b" { resource "aws_instance" "b" {
command = "echo ${var.a_id}" foo = "echo ${var.a_id}"
} }