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
This commit is contained in:
Paul Hinze 2016-02-23 10:24:53 -06:00
parent 5d9637ab1a
commit f882dd1427
5 changed files with 88 additions and 8 deletions

View File

@ -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

View File

@ -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 = <not created>
Tainted ID 1 = ifailedprovisioners
`)
if actual != expected {
t.Fatalf("expected state: \n%s\ngot: \n%s", expected, actual)
}

View File

@ -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)
}

View File

@ -0,0 +1,3 @@
resource "aws_instance" "ifailedprovisioners" { }
resource "aws_instance" "iambeingadded" { }

View File

@ -0,0 +1,5 @@
resource "aws_instance" "ifailedprovisioners" {
}
resource "aws_instance" "iambeingadded" {
}