core: validate graph w/ diff during plan phase

This reimplements my prior attempt at nipping issues where a plan did
not yield the same cycle an apply did. My prior attempt was to have
ctx.Validate generate a "Verbose" worst-case graph. It turns out that
skipping PruneDestroyTransformer to generate this graph misses important
heuristics that prevent cycles by dropping destroy nodes that are
determined to be unused.

This resulted in Validate improperly failing in scenarios where these
heuristics would have broken the cycle.

We detected the problem during the work on #1781 and worked around the
issue by reverting to the non-Verbose graph in Validate.

This commit accomplishes the original goal in a better way - by
generating the full graph and checking it once Plan has calculated the
diff. This guarantees that any graph issue that would be caught by Apply
will be caught by Plan.
This commit is contained in:
Paul Hinze 2015-05-05 17:24:44 -05:00
parent c8635654cb
commit 0fff7d1673
3 changed files with 18 additions and 12 deletions

View File

@ -331,6 +331,12 @@ func (c *Context) Plan() (*Plan, error) {
} }
p.Diff = c.diff p.Diff = c.diff
// Now that we have a diff, we can build the exact graph that Apply will use
// and catch any possible cycles during the Plan phase.
if _, err := c.Graph(&ContextGraphOpts{Validate: true}); err != nil {
return nil, err
}
return p, nil return p, nil
} }
@ -406,12 +412,11 @@ func (c *Context) Validate() ([]string, []error) {
} }
} }
// Build a Verbose version of the graph so we can catch any potential cycles // Build the graph so we can walk it and run Validate on nodes.
// in the validate stage // We also validate the graph generated here, but this graph doesn't
graph, err := c.Graph(&ContextGraphOpts{ // necessarily match the graph that Plan will generate, so we'll validate the
Validate: true, // graph again later after Planning.
Verbose: false, graph, err := c.Graph(&ContextGraphOpts{Validate: true})
})
if err != nil { if err != nil {
return nil, []error{err} return nil, []error{err}
} }

View File

@ -426,10 +426,13 @@ func (n *graphNodeResourceDestroy) destroyIncludePrimary(
// to include this resource. // to include this resource.
prefix := n.Original.Resource.Id() prefix := n.Original.Resource.Id()
// If we're present in the diff proper, then keep it. // If we're present in the diff proper, then keep it. We're looking
// only for resources in the diff that match our resource or a count-index
// of our resource that are marked for destroy.
if d != nil { if d != nil {
for k, _ := range d.Resources { for k, d := range d.Resources {
if strings.HasPrefix(k, prefix) { match := k == prefix || strings.HasPrefix(k, prefix+".")
if match && d.Destroy {
return true return true
} }
} }

View File

@ -166,7 +166,7 @@ func TestPruneDestroyTransformer_diff(t *testing.T) {
actual := strings.TrimSpace(g.String()) actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformPruneDestroyBasicDiffStr) expected := strings.TrimSpace(testTransformPruneDestroyBasicDiffStr)
if actual != expected { if actual != expected {
t.Fatalf("bad:\n\n%s", actual) t.Fatalf("expected:\n\n%s\n\nbad:\n\n%s", expected, actual)
} }
} }
@ -421,9 +421,7 @@ aws_instance.foo
const testTransformPruneDestroyBasicDiffStr = ` const testTransformPruneDestroyBasicDiffStr = `
aws_instance.bar aws_instance.bar
aws_instance.bar (destroy)
aws_instance.foo aws_instance.foo
aws_instance.bar (destroy)
aws_instance.foo aws_instance.foo
` `