From f882dd14275256c10dccb110f786daa3ef3248cd Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Tue, 23 Feb 2016 10:24:53 -0600 Subject: [PATCH] core: Encode Targets in saved Planfile When a user specifies `-target`s on a `terraform plan` and stores the resulting diff in a plan file using `-out` - it usually works just fine since the diff is scoped based on the targets. When there are tainted resources in the state, however, graph nodes to destroy them were popping back into the plan when it was being loaded from a file. This was because Targets weren't being stored in the Planfile, so Terraform didn't know to filter them out. (In the non-Planfile scenario, we still had the Targets loaded directly from the flags.) By encoding Targets in with the Planfile we can ensure that the same filters are always applied. Backwards compatibility should be fine here, since we're just adding a field. The gob encoder/decoder will just do the right thing (ignore/skip the field) with planfiles stored w/ versions that don't know about Targets. Fixes #5183 --- terraform/context.go | 7 +- terraform/context_apply_test.go | 71 ++++++++++++++++++- terraform/plan.go | 10 +-- .../apply-tainted-targets/main.tf | 3 + .../plan-targeted-with-tainted/main.tf | 5 ++ 5 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 terraform/test-fixtures/apply-tainted-targets/main.tf create mode 100644 terraform/test-fixtures/plan-targeted-with-tainted/main.tf diff --git a/terraform/context.go b/terraform/context.go index d91a85176..a645f29f7 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -316,9 +316,10 @@ func (c *Context) Plan() (*Plan, error) { defer c.releaseRun(v) p := &Plan{ - Module: c.module, - Vars: c.variables, - State: c.state, + Module: c.module, + Vars: c.variables, + State: c.state, + Targets: c.targets, } var operation walkOperation diff --git a/terraform/context_apply_test.go b/terraform/context_apply_test.go index ffb225107..a815f3928 100644 --- a/terraform/context_apply_test.go +++ b/terraform/context_apply_test.go @@ -4010,7 +4010,76 @@ template_file.parent: ID = foo template = Hi type = template_file - `) + `) + if actual != expected { + t.Fatalf("expected state: \n%s\ngot: \n%s", expected, actual) + } +} + +func TestContext2Apply_targetedWithTaintedInState(t *testing.T) { + p := testProvider("aws") + p.DiffFn = testDiffFn + p.ApplyFn = testApplyFn + ctx := testContext2(t, &ContextOpts{ + Module: testModule(t, "apply-tainted-targets"), + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + Targets: []string{"aws_instance.iambeingadded"}, + State: &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.ifailedprovisioners": &ResourceState{ + Tainted: []*InstanceState{ + &InstanceState{ + ID: "ifailedprovisioners", + }, + }, + }, + }, + }, + }, + }, + }) + + plan, err := ctx.Plan() + if err != nil { + t.Fatalf("err: %s", err) + } + + // Write / Read plan to simulate running it through a Plan file + var buf bytes.Buffer + if err := WritePlan(plan, &buf); err != nil { + t.Fatalf("err: %s", err) + } + + planFromFile, err := ReadPlan(&buf) + if err != nil { + t.Fatalf("err: %s", err) + } + + ctx = planFromFile.Context(&ContextOpts{ + Module: testModule(t, "apply-tainted-targets"), + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + state, err := ctx.Apply() + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(state.String()) + expected := strings.TrimSpace(` +aws_instance.iambeingadded: + ID = foo +aws_instance.ifailedprovisioners: (1 tainted) + ID = + Tainted ID 1 = ifailedprovisioners + `) if actual != expected { t.Fatalf("expected state: \n%s\ngot: \n%s", expected, actual) } diff --git a/terraform/plan.go b/terraform/plan.go index 715136edc..b15ea5c59 100644 --- a/terraform/plan.go +++ b/terraform/plan.go @@ -21,10 +21,11 @@ func init() { // Plan represents a single Terraform execution plan, which contains // all the information necessary to make an infrastructure change. type Plan struct { - Diff *Diff - Module *module.Tree - State *State - Vars map[string]string + Diff *Diff + Module *module.Tree + State *State + Vars map[string]string + Targets []string once sync.Once } @@ -38,6 +39,7 @@ func (p *Plan) Context(opts *ContextOpts) *Context { opts.Module = p.Module opts.State = p.State opts.Variables = p.Vars + opts.Targets = p.Targets return NewContext(opts) } diff --git a/terraform/test-fixtures/apply-tainted-targets/main.tf b/terraform/test-fixtures/apply-tainted-targets/main.tf new file mode 100644 index 000000000..8f6b317d5 --- /dev/null +++ b/terraform/test-fixtures/apply-tainted-targets/main.tf @@ -0,0 +1,3 @@ +resource "aws_instance" "ifailedprovisioners" { } + +resource "aws_instance" "iambeingadded" { } diff --git a/terraform/test-fixtures/plan-targeted-with-tainted/main.tf b/terraform/test-fixtures/plan-targeted-with-tainted/main.tf new file mode 100644 index 000000000..f17e08094 --- /dev/null +++ b/terraform/test-fixtures/plan-targeted-with-tainted/main.tf @@ -0,0 +1,5 @@ +resource "aws_instance" "ifailedprovisioners" { +} + +resource "aws_instance" "iambeingadded" { +}