Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

terraform: destroy graph builder based on state #9527

Merged
merged 14 commits into from
Oct 26, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions command/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,26 @@ func (c *ApplyCommand) Run(args []string) int {
}
}

// Check for the new destroy
if terraform.X_newDestroy {
desc := "Experimental new destroy graph has been enabled. This may still\n" +
"have bugs, and should be used with care. If you'd like to continue,\n" +
"you must enter exactly 'yes' as a response."
v, err := c.UIInput().Input(&terraform.InputOpts{
Id: "Xnew-destroy",
Query: "Experimental feature enabled: new destroy graph. Continue?",
Description: desc,
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Error asking for confirmation: %s", err))
return 1
}
if v != "yes" {
c.Ui.Output("Apply cancelled.")
return 1
}
}

// Build the context based on the arguments given
ctx, planned, err := c.Context(contextOpts{
Destroy: c.Destroy,
Expand Down
1 change: 1 addition & 0 deletions command/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ func (m *Meta) flagSet(n string) *flag.FlagSet {

// Experimental features
f.BoolVar(&terraform.X_newApply, "Xnew-apply", false, "experiment: new apply")
f.BoolVar(&terraform.X_newDestroy, "Xnew-destroy", false, "experiment: new destroy")

// Create an io.Writer that writes to our Ui properly for errors.
// This is kind of a hack, but it does the job. Basically: create
Expand Down
2 changes: 1 addition & 1 deletion go.sh
Original file line number Diff line number Diff line change
@@ -1 +1 @@
go test ./terraform | grep -E '(FAIL|panic)' | tee /dev/tty | wc -l
go test ./terraform -Xnew-apply -Xnew-destroy | grep -E '(FAIL|panic)' | tee /dev/tty | wc -l
53 changes: 36 additions & 17 deletions terraform/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ var (
// X_newApply will enable the new apply graph. This will be removed
// and be on by default in 0.8.0.
X_newApply = false

// X_newDestroy will enable the new destroy graph. This will be removed
// and be on by default in 0.8.0.
X_newDestroy = false
)

// InputMode defines what sort of input will be asked for when Input
Expand Down Expand Up @@ -371,6 +375,8 @@ func (c *Context) Apply() (*State, error) {
// Copy our own state
c.state = c.state.DeepCopy()

newGraphEnabled := (c.destroy && X_newDestroy) || (!c.destroy && X_newApply)

// Build the original graph. This is before the new graph builders
// coming in 0.8. We do this for shadow graphing.
oldGraph, err := c.Graph(&ContextGraphOpts{Validate: true})
Expand All @@ -392,12 +398,13 @@ func (c *Context) Apply() (*State, error) {
State: c.state,
Providers: c.components.ResourceProviders(),
Provisioners: c.components.ResourceProvisioners(),
Destroy: c.destroy,
}).Build(RootModulePath)
if err != nil && !X_newApply {
if err != nil && !newGraphEnabled {
// If we had an error graphing but we're not using this graph, just
// set it to nil and record it as a shadow error.
c.shadowErr = multierror.Append(c.shadowErr, fmt.Errorf(
"Error building new apply graph: %s", err))
"Error building new graph: %s", err))

newGraph = nil
err = nil
Expand All @@ -418,16 +425,11 @@ func (c *Context) Apply() (*State, error) {
//
real := oldGraph
shadow := newGraph
if c.destroy {
log.Printf("[WARN] terraform: real graph is original, shadow is nil")
shadow = nil
if newGraphEnabled {
log.Printf("[WARN] terraform: real graph is experiment, shadow is experiment")
real = shadow
} else {
if X_newApply {
log.Printf("[WARN] terraform: real graph is Xnew-apply, shadow is Xnew-apply")
real = shadow
} else {
log.Printf("[WARN] terraform: real graph is original, shadow is Xnew-apply")
}
log.Printf("[WARN] terraform: real graph is original, shadow is experiment")
}

// For now, always shadow with the real graph for verification. We don't
Expand Down Expand Up @@ -505,8 +507,20 @@ func (c *Context) Plan() (*Plan, error) {
c.diff.init()
c.diffLock.Unlock()

// Build the graph
graph, err := c.Graph(&ContextGraphOpts{Validate: true})
// Build the graph. We have a branch here since for the pure-destroy
// plan (c.destroy) we use a much simpler graph builder that simply
// walks the state and reverses edges.
var graph *Graph
var err error
if c.destroy && X_newDestroy {
graph, err = (&DestroyPlanGraphBuilder{
Module: c.module,
State: c.state,
Targets: c.targets,
}).Build(RootModulePath)
} else {
graph, err = c.Graph(&ContextGraphOpts{Validate: true})
}
if err != nil {
return nil, err
}
Expand All @@ -529,11 +543,16 @@ func (c *Context) Plan() (*Plan, error) {
p.Diff.DeepCopy()
}

// 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
// We don't do the reverification during the new destroy plan because
// it will use a different apply process.
if !(c.destroy && X_newDestroy) {
// 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
}
}

var errs error
if len(walker.ValidationErrors) > 0 {
errs = multierror.Append(errs, walker.ValidationErrors...)
Expand Down
2 changes: 1 addition & 1 deletion terraform/context_apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -739,7 +739,7 @@ func getContextForApply_destroyCrossProviders(
},
},
&ModuleState{
Path: []string{"root", "example"},
Path: []string{"root", "child"},
Resources: map[string]*ResourceState{
"aws_vpc.bar": &ResourceState{
Type: "aws_vpc",
Expand Down
6 changes: 3 additions & 3 deletions terraform/context_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1582,7 +1582,7 @@ func TestContext2Plan_moduleDestroy(t *testing.T) {
actual := strings.TrimSpace(plan.String())
expected := strings.TrimSpace(testTerraformPlanModuleDestroyStr)
if actual != expected {
t.Fatalf("bad:\n%s", actual)
t.Fatalf("bad:\n%s\n\nexpected:\n\n%s", actual, expected)
}
}

Expand Down Expand Up @@ -1634,7 +1634,7 @@ func TestContext2Plan_moduleDestroyCycle(t *testing.T) {
actual := strings.TrimSpace(plan.String())
expected := strings.TrimSpace(testTerraformPlanModuleDestroyCycleStr)
if actual != expected {
t.Fatalf("bad:\n%s", actual)
t.Fatalf("bad:\n%s\n\nexpected:\n\n%s", actual, expected)
}
}

Expand Down Expand Up @@ -1684,7 +1684,7 @@ func TestContext2Plan_moduleDestroyMultivar(t *testing.T) {
actual := strings.TrimSpace(plan.String())
expected := strings.TrimSpace(testTerraformPlanModuleDestroyMultivarStr)
if actual != expected {
t.Fatalf("bad:\n%s", actual)
t.Fatalf("bad:\n%s\n\nexpected:\n\n%s", actual, expected)
}
}

Expand Down
18 changes: 13 additions & 5 deletions terraform/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,20 @@ func (d *Diff) Equal(d2 *Diff) bool {
sort.Sort(moduleDiffSort(d.Modules))
sort.Sort(moduleDiffSort(d2.Modules))

// Copy since we have to modify the module destroy flag to false so
// we don't compare that. TODO: delete this when we get rid of the
// destroy flag on modules.
dCopy := d.DeepCopy()
d2Copy := d2.DeepCopy()
for _, m := range dCopy.Modules {
m.Destroy = false
}
for _, m := range d2Copy.Modules {
m.Destroy = false
}

// Use DeepEqual
return reflect.DeepEqual(d, d2)
return reflect.DeepEqual(dCopy, d2Copy)
}

// DeepCopy performs a deep copy of all parts of the Diff, making the
Expand Down Expand Up @@ -238,10 +250,6 @@ func (d *ModuleDiff) IsRoot() bool {
func (d *ModuleDiff) String() string {
var buf bytes.Buffer

if d.Destroy {
buf.WriteString("DESTROY MODULE\n")
}

names := make([]string, 0, len(d.Resources))
for name, _ := range d.Resources {
names = append(names, name)
Expand Down
14 changes: 14 additions & 0 deletions terraform/diff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,20 @@ func TestDiffEqual(t *testing.T) {
},
true,
},

"different module diff destroys": {
&Diff{
Modules: []*ModuleDiff{
&ModuleDiff{Path: []string{"root", "foo"}, Destroy: true},
},
},
&Diff{
Modules: []*ModuleDiff{
&ModuleDiff{Path: []string{"root", "foo"}, Destroy: false},
},
},
true,
},
}

for name, tc := range cases {
Expand Down
4 changes: 4 additions & 0 deletions terraform/graph_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ type BasicGraphBuilder struct {
func (b *BasicGraphBuilder) Build(path []string) (*Graph, error) {
g := &Graph{Path: path}
for _, step := range b.Steps {
if step == nil {
continue
}

if err := step.Transform(g); err != nil {
return g, err
}
Expand Down
17 changes: 14 additions & 3 deletions terraform/graph_builder_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ type ApplyGraphBuilder struct {

// DisableReduce, if true, will not reduce the graph. Great for testing.
DisableReduce bool

// Destroy, if true, represents a pure destroy operation
Destroy bool
}

// See GraphBuilder
Expand Down Expand Up @@ -77,7 +80,10 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer {

// Destruction ordering
&DestroyEdgeTransformer{Module: b.Module, State: b.State},
&CBDEdgeTransformer{Module: b.Module, State: b.State},
GraphTransformIf(
func() bool { return !b.Destroy },
&CBDEdgeTransformer{Module: b.Module, State: b.State},
),

// Create all the providers
&MissingProviderTransformer{Providers: b.Providers, Factory: providerFactory},
Expand All @@ -87,8 +93,13 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer {
&AttachProviderConfigTransformer{Module: b.Module},

// Provisioner-related transformations
&MissingProvisionerTransformer{Provisioners: b.Provisioners},
&ProvisionerTransformer{},
GraphTransformIf(
func() bool { return !b.Destroy },
GraphTransformMulti(
&MissingProvisionerTransformer{Provisioners: b.Provisioners},
&ProvisionerTransformer{},
),
),

// Add root variables
&RootVariableTransformer{Module: b.Module},
Expand Down
58 changes: 58 additions & 0 deletions terraform/graph_builder_destroy_plan.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package terraform

import (
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/dag"
)

// DestroyPlanGraphBuilder implements GraphBuilder and is responsible for
// planning a pure-destroy.
//
// Planning a pure destroy operation is simple because we can ignore most
// ordering configuration and simply reverse the state.
type DestroyPlanGraphBuilder struct {
// Module is the root module for the graph to build.
Module *module.Tree

// State is the current state
State *State

// Targets are resources to target
Targets []string
}

// See GraphBuilder
func (b *DestroyPlanGraphBuilder) Build(path []string) (*Graph, error) {
return (&BasicGraphBuilder{
Steps: b.Steps(),
Validate: true,
}).Build(path)
}

// See GraphBuilder
func (b *DestroyPlanGraphBuilder) Steps() []GraphTransformer {
concreteResource := func(a *NodeAbstractResource) dag.Vertex {
return &NodePlanDestroyableResource{
NodeAbstractResource: a,
}
}

steps := []GraphTransformer{
// Creates all the nodes represented in the state.
&StateTransformer{
Concrete: concreteResource,
State: b.State,
},

// Target
&TargetsTransformer{Targets: b.Targets},

// Attach the configuration to any resources
&AttachResourceConfigTransformer{Module: b.Module},

// Single root
&RootTransformer{},
}

return steps
}
4 changes: 0 additions & 4 deletions terraform/graph_config_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ type graphNodeConfig interface {
// configuration graph need to implement in order to be be addressed / targeted
// properly.
type GraphNodeAddressable interface {
graphNodeConfig

ResourceAddress() *ResourceAddress
}

Expand All @@ -35,7 +33,5 @@ type GraphNodeAddressable interface {
// provided will contain every target provided, and each implementing graph
// node must filter this list to targets considered relevant.
type GraphNodeTargetable interface {
GraphNodeAddressable

SetTargets([]ResourceAddress)
}
2 changes: 1 addition & 1 deletion terraform/graph_config_node_module.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func (n *GraphNodeConfigModule) Expand(b GraphBuilder) (GraphNodeSubgraph, error

{
// Add the destroy marker to the graph
t := &ModuleDestroyTransformer{}
t := &ModuleDestroyTransformerOld{}
if err := t.Transform(graph); err != nil {
return nil, err
}
Expand Down
29 changes: 29 additions & 0 deletions terraform/node_module_destroy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package terraform

import (
"fmt"
)

// NodeDestroyableModule represents a module destruction.
type NodeDestroyableModuleVariable struct {
PathValue []string
}

func (n *NodeDestroyableModuleVariable) Name() string {
result := "plan-destroy"
if len(n.PathValue) > 1 {
result = fmt.Sprintf("%s.%s", modulePrefixStr(n.PathValue), result)
}

return result
}

// GraphNodeSubPath
func (n *NodeDestroyableModuleVariable) Path() []string {
return n.PathValue
}

// GraphNodeEvalable
func (n *NodeDestroyableModuleVariable) EvalTree() EvalNode {
return &EvalDiffDestroyModule{Path: n.PathValue}
}
Loading