diff --git a/terraform/transform_noop.go b/terraform/transform_noop.go new file mode 100644 index 000000000..982f755ce --- /dev/null +++ b/terraform/transform_noop.go @@ -0,0 +1,100 @@ +package terraform + +import ( + "github.com/hashicorp/terraform/dag" +) + +// GraphNodeNoopPrunable can be implemented by nodes that can be +// pruned if they are noops. +type GraphNodeNoopPrunable interface { + Noop(*NoopOpts) bool +} + +// NoopOpts are the options available to determine if your node is a noop. +type NoopOpts struct { + Graph *Graph + Vertex dag.Vertex + Diff *ModuleDiff + State *ModuleState +} + +// PruneNoopTransformer is a graph transform that prunes nodes that +// consider themselves no-ops. This is done to both simplify the graph +// as well as to remove graph nodes that might otherwise cause problems +// during the graph run. Therefore, this transformer isn't completely +// an optimization step, and can instead be considered critical to +// Terraform operations. +// +// Example of the above case: variables for modules interpolate their values. +// Interpolation will fail on destruction (since attributes are being deleted), +// but variables shouldn't even eval if there is nothing that will consume +// the variable. Therefore, variables can note that they can be omitted +// safely in this case. +// +// The PruneNoopTransformer will prune nodes depth first, and will automatically +// create connect through the dependencies of pruned nodes. For example, +// if we have a graph A => B => C (A depends on B, etc.), and B decides to +// be removed, we'll still be left with A => C; the edge will be properly +// connected. +type PruneNoopTransformer struct { + Diff *Diff + State *State +} + +func (t *PruneNoopTransformer) Transform(g *Graph) error { + // Find the leaves. + leaves := make([]dag.Vertex, 0, 10) + for _, v := range g.Vertices() { + if g.DownEdges(v).Len() == 0 { + leaves = append(leaves, v) + } + } + + // Do a depth first walk from the leaves and remove things. + return g.ReverseDepthFirstWalk(leaves, func(v dag.Vertex, depth int) error { + // We need a prunable + pn, ok := v.(GraphNodeNoopPrunable) + if !ok { + return nil + } + + // Start building the noop opts + path := g.Path + if pn, ok := v.(GraphNodeSubPath); ok { + path = pn.Path() + } + + var modDiff *ModuleDiff + var modState *ModuleState + if t.Diff != nil { + modDiff = t.Diff.ModuleByPath(path) + } + if t.State != nil { + modState = t.State.ModuleByPath(path) + } + + // Determine if its a noop. If it isn't, just return + noop := pn.Noop(&NoopOpts{ + Graph: g, + Vertex: v, + Diff: modDiff, + State: modState, + }) + if !noop { + return nil + } + + // It is a noop! We first preserve edges. + up := g.UpEdges(v).List() + for _, downV := range g.DownEdges(v).List() { + for _, upV := range up { + g.Connect(dag.BasicEdge(upV, downV)) + } + } + + // Then remove it + g.Remove(v) + + return nil + }) +} diff --git a/terraform/transform_noop_test.go b/terraform/transform_noop_test.go new file mode 100644 index 000000000..65db95fda --- /dev/null +++ b/terraform/transform_noop_test.go @@ -0,0 +1,54 @@ +package terraform + +import ( + "strings" + "testing" + + "github.com/hashicorp/terraform/dag" +) + +func TestPruneNoopTransformer(t *testing.T) { + g := Graph{Path: RootModulePath} + + a := &testGraphNodeNoop{NameValue: "A"} + b := &testGraphNodeNoop{NameValue: "B", Value: true} + c := &testGraphNodeNoop{NameValue: "C"} + + g.Add(a) + g.Add(b) + g.Add(c) + g.Connect(dag.BasicEdge(a, b)) + g.Connect(dag.BasicEdge(b, c)) + + { + tf := &PruneNoopTransformer{} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformPruneNoopStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +const testTransformPruneNoopStr = ` +A + C +C +` + +type testGraphNodeNoop struct { + NameValue string + Value bool +} + +func (v *testGraphNodeNoop) Name() string { + return v.NameValue +} + +func (v *testGraphNodeNoop) Noop(*NoopOpts) bool { + return v.Value +}