diff --git a/Makefile b/Makefile index 7ea72d97f..fdb9ab3d6 100644 --- a/Makefile +++ b/Makefile @@ -40,6 +40,14 @@ updatedeps: | sort -u \ | xargs go get -f -u -v +cover: + @go tool cover 2>/dev/null; if [ $$? -eq 3 ]; then \ + go get -u golang.org/x/tools/cmd/cover; \ + fi + go test $(TEST) -coverprofile=coverage.out + go tool cover -html=coverage.out + rm coverage.out + # vet runs the Go source code static analysis tool `vet` to find # any common errors. vet: diff --git a/command/graph.go b/command/graph.go index f9011943d..834d6c865 100644 --- a/command/graph.go +++ b/command/graph.go @@ -58,11 +58,7 @@ func (c *GraphCommand) Run(args []string) int { return 1 } - opts := &terraform.GraphDotOpts{ - ModuleDepth: moduleDepth, - } - - c.Ui.Output(terraform.GraphDot(g, opts)) + c.Ui.Output(terraform.GraphDot(g, nil)) return 0 } diff --git a/command/graph_test.go b/command/graph_test.go index 29f3f5874..0139142c0 100644 --- a/command/graph_test.go +++ b/command/graph_test.go @@ -26,7 +26,7 @@ func TestGraph(t *testing.T) { } output := ui.OutputWriter.String() - if !strings.Contains(output, "digraph {") { + if !strings.Contains(output, "provider.test") { t.Fatalf("doesn't look like digraph: %s", output) } } @@ -73,7 +73,7 @@ func TestGraph_noArgs(t *testing.T) { } output := ui.OutputWriter.String() - if !strings.Contains(output, "digraph {") { + if !strings.Contains(output, "provider.test") { t.Fatalf("doesn't look like digraph: %s", output) } } @@ -99,7 +99,7 @@ func TestGraph_plan(t *testing.T) { } output := ui.OutputWriter.String() - if !strings.Contains(output, "digraph {") { + if !strings.Contains(output, "provider.test") { t.Fatalf("doesn't look like digraph: %s", output) } } diff --git a/config/raw_config.go b/config/raw_config.go index 753e7f1b8..f8fcc2ca7 100644 --- a/config/raw_config.go +++ b/config/raw_config.go @@ -92,6 +92,51 @@ func (r *RawConfig) Interpolate(vs map[string]ast.Variable) error { }) } +// Merge merges another RawConfig into this one (overriding any conflicting +// values in this config) and returns a new config. The original config +// is not modified. +func (r *RawConfig) Merge(other *RawConfig) *RawConfig { + // Merge the raw configurations + raw := make(map[string]interface{}) + for k, v := range r.Raw { + raw[k] = v + } + for k, v := range other.Raw { + raw[k] = v + } + + // Create the result + result, err := NewRawConfig(raw) + if err != nil { + panic(err) + } + + // Merge the interpolated results + result.config = make(map[string]interface{}) + for k, v := range r.config { + result.config[k] = v + } + for k, v := range other.config { + result.config[k] = v + } + + // Build the unknown keys + unknownKeys := make(map[string]struct{}) + for _, k := range r.unknownKeys { + unknownKeys[k] = struct{}{} + } + for _, k := range other.unknownKeys { + unknownKeys[k] = struct{}{} + } + + result.unknownKeys = make([]string, 0, len(unknownKeys)) + for k, _ := range unknownKeys { + result.unknownKeys = append(result.unknownKeys, k) + } + + return result +} + func (r *RawConfig) init() error { r.config = r.Raw r.Interpolations = nil diff --git a/config/raw_config_test.go b/config/raw_config_test.go index 49e13a45a..324f98ee6 100644 --- a/config/raw_config_test.go +++ b/config/raw_config_test.go @@ -114,6 +114,87 @@ func TestRawConfig_double(t *testing.T) { } } +func TestRawConfig_merge(t *testing.T) { + raw1 := map[string]interface{}{ + "foo": "${var.foo}", + "bar": "${var.bar}", + } + + rc1, err := NewRawConfig(raw1) + if err != nil { + t.Fatalf("err: %s", err) + } + + { + vars := map[string]ast.Variable{ + "var.foo": ast.Variable{ + Value: "foovalue", + Type: ast.TypeString, + }, + "var.bar": ast.Variable{ + Value: "nope", + Type: ast.TypeString, + }, + } + if err := rc1.Interpolate(vars); err != nil { + t.Fatalf("err: %s", err) + } + } + + raw2 := map[string]interface{}{ + "bar": "${var.bar}", + "baz": "${var.baz}", + } + + rc2, err := NewRawConfig(raw2) + if err != nil { + t.Fatalf("err: %s", err) + } + + { + vars := map[string]ast.Variable{ + "var.bar": ast.Variable{ + Value: "barvalue", + Type: ast.TypeString, + }, + "var.baz": ast.Variable{ + Value: UnknownVariableValue, + Type: ast.TypeString, + }, + } + if err := rc2.Interpolate(vars); err != nil { + t.Fatalf("err: %s", err) + } + } + + // Merge the two + rc3 := rc1.Merge(rc2) + + // Raw should be merged + raw3 := map[string]interface{}{ + "foo": "${var.foo}", + "bar": "${var.bar}", + "baz": "${var.baz}", + } + if !reflect.DeepEqual(rc3.Raw, raw3) { + t.Fatalf("bad: %#v", rc3.Raw) + } + + actual := rc3.Config() + expected := map[string]interface{}{ + "foo": "foovalue", + "bar": "barvalue", + } + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) + } + + expectedKeys := []string{"baz"} + if !reflect.DeepEqual(rc3.UnknownKeys(), expectedKeys) { + t.Fatalf("bad: %#v", rc3.UnknownKeys()) + } +} + func TestRawConfig_syntax(t *testing.T) { raw := map[string]interface{}{ "foo": "${var", diff --git a/dag/dag.go b/dag/dag.go new file mode 100644 index 000000000..9490cf98a --- /dev/null +++ b/dag/dag.go @@ -0,0 +1,163 @@ +package dag + +import ( + "fmt" + "strings" + "sync" + + "github.com/hashicorp/go-multierror" +) + +// AcyclicGraph is a specialization of Graph that cannot have cycles. With +// this property, we get the property of sane graph traversal. +type AcyclicGraph struct { + Graph +} + +// WalkFunc is the callback used for walking the graph. +type WalkFunc func(Vertex) error + +// Root returns the root of the DAG, or an error. +// +// Complexity: O(V) +func (g *AcyclicGraph) Root() (Vertex, error) { + roots := make([]Vertex, 0, 1) + for _, v := range g.Vertices() { + if g.UpEdges(v).Len() == 0 { + roots = append(roots, v) + } + } + + if len(roots) > 1 { + // TODO(mitchellh): make this error message a lot better + return nil, fmt.Errorf("multiple roots: %#v", roots) + } + + if len(roots) == 0 { + return nil, fmt.Errorf("no roots found") + } + + return roots[0], nil +} + +// Validate validates the DAG. A DAG is valid if it has a single root +// with no cycles. +func (g *AcyclicGraph) Validate() error { + if _, err := g.Root(); err != nil { + return err + } + + // Look for cycles of more than 1 component + var err error + var cycles [][]Vertex + for _, cycle := range StronglyConnected(&g.Graph) { + if len(cycle) > 1 { + cycles = append(cycles, cycle) + } + } + if len(cycles) > 0 { + for _, cycle := range cycles { + cycleStr := make([]string, len(cycle)) + for j, vertex := range cycle { + cycleStr[j] = VertexName(vertex) + } + + err = multierror.Append(err, fmt.Errorf( + "Cycle: %s", strings.Join(cycleStr, ", "))) + } + } + + // Look for cycles to self + for _, e := range g.Edges() { + if e.Source() == e.Target() { + err = multierror.Append(err, fmt.Errorf( + "Self reference: %s", VertexName(e.Source()))) + } + } + + return err +} + +// Walk walks the graph, calling your callback as each node is visited. +// This will walk nodes in parallel if it can. Because the walk is done +// in parallel, the error returned will be a multierror. +func (g *AcyclicGraph) Walk(cb WalkFunc) error { + // Cache the vertices since we use it multiple times + vertices := g.Vertices() + + // Build the waitgroup that signals when we're done + var wg sync.WaitGroup + wg.Add(len(vertices)) + doneCh := make(chan struct{}) + go func() { + defer close(doneCh) + wg.Wait() + }() + + // The map of channels to watch to wait for vertices to finish + vertMap := make(map[Vertex]chan struct{}) + for _, v := range vertices { + vertMap[v] = make(chan struct{}) + } + + // The map of whether a vertex errored or not during the walk + var errLock sync.Mutex + var errs error + errMap := make(map[Vertex]bool) + for _, v := range vertices { + // Build our list of dependencies and the list of channels to + // wait on until we start executing for this vertex. + depsRaw := g.DownEdges(v).List() + deps := make([]Vertex, len(depsRaw)) + depChs := make([]<-chan struct{}, len(deps)) + for i, raw := range depsRaw { + deps[i] = raw.(Vertex) + depChs[i] = vertMap[deps[i]] + } + + // Get our channel so that we can close it when we're done + ourCh := vertMap[v] + + // Start the goroutine to wait for our dependencies + readyCh := make(chan bool) + go func(deps []Vertex, chs []<-chan struct{}, readyCh chan<- bool) { + // First wait for all the dependencies + for _, ch := range chs { + <-ch + } + + // Then, check the map to see if any of our dependencies failed + errLock.Lock() + defer errLock.Unlock() + for _, dep := range deps { + if errMap[dep] { + readyCh <- false + return + } + } + + readyCh <- true + }(deps, depChs, readyCh) + + // Start the goroutine that executes + go func(v Vertex, doneCh chan<- struct{}, readyCh <-chan bool) { + defer close(doneCh) + defer wg.Done() + + var err error + if ready := <-readyCh; ready { + err = cb(v) + } + + errLock.Lock() + defer errLock.Unlock() + if err != nil { + errMap[v] = true + errs = multierror.Append(errs, err) + } + }(v, ourCh, readyCh) + } + + <-doneCh + return errs +} diff --git a/dag/dag_test.go b/dag/dag_test.go new file mode 100644 index 000000000..cc9442be5 --- /dev/null +++ b/dag/dag_test.go @@ -0,0 +1,158 @@ +package dag + +import ( + "fmt" + "reflect" + "sync" + "testing" +) + +func TestAcyclicGraphRoot(t *testing.T) { + var g AcyclicGraph + g.Add(1) + g.Add(2) + g.Add(3) + g.Connect(BasicEdge(3, 2)) + g.Connect(BasicEdge(3, 1)) + + if root, err := g.Root(); err != nil { + t.Fatalf("err: %s", err) + } else if root != 3 { + t.Fatalf("bad: %#v", root) + } +} + +func TestAcyclicGraphRoot_cycle(t *testing.T) { + var g AcyclicGraph + g.Add(1) + g.Add(2) + g.Add(3) + g.Connect(BasicEdge(1, 2)) + g.Connect(BasicEdge(2, 3)) + g.Connect(BasicEdge(3, 1)) + + if _, err := g.Root(); err == nil { + t.Fatal("should error") + } +} + +func TestAcyclicGraphRoot_multiple(t *testing.T) { + var g AcyclicGraph + g.Add(1) + g.Add(2) + g.Add(3) + g.Connect(BasicEdge(3, 2)) + + if _, err := g.Root(); err == nil { + t.Fatal("should error") + } +} + +func TestAcyclicGraphValidate(t *testing.T) { + var g AcyclicGraph + g.Add(1) + g.Add(2) + g.Add(3) + g.Connect(BasicEdge(3, 2)) + g.Connect(BasicEdge(3, 1)) + + if err := g.Validate(); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestAcyclicGraphValidate_cycle(t *testing.T) { + var g AcyclicGraph + g.Add(1) + g.Add(2) + g.Add(3) + g.Connect(BasicEdge(3, 2)) + g.Connect(BasicEdge(3, 1)) + g.Connect(BasicEdge(1, 2)) + g.Connect(BasicEdge(2, 1)) + + if err := g.Validate(); err == nil { + t.Fatal("should error") + } +} + +func TestAcyclicGraphValidate_cycleSelf(t *testing.T) { + var g AcyclicGraph + g.Add(1) + g.Add(2) + g.Connect(BasicEdge(1, 1)) + + if err := g.Validate(); err == nil { + t.Fatal("should error") + } +} + +func TestAcyclicGraphWalk(t *testing.T) { + var g AcyclicGraph + g.Add(1) + g.Add(2) + g.Add(3) + g.Connect(BasicEdge(3, 2)) + g.Connect(BasicEdge(3, 1)) + + var visits []Vertex + var lock sync.Mutex + err := g.Walk(func(v Vertex) error { + lock.Lock() + defer lock.Unlock() + visits = append(visits, v) + return nil + }) + if err != nil { + t.Fatalf("err: %s", err) + } + + expected := [][]Vertex{ + {1, 2, 3}, + {2, 1, 3}, + } + for _, e := range expected { + if reflect.DeepEqual(visits, e) { + return + } + } + + t.Fatalf("bad: %#v", visits) +} + +func TestAcyclicGraphWalk_error(t *testing.T) { + var g AcyclicGraph + g.Add(1) + g.Add(2) + g.Add(3) + g.Connect(BasicEdge(3, 2)) + g.Connect(BasicEdge(3, 1)) + + var visits []Vertex + var lock sync.Mutex + err := g.Walk(func(v Vertex) error { + lock.Lock() + defer lock.Unlock() + + if v == 2 { + return fmt.Errorf("error") + } + + visits = append(visits, v) + return nil + }) + if err == nil { + t.Fatal("should error") + } + + expected := [][]Vertex{ + {1}, + } + for _, e := range expected { + if reflect.DeepEqual(visits, e) { + return + } + } + + t.Fatalf("bad: %#v", visits) +} diff --git a/dag/edge.go b/dag/edge.go new file mode 100644 index 000000000..f0d99ee3a --- /dev/null +++ b/dag/edge.go @@ -0,0 +1,37 @@ +package dag + +import ( + "fmt" +) + +// Edge represents an edge in the graph, with a source and target vertex. +type Edge interface { + Source() Vertex + Target() Vertex + + Hashable +} + +// BasicEdge returns an Edge implementation that simply tracks the source +// and target given as-is. +func BasicEdge(source, target Vertex) Edge { + return &basicEdge{S: source, T: target} +} + +// basicEdge is a basic implementation of Edge that has the source and +// target vertex. +type basicEdge struct { + S, T Vertex +} + +func (e *basicEdge) Hashcode() interface{} { + return fmt.Sprintf("%p-%p", e.S, e.T) +} + +func (e *basicEdge) Source() Vertex { + return e.S +} + +func (e *basicEdge) Target() Vertex { + return e.T +} diff --git a/dag/edge_test.go b/dag/edge_test.go new file mode 100644 index 000000000..67d2cbed4 --- /dev/null +++ b/dag/edge_test.go @@ -0,0 +1,26 @@ +package dag + +import ( + "testing" +) + +func TestBasicEdgeHashcode(t *testing.T) { + e1 := BasicEdge(1, 2) + e2 := BasicEdge(1, 2) + if e1.Hashcode() != e2.Hashcode() { + t.Fatalf("bad") + } +} + +func TestBasicEdgeHashcode_pointer(t *testing.T) { + type test struct { + Value string + } + + v1, v2 := &test{"foo"}, &test{"bar"} + e1 := BasicEdge(v1, v2) + e2 := BasicEdge(v1, v2) + if e1.Hashcode() != e2.Hashcode() { + t.Fatalf("bad") + } +} diff --git a/dag/graph.go b/dag/graph.go new file mode 100644 index 000000000..7fac2a534 --- /dev/null +++ b/dag/graph.go @@ -0,0 +1,219 @@ +package dag + +import ( + "bytes" + "fmt" + "sort" + "sync" +) + +// Graph is used to represent a dependency graph. +type Graph struct { + vertices *Set + edges *Set + downEdges map[Vertex]*Set + upEdges map[Vertex]*Set + once sync.Once +} + +// Vertex of the graph. +type Vertex interface{} + +// NamedVertex is an optional interface that can be implemented by Vertex +// to give it a human-friendly name that is used for outputting the graph. +type NamedVertex interface { + Vertex + Name() string +} + +// Vertices returns the list of all the vertices in the graph. +func (g *Graph) Vertices() []Vertex { + list := g.vertices.List() + result := make([]Vertex, len(list)) + for i, v := range list { + result[i] = v.(Vertex) + } + + return result +} + +// Edges returns the list of all the edges in the graph. +func (g *Graph) Edges() []Edge { + list := g.edges.List() + result := make([]Edge, len(list)) + for i, v := range list { + result[i] = v.(Edge) + } + + return result +} + +// Add adds a vertex to the graph. This is safe to call multiple time with +// the same Vertex. +func (g *Graph) Add(v Vertex) Vertex { + g.once.Do(g.init) + g.vertices.Add(v) + return v +} + +// Remove removes a vertex from the graph. This will also remove any +// edges with this vertex as a source or target. +func (g *Graph) Remove(v Vertex) Vertex { + // Delete the vertex itself + g.vertices.Delete(v) + + // Delete the edges to non-existent things + for _, target := range g.DownEdges(v).List() { + g.RemoveEdge(BasicEdge(v, target)) + } + for _, source := range g.UpEdges(v).List() { + g.RemoveEdge(BasicEdge(source, v)) + } + + return nil +} + +// Replace replaces the original Vertex with replacement. If the original +// does not exist within the graph, then false is returned. Otherwise, true +// is returned. +func (g *Graph) Replace(original, replacement Vertex) bool { + // If we don't have the original, we can't do anything + if !g.vertices.Include(original) { + return false + } + + // Add our new vertex, then copy all the edges + g.Add(replacement) + for _, target := range g.DownEdges(original).List() { + g.Connect(BasicEdge(replacement, target)) + } + for _, source := range g.UpEdges(original).List() { + g.Connect(BasicEdge(source, replacement)) + } + + // Remove our old vertex, which will also remove all the edges + g.Remove(original) + + return true +} + +// RemoveEdge removes an edge from the graph. +func (g *Graph) RemoveEdge(edge Edge) { + g.once.Do(g.init) + + // Delete the edge from the set + g.edges.Delete(edge) + + // Delete the up/down edges + if s, ok := g.downEdges[edge.Source()]; ok { + s.Delete(edge.Target()) + } + if s, ok := g.upEdges[edge.Target()]; ok { + s.Delete(edge.Source()) + } +} + +// DownEdges returns the outward edges from the source Vertex v. +func (g *Graph) DownEdges(v Vertex) *Set { + g.once.Do(g.init) + return g.downEdges[v] +} + +// UpEdges returns the inward edges to the destination Vertex v. +func (g *Graph) UpEdges(v Vertex) *Set { + g.once.Do(g.init) + return g.upEdges[v] +} + +// Connect adds an edge with the given source and target. This is safe to +// call multiple times with the same value. Note that the same value is +// verified through pointer equality of the vertices, not through the +// value of the edge itself. +func (g *Graph) Connect(edge Edge) { + g.once.Do(g.init) + + source := edge.Source() + target := edge.Target() + + // Do we have this already? If so, don't add it again. + if s, ok := g.downEdges[source]; ok && s.Include(target) { + return + } + + // Add the edge to the set + g.edges.Add(edge) + + // Add the down edge + s, ok := g.downEdges[source] + if !ok { + s = new(Set) + g.downEdges[source] = s + } + s.Add(target) + + // Add the up edge + s, ok = g.upEdges[target] + if !ok { + s = new(Set) + g.upEdges[target] = s + } + s.Add(source) +} + +// String outputs some human-friendly output for the graph structure. +func (g *Graph) String() string { + var buf bytes.Buffer + + // Build the list of node names and a mapping so that we can more + // easily alphabetize the output to remain deterministic. + vertices := g.Vertices() + names := make([]string, 0, len(vertices)) + mapping := make(map[string]Vertex, len(vertices)) + for _, v := range vertices { + name := VertexName(v) + names = append(names, name) + mapping[name] = v + } + sort.Strings(names) + + // Write each node in order... + for _, name := range names { + v := mapping[name] + targets := g.downEdges[v] + + buf.WriteString(fmt.Sprintf("%s\n", name)) + + // Alphabetize dependencies + deps := make([]string, 0, targets.Len()) + for _, target := range targets.List() { + deps = append(deps, VertexName(target)) + } + sort.Strings(deps) + + // Write dependencies + for _, d := range deps { + buf.WriteString(fmt.Sprintf(" %s\n", d)) + } + } + + return buf.String() +} + +func (g *Graph) init() { + g.vertices = new(Set) + g.edges = new(Set) + g.downEdges = make(map[Vertex]*Set) + g.upEdges = make(map[Vertex]*Set) +} + +// VertexName returns the name of a vertex. +func VertexName(raw Vertex) string { + switch v := raw.(type) { + case NamedVertex: + return v.Name() + case fmt.Stringer: + return fmt.Sprintf("%s", v) + default: + return fmt.Sprintf("%v", v) + } +} diff --git a/dag/graph_test.go b/dag/graph_test.go new file mode 100644 index 000000000..e67535659 --- /dev/null +++ b/dag/graph_test.go @@ -0,0 +1,90 @@ +package dag + +import ( + "strings" + "testing" +) + +func TestGraph_empty(t *testing.T) { + var g Graph + g.Add(1) + g.Add(2) + g.Add(3) + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testGraphEmptyStr) + if actual != expected { + t.Fatalf("bad: %s", actual) + } +} + +func TestGraph_basic(t *testing.T) { + var g Graph + g.Add(1) + g.Add(2) + g.Add(3) + g.Connect(BasicEdge(1, 3)) + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testGraphBasicStr) + if actual != expected { + t.Fatalf("bad: %s", actual) + } +} + +func TestGraph_remove(t *testing.T) { + var g Graph + g.Add(1) + g.Add(2) + g.Add(3) + g.Connect(BasicEdge(1, 3)) + g.Remove(3) + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testGraphRemoveStr) + if actual != expected { + t.Fatalf("bad: %s", actual) + } +} + +func TestGraph_replace(t *testing.T) { + var g Graph + g.Add(1) + g.Add(2) + g.Add(3) + g.Connect(BasicEdge(1, 2)) + g.Connect(BasicEdge(2, 3)) + g.Replace(2, 42) + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testGraphReplaceStr) + if actual != expected { + t.Fatalf("bad: %s", actual) + } +} + +const testGraphBasicStr = ` +1 + 3 +2 +3 +` + +const testGraphEmptyStr = ` +1 +2 +3 +` + +const testGraphRemoveStr = ` +1 +2 +` + +const testGraphReplaceStr = ` +1 + 42 +3 +42 + 3 +` diff --git a/dag/set.go b/dag/set.go new file mode 100644 index 000000000..2b1d7f21b --- /dev/null +++ b/dag/set.go @@ -0,0 +1,72 @@ +package dag + +import ( + "sync" +) + +// Set is a set data structure. +type Set struct { + m map[interface{}]interface{} + once sync.Once +} + +// Hashable is the interface used by set to get the hash code of a value. +// If this isn't given, then the value of the item being added to the set +// itself is used as the comparison value. +type Hashable interface { + Hashcode() interface{} +} + +// Add adds an item to the set +func (s *Set) Add(v interface{}) { + s.once.Do(s.init) + s.m[s.code(v)] = v +} + +// Delete removes an item from the set. +func (s *Set) Delete(v interface{}) { + s.once.Do(s.init) + delete(s.m, s.code(v)) +} + +// Include returns true/false of whether a value is in the set. +func (s *Set) Include(v interface{}) bool { + s.once.Do(s.init) + _, ok := s.m[s.code(v)] + return ok +} + +// Len is the number of items in the set. +func (s *Set) Len() int { + if s == nil { + return 0 + } + + return len(s.m) +} + +// List returns the list of set elements. +func (s *Set) List() []interface{} { + if s == nil { + return nil + } + + r := make([]interface{}, 0, len(s.m)) + for _, v := range s.m { + r = append(r, v) + } + + return r +} + +func (s *Set) code(v interface{}) interface{} { + if h, ok := v.(Hashable); ok { + return h.Hashcode() + } + + return v +} + +func (s *Set) init() { + s.m = make(map[interface{}]interface{}) +} diff --git a/dag/tarjan.go b/dag/tarjan.go new file mode 100644 index 000000000..9d8b25ce2 --- /dev/null +++ b/dag/tarjan.go @@ -0,0 +1,107 @@ +package dag + +// StronglyConnected returns the list of strongly connected components +// within the Graph g. This information is primarily used by this package +// for cycle detection, but strongly connected components have widespread +// use. +func StronglyConnected(g *Graph) [][]Vertex { + vs := g.Vertices() + acct := sccAcct{ + NextIndex: 1, + VertexIndex: make(map[Vertex]int, len(vs)), + } + for _, v := range vs { + // Recurse on any non-visited nodes + if acct.VertexIndex[v] == 0 { + stronglyConnected(&acct, g, v) + } + } + return acct.SCC +} + +func stronglyConnected(acct *sccAcct, g *Graph, v Vertex) int { + // Initial vertex visit + index := acct.visit(v) + minIdx := index + + for _, raw := range g.DownEdges(v).List() { + target := raw.(Vertex) + targetIdx := acct.VertexIndex[target] + + // Recurse on successor if not yet visited + if targetIdx == 0 { + minIdx = min(minIdx, stronglyConnected(acct, g, target)) + } else if acct.inStack(target) { + // Check if the vertex is in the stack + minIdx = min(minIdx, targetIdx) + } + } + + // Pop the strongly connected components off the stack if + // this is a root vertex + if index == minIdx { + var scc []Vertex + for { + v2 := acct.pop() + scc = append(scc, v2) + if v2 == v { + break + } + } + + acct.SCC = append(acct.SCC, scc) + } + + return minIdx +} + +func min(a, b int) int { + if a <= b { + return a + } + return b +} + +// sccAcct is used ot pass around accounting information for +// the StronglyConnectedComponents algorithm +type sccAcct struct { + NextIndex int + VertexIndex map[Vertex]int + Stack []Vertex + SCC [][]Vertex +} + +// visit assigns an index and pushes a vertex onto the stack +func (s *sccAcct) visit(v Vertex) int { + idx := s.NextIndex + s.VertexIndex[v] = idx + s.NextIndex++ + s.push(v) + return idx +} + +// push adds a vertex to the stack +func (s *sccAcct) push(n Vertex) { + s.Stack = append(s.Stack, n) +} + +// pop removes a vertex from the stack +func (s *sccAcct) pop() Vertex { + n := len(s.Stack) + if n == 0 { + return nil + } + vertex := s.Stack[n-1] + s.Stack = s.Stack[:n-1] + return vertex +} + +// inStack checks if a vertex is in the stack +func (s *sccAcct) inStack(needle Vertex) bool { + for _, n := range s.Stack { + if n == needle { + return true + } + } + return false +} diff --git a/dag/tarjan_test.go b/dag/tarjan_test.go new file mode 100644 index 000000000..cdebcb3bb --- /dev/null +++ b/dag/tarjan_test.go @@ -0,0 +1,86 @@ +package dag + +import ( + "sort" + "strings" + "testing" +) + +func TestGraphStronglyConnected(t *testing.T) { + var g Graph + g.Add(1) + g.Add(2) + g.Connect(BasicEdge(1, 2)) + g.Connect(BasicEdge(2, 1)) + + actual := strings.TrimSpace(testSCCStr(StronglyConnected(&g))) + expected := strings.TrimSpace(testGraphStronglyConnectedStr) + if actual != expected { + t.Fatalf("bad: %s", actual) + } +} + +func TestGraphStronglyConnected_two(t *testing.T) { + var g Graph + g.Add(1) + g.Add(2) + g.Connect(BasicEdge(1, 2)) + g.Connect(BasicEdge(2, 1)) + g.Add(3) + + actual := strings.TrimSpace(testSCCStr(StronglyConnected(&g))) + expected := strings.TrimSpace(testGraphStronglyConnectedTwoStr) + if actual != expected { + t.Fatalf("bad: %s", actual) + } +} + +func TestGraphStronglyConnected_three(t *testing.T) { + var g Graph + g.Add(1) + g.Add(2) + g.Connect(BasicEdge(1, 2)) + g.Connect(BasicEdge(2, 1)) + g.Add(3) + g.Add(4) + g.Add(5) + g.Add(6) + g.Connect(BasicEdge(4, 5)) + g.Connect(BasicEdge(5, 6)) + g.Connect(BasicEdge(6, 4)) + + actual := strings.TrimSpace(testSCCStr(StronglyConnected(&g))) + expected := strings.TrimSpace(testGraphStronglyConnectedThreeStr) + if actual != expected { + t.Fatalf("bad: %s", actual) + } +} + +func testSCCStr(list [][]Vertex) string { + var lines []string + for _, vs := range list { + result := make([]string, len(vs)) + for i, v := range vs { + result[i] = VertexName(v) + } + + sort.Strings(result) + lines = append(lines, strings.Join(result, ",")) + } + + sort.Strings(lines) + return strings.Join(lines, "\n") +} + +const testGraphStronglyConnectedStr = `1,2` + +const testGraphStronglyConnectedTwoStr = ` +1,2 +3 +` + +const testGraphStronglyConnectedThreeStr = ` +1,2 +3 +4,5,6 +` diff --git a/terraform/context.go b/terraform/context.go index 50ad2a2bc..f1326dae7 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -3,62 +3,14 @@ package terraform import ( "fmt" "log" - "os" "sort" - "strconv" - "strings" "sync" - "sync/atomic" + "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform/config" - "github.com/hashicorp/terraform/config/lang/ast" "github.com/hashicorp/terraform/config/module" - "github.com/hashicorp/terraform/depgraph" - "github.com/hashicorp/terraform/helper/multierror" ) -// This is a function type used to implement a walker for the resource -// tree internally on the Terraform structure. -type genericWalkFunc func(*walkContext, *Resource) error - -// Context represents all the context that Terraform needs in order to -// perform operations on infrastructure. This structure is built using -// ContextOpts and NewContext. See the documentation for those. -// -// Additionally, a context can be created from a Plan using Plan.Context. -type Context struct { - module *module.Tree - diff *Diff - hooks []Hook - state *State - providerConfig map[string]map[string]map[string]interface{} - providers map[string]ResourceProviderFactory - provisioners map[string]ResourceProvisionerFactory - variables map[string]string - uiInput UIInput - - parallelSem Semaphore // Semaphore used to limit parallelism - l sync.Mutex // Lock acquired during any task - sl sync.RWMutex // Lock acquired to R/W internal data - runCh <-chan struct{} - sh *stopHook -} - -// ContextOpts are the user-creatable configuration structure to create -// a context with NewContext. -type ContextOpts struct { - Diff *Diff - Hooks []Hook - Module *module.Tree - Parallelism int - State *State - Providers map[string]ResourceProviderFactory - Provisioners map[string]ResourceProvisionerFactory - Variables map[string]string - - UIInput UIInput -} - // InputMode defines what sort of input will be asked for when Input // is called on Context. type InputMode byte @@ -75,79 +27,112 @@ const ( InputModeStd = InputModeVar | InputModeProvider ) -// NewContext creates a new context. -// -// Once a context is created, the pointer values within ContextOpts should -// not be mutated in any way, since the pointers are copied, not the values -// themselves. -func NewContext(opts *ContextOpts) *Context { - sh := new(stopHook) +// ContextOpts are the user-configurable options to create a context with +// NewContext. +type ContextOpts struct { + Diff *Diff + Hooks []Hook + Module *module.Tree + Parallelism int + State *State + Providers map[string]ResourceProviderFactory + Provisioners map[string]ResourceProvisionerFactory + Variables map[string]string + UIInput UIInput +} + +// Context represents all the context that Terraform needs in order to +// perform operations on infrastructure. This structure is built using +// NewContext. See the documentation for that. +type Context struct { + diff *Diff + diffLock sync.RWMutex + hooks []Hook + module *module.Tree + providers map[string]ResourceProviderFactory + provisioners map[string]ResourceProvisionerFactory + sh *stopHook + state *State + stateLock sync.RWMutex + uiInput UIInput + variables map[string]string + + l sync.Mutex // Lock acquired during any task + parallelSem Semaphore + providerInputConfig map[string]map[string]interface{} + runCh <-chan struct{} +} + +// NewContext creates a new Context structure. +// +// Once a Context is creator, the pointer values within ContextOpts +// should not be mutated in any way, since the pointers are copied, not +// the values themselves. +func NewContext(opts *ContextOpts) *Context { // Copy all the hooks and add our stop hook. We don't append directly // to the Config so that we're not modifying that in-place. + sh := new(stopHook) hooks := make([]Hook, len(opts.Hooks)+1) copy(hooks, opts.Hooks) hooks[len(opts.Hooks)] = sh - // Make the parallelism channel + state := opts.State + if state == nil { + state = new(State) + state.init() + } + + // Determine parallelism, default to 10. We do this both to limit + // CPU pressure but also to have an extra guard against rate throttling + // from providers. par := opts.Parallelism if par == 0 { par = 10 } return &Context{ - diff: opts.Diff, - hooks: hooks, - module: opts.Module, - state: opts.State, - providerConfig: make(map[string]map[string]map[string]interface{}), - providers: opts.Providers, - provisioners: opts.Provisioners, - variables: opts.Variables, - uiInput: opts.UIInput, + diff: opts.Diff, + hooks: hooks, + module: opts.Module, + providers: opts.Providers, + provisioners: opts.Provisioners, + state: state, + uiInput: opts.UIInput, + variables: opts.Variables, - parallelSem: NewSemaphore(par), - sh: sh, + parallelSem: NewSemaphore(par), + providerInputConfig: make(map[string]map[string]interface{}), + sh: sh, } } -// Apply applies the changes represented by this context and returns -// the resulting state. -// -// In addition to returning the resulting state, this context is updated -// with the latest state. -func (c *Context) Apply() (*State, error) { - v := c.acquireRun() - defer c.releaseRun(v) - - // Set our state right away. No matter what, this IS our new state, - // even if there is an error below. - c.state = c.state.deepcopy() - if c.state == nil { - c.state = &State{} - } - c.state.init() - - // Walk - log.Printf("[INFO] Apply walk starting") - err := c.walkContext(walkApply, rootModulePath).Walk() - log.Printf("[INFO] Apply walk complete") - - // Prune the state so that we have as clean a state as possible - c.state.prune() - - return c.state, err +// Graph returns the graph for this config. +func (c *Context) Graph() (*Graph, error) { + return c.GraphBuilder().Build(RootModulePath) } -// Graph returns the graph for this context. -func (c *Context) Graph() (*depgraph.Graph, error) { - return Graph(&GraphOpts{ +// GraphBuilder returns the GraphBuilder that will be used to create +// the graphs for this context. +func (c *Context) GraphBuilder() GraphBuilder { + // TODO test + providers := make([]string, 0, len(c.providers)) + for k, _ := range c.providers { + providers = append(providers, k) + } + + provisioners := make([]string, 0, len(c.provisioners)) + for k, _ := range c.provisioners { + provisioners = append(provisioners, k) + } + + return &BuiltinGraphBuilder{ + Root: c.module, Diff: c.diff, - Module: c.module, - Providers: c.providers, - Provisioners: c.provisioners, + Providers: providers, + Provisioners: provisioners, State: c.state, - }) + } } // Input asks for input to fill variables and provider configurations. @@ -220,16 +205,37 @@ func (c *Context) Input(mode InputMode) error { } if mode&InputModeProvider != 0 { - // Create the walk context and walk the inputs, which will gather the - // inputs for any resource providers. - wc := c.walkContext(walkInput, rootModulePath) - wc.Meta = new(walkInputMeta) - return wc.Walk() + // Do the walk + if _, err := c.walk(walkInput); err != nil { + return err + } } return nil } +// Apply applies the changes represented by this context and returns +// the resulting state. +// +// In addition to returning the resulting state, this context is updated +// with the latest state. +func (c *Context) Apply() (*State, error) { + v := c.acquireRun() + defer c.releaseRun(v) + + // Copy our own state + c.state = c.state.deepcopy() + + // Do the walk + _, err := c.walk(walkApply) + + // Clean out any unused things + c.state.prune() + println(fmt.Sprintf("%#v", c.state)) + + return c.state, err +} + // Plan generates an execution plan for the given context. // // The execution plan encapsulates the context and can be stored @@ -247,11 +253,9 @@ func (c *Context) Plan(opts *PlanOpts) (*Plan, error) { State: c.state, } - wc := c.walkContext(walkInvalid, rootModulePath) - wc.Meta = p - + var operation walkOperation if opts != nil && opts.Destroy { - wc.Operation = walkPlanDestroy + operation = walkPlanDestroy } else { // Set our state to be something temporary. We do this so that // the plan can update a fake state so that variables work, then @@ -267,16 +271,22 @@ func (c *Context) Plan(opts *PlanOpts) (*Plan, error) { c.state = old }() - wc.Operation = walkPlan + operation = walkPlan } - // Walk and run the plan - err := wc.Walk() + // Setup our diff + c.diffLock.Lock() + c.diff = new(Diff) + c.diff.init() + c.diffLock.Unlock() - // Update the diff so that our context is up-to-date - c.diff = p.Diff + // Do the walk + if _, err := c.walk(operation); err != nil { + return nil, err + } + p.Diff = c.diff - return p, err + return p, nil } // Refresh goes through all the resources in the state and refreshes them @@ -289,15 +299,18 @@ func (c *Context) Refresh() (*State, error) { v := c.acquireRun() defer c.releaseRun(v) - // Update our state + // Copy our own state c.state = c.state.deepcopy() - // Walk the graph - err := c.walkContext(walkRefresh, rootModulePath).Walk() + // Do the walk + if _, err := c.walk(walkRefresh); err != nil { + return nil, err + } - // Prune the state + // Clean out any unused things c.state.prune() - return c.state, err + + return c.state, nil } // Stop stops the running task. @@ -323,43 +336,34 @@ func (c *Context) Stop() { // Validate validates the configuration and returns any warnings or errors. func (c *Context) Validate() ([]string, []error) { - var rerr *multierror.Error + v := c.acquireRun() + defer c.releaseRun(v) + + var errs error // Validate the configuration itself if err := c.module.Validate(); err != nil { - rerr = multierror.ErrorAppend(rerr, err) + errs = multierror.Append(errs, err) } // This only needs to be done for the root module, since inter-module // variables are validated in the module tree. if config := c.module.Config(); config != nil { // Validate the user variables - if errs := smcUserVariables(config, c.variables); len(errs) > 0 { - rerr = multierror.ErrorAppend(rerr, errs...) + if err := smcUserVariables(config, c.variables); len(err) > 0 { + errs = multierror.Append(errs, err...) } } - // Validate the entire graph - walkMeta := new(walkValidateMeta) - wc := c.walkContext(walkValidate, rootModulePath) - wc.Meta = walkMeta - if err := wc.Walk(); err != nil { - rerr = multierror.ErrorAppend(rerr, err) + // Walk + walker, err := c.walk(walkValidate) + if err != nil { + return nil, multierror.Append(errs, err).Errors } - // Flatten the warns/errs so that we get all the module errors as well, - // then aggregate. - warns, errs := walkMeta.Flatten() - if len(errs) > 0 { - rerr = multierror.ErrorAppend(rerr, errs...) - } - - errs = nil - if rerr != nil && len(rerr.Errors) > 0 { - errs = rerr.Errors - } - - return warns, errs + // Return the result + rerrs := multierror.Append(errs, walker.ValidationErrors...) + return walker.ValidationWarnings, rerrs.Errors } func (c *Context) acquireRun() chan<- struct{} { @@ -388,1528 +392,15 @@ func (c *Context) releaseRun(ch chan<- struct{}) { c.sh.Reset() } -func (c *Context) walkContext(op walkOperation, path []string) *walkContext { - // Get the config structure - m := c.module - for _, n := range path[1:] { - cs := m.Children() - m = cs[n] - } - var conf *config.Config - if m != nil { - conf = m.Config() - } - - // Calculate the default variable values - defaultVars := make(map[string]string) - if conf != nil { - for _, v := range conf.Variables { - for k, val := range v.DefaultsMap() { - defaultVars[k] = val - } - } - } - - return &walkContext{ - Context: c, - Operation: op, - Path: path, - Variables: c.variables, - - defaultVariables: defaultVars, - } -} - -// walkContext is the context in which a graph walk is done. It stores -// much the same as a Context but works on a specific module. -type walkContext struct { - Context *Context - Meta interface{} - Operation walkOperation - Path []string - Variables map[string]string - - defaultVariables map[string]string - - // This is only set manually by subsequent context creations - // in genericWalkFunc. - graph *depgraph.Graph -} - -// walkOperation is an enum which tells the walkContext what to do. -type walkOperation byte - -const ( - walkInvalid walkOperation = iota - walkInput - walkApply - walkPlan - walkPlanDestroy - walkRefresh - walkValidate -) - -func (c *walkContext) Walk() error { - g := c.graph - if g == nil { - gopts := &GraphOpts{ - Module: c.Context.module, - Providers: c.Context.providers, - Provisioners: c.Context.provisioners, - State: c.Context.state, - } - if c.Operation == walkApply { - gopts.Diff = c.Context.diff - } - - var err error - g, err = Graph(gopts) - if err != nil { - return err - } - } - - var walkFn depgraph.WalkFunc - switch c.Operation { - case walkInput: - walkFn = c.inputWalkFn() - case walkApply: - walkFn = c.applyWalkFn() - case walkPlan: - walkFn = c.planWalkFn() - case walkPlanDestroy: - walkFn = c.planDestroyWalkFn() - case walkRefresh: - walkFn = c.refreshWalkFn() - case walkValidate: - walkFn = c.validateWalkFn() - default: - panic(fmt.Sprintf("unknown operation: %#v", c.Operation)) - } - - if err := g.Walk(walkFn); err != nil { - return err - } - - switch c.Operation { - case walkInput: - fallthrough - case walkValidate: - // Don't calculate outputs - return nil - } - - // We did an apply, so we need to calculate the outputs. If we have no - // outputs, then we're done. - m := c.Context.module - for _, n := range c.Path[1:] { - cs := m.Children() - m = cs[n] - } - if m == nil { - return nil - } - conf := m.Config() - if len(conf.Outputs) == 0 { - return nil - } - - // Likewise, if we have no resources in our state, we're done. This - // guards against the case that we destroyed. - mod := c.Context.state.ModuleByPath(c.Path) - if mod == nil { - return nil - } - if c.Operation == walkApply { - // On Apply, we prune so that we don't do outputs if we destroyed - mod.prune() - } - if len(mod.Resources) == 0 && len(conf.Resources) != 0 { - mod.Outputs = nil - return nil - } - - outputs := make(map[string]string) - for _, o := range conf.Outputs { - if err := c.computeVars(o.RawConfig, nil); err != nil { - // If we're refreshing, then we ignore output errors. This is - // properly not fully the correct behavior, but fixes a range - // of issues right now. As we expand test cases to find the - // correct behavior, this will likely be removed. - if c.Operation == walkRefresh { - continue - } - - return err - } - vraw := o.RawConfig.Config()["value"] - if vraw == nil { - // This likely means that the result of the output is - // a computed variable. - if o.RawConfig.Raw["value"] != nil { - vraw = config.UnknownVariableValue - } - } - if vraw != nil { - if list, ok := vraw.([]interface{}); ok { - vraw = list[0] - } - if s, ok := vraw.(string); ok { - outputs[o.Name] = s - } else { - return fmt.Errorf("Type of output '%s' is not a string: %#v", o.Name, vraw) - } - } - } - - // Assign the outputs to the root module - mod.Outputs = outputs - - return nil -} - -func (c *walkContext) inputWalkFn() depgraph.WalkFunc { - meta := c.Meta.(*walkInputMeta) - meta.Lock() - if meta.Done == nil { - meta.Done = make(map[string]struct{}) - } - meta.Unlock() - - return func(n *depgraph.Noun) error { - // If it is the root node, ignore - if n.Name == GraphRootNode { - return nil - } - - switch rn := n.Meta.(type) { - case *GraphNodeModule: - // Build another walkContext for this module and walk it. - wc := c.Context.walkContext(c.Operation, rn.Path) - - // Set the graph to specifically walk this subgraph - wc.graph = rn.Graph - - // Preserve the meta - wc.Meta = c.Meta - - return wc.Walk() - case *GraphNodeResource: - // Resources don't matter for input. Continue. - return nil - case *GraphNodeResourceProvider: - // Acquire the lock the whole time so we only ask for input - // one at a time. - meta.Lock() - defer meta.Unlock() - - // If we already did this provider, then we're done. - if _, ok := meta.Done[rn.ID]; ok { - return nil - } - - // Get the raw configuration because this is what we - // pass into the API. - var raw *config.RawConfig - sharedProvider := rn.Provider - if sharedProvider.Config != nil { - raw = sharedProvider.Config.RawConfig - } - rc := NewResourceConfig(raw) - rc.Config = make(map[string]interface{}) - - // Wrap the input into a namespace - input := &PrefixUIInput{ - IdPrefix: fmt.Sprintf("provider.%s", rn.ID), - QueryPrefix: fmt.Sprintf("provider.%s.", rn.ID), - UIInput: c.Context.uiInput, - } - - // Go through each provider and capture the input necessary - // to satisfy it. - configs := make(map[string]map[string]interface{}) - for k, p := range sharedProvider.Providers { - newc, err := p.Input(input, rc) - if err != nil { - return fmt.Errorf( - "Error configuring %s: %s", k, err) - } - if newc != nil && len(newc.Config) > 0 { - configs[k] = newc.Config - } - } - - // Mark this provider as done - meta.Done[rn.ID] = struct{}{} - - // Set the configuration - c.Context.providerConfig[rn.ID] = configs - } - - return nil - } -} - -func (c *walkContext) applyWalkFn() depgraph.WalkFunc { - cb := func(c *walkContext, r *Resource) error { - var err error - - diff := r.Diff - if diff.Empty() { - log.Printf("[DEBUG] %s: Diff is empty. Will not apply.", r.Id) - return nil - } - - is := r.State - if is == nil { - is = new(InstanceState) - } - is.init() - - if !diff.Destroy { - // Since we need the configuration, interpolate the variables - if err := r.Config.interpolate(c, r); err != nil { - return err - } - - diff, err = r.Provider.Diff(r.Info, is, r.Config) - if err != nil { - return err - } - - // This can happen if we aren't actually applying anything - // except an ID (the "null" provider). It is not really an issue - // since the Same check later down will catch any real problems. - if diff == nil { - diff = new(InstanceDiff) - diff.init() - } - - // Delete id from the diff because it is dependent on - // our internal plan function. - delete(r.Diff.Attributes, "id") - delete(diff.Attributes, "id") - - // Verify the diffs are the same - if !r.Diff.Same(diff) { - log.Printf( - "[ERROR] Diffs don't match.\n\nDiff 1: %#v"+ - "\n\nDiff 2: %#v", - r.Diff, diff) - return fmt.Errorf( - "%s: diffs didn't match during apply. This is a "+ - "bug with the resource provider, please report a bug.", - r.Id) - } - } - - // Remove any output values from the diff - for k, ad := range diff.Attributes { - if ad.Type == DiffAttrOutput { - delete(diff.Attributes, k) - } - } - - for _, h := range c.Context.hooks { - handleHook(h.PreApply(r.Info, is, diff)) - } - - // We create a new instance if there was no ID - // previously or the diff requires re-creating the - // underlying instance - createNew := (is.ID == "" && !diff.Destroy) || diff.RequiresNew() - - // With the completed diff, apply! - log.Printf("[DEBUG] %s: Executing Apply", r.Id) - is, applyerr := r.Provider.Apply(r.Info, is, diff) - - var errs []error - if applyerr != nil { - errs = append(errs, applyerr) - } - - // Make sure the result is instantiated - if is == nil { - is = new(InstanceState) - } - is.init() - - // Force the "id" attribute to be our ID - if is.ID != "" { - is.Attributes["id"] = is.ID - } - - for ak, av := range is.Attributes { - // If the value is the unknown variable value, then it is an error. - // In this case we record the error and remove it from the state - if av == config.UnknownVariableValue { - errs = append(errs, fmt.Errorf( - "Attribute with unknown value: %s", ak)) - delete(is.Attributes, ak) - } - } - - // Set the result state - r.State = is - c.persistState(r) - - // Invoke any provisioners we have defined. This is only done - // if the resource was created, as updates or deletes do not - // invoke provisioners. - // - // Additionally, we need to be careful to not run this if there - // was an error during the provider apply. - tainted := false - if createNew && len(r.Provisioners) > 0 { - if applyerr == nil { - // If the apply succeeded, we have to run the provisioners - for _, h := range c.Context.hooks { - handleHook(h.PreProvisionResource(r.Info, is)) - } - - if err := c.applyProvisioners(r, is); err != nil { - errs = append(errs, err) - tainted = true - } - - for _, h := range c.Context.hooks { - handleHook(h.PostProvisionResource(r.Info, is)) - } - } else { - // If we failed to create properly and we have provisioners, - // then we have to mark ourselves as tainted to try again. - tainted = true - } - } - - // If we're tainted then we need to update some flags - if tainted && r.Flags&FlagTainted == 0 { - r.Flags &^= FlagPrimary - r.Flags &^= FlagHasTainted - r.Flags |= FlagTainted - r.TaintedIndex = -1 - c.persistState(r) - } - - for _, h := range c.Context.hooks { - handleHook(h.PostApply(r.Info, is, applyerr)) - } - - // Determine the new state and update variables - err = nil - if len(errs) > 0 { - err = &multierror.Error{Errors: errs} - } - - return err - } - - return c.genericWalkFn(cb) -} - -func (c *walkContext) planWalkFn() depgraph.WalkFunc { - var l sync.Mutex - - // Initialize the result - result := c.Meta.(*Plan) - result.init() - - cb := func(c *walkContext, r *Resource) error { - if r.Flags&FlagTainted != 0 { - // We don't diff tainted resources. - return nil - } - - var diff *InstanceDiff - - is := r.State - - for _, h := range c.Context.hooks { - handleHook(h.PreDiff(r.Info, is)) - } - - if r.Flags&FlagOrphan != 0 { - log.Printf("[DEBUG] %s: Orphan, marking for destroy", r.Id) - - // This is an orphan (no config), so we mark it to be destroyed - diff = &InstanceDiff{Destroy: true} - } else { - // Make sure the configuration is interpolated - if err := r.Config.interpolate(c, r); err != nil { - return err - } - - // Get a diff from the newest state - log.Printf("[DEBUG] %s: Executing diff", r.Id) - var err error - - diffIs := is - if diffIs == nil || r.Flags&FlagHasTainted != 0 { - // If we're tainted, we pretend to create a new thing. - diffIs = new(InstanceState) - } - diffIs.init() - - diff, err = r.Provider.Diff(r.Info, diffIs, r.Config) - if err != nil { - return err - } - } - - if diff == nil { - diff = new(InstanceDiff) - } - - if r.Flags&FlagHasTainted != 0 { - // This primary has a tainted resource, so just mark for - // destroy... - log.Printf("[DEBUG] %s: Tainted children, marking for destroy", r.Id) - diff.DestroyTainted = true - } - - if diff.RequiresNew() && is != nil && is.ID != "" { - // This will also require a destroy - diff.Destroy = true - } - - if diff.RequiresNew() || is == nil || is.ID == "" { - var oldID string - if is != nil { - oldID = is.Attributes["id"] - } - - // Add diff to compute new ID - diff.init() - diff.Attributes["id"] = &ResourceAttrDiff{ - Old: oldID, - NewComputed: true, - RequiresNew: true, - Type: DiffAttrOutput, - } - } - - if !diff.Empty() { - log.Printf("[DEBUG] %s: Diff: %#v", r.Id, diff) - - l.Lock() - md := result.Diff.ModuleByPath(c.Path) - if md == nil { - md = result.Diff.AddModule(c.Path) - } - md.Resources[r.Id] = diff - l.Unlock() - } - - for _, h := range c.Context.hooks { - handleHook(h.PostDiff(r.Info, diff)) - } - - // Determine the new state and update variables - if !diff.Empty() { - is = is.MergeDiff(diff) - } - - // Set it so that it can be updated - r.State = is - c.persistState(r) - - return nil - } - - return c.genericWalkFn(cb) -} - -func (c *walkContext) planDestroyWalkFn() depgraph.WalkFunc { - var l sync.Mutex - - // Initialize the result - result := c.Meta.(*Plan) - result.init() - - var walkFn depgraph.WalkFunc - walkFn = func(n *depgraph.Noun) error { - switch m := n.Meta.(type) { - case *GraphNodeModule: - // Set the destroy bool on the module - md := result.Diff.ModuleByPath(m.Path) - if md == nil { - md = result.Diff.AddModule(m.Path) - } - md.Destroy = true - - // Build another walkContext for this module and walk it. - wc := c.Context.walkContext(c.Operation, m.Path) - - // compute incoming vars - if m.Config != nil { - wc.Variables = make(map[string]string) - - rc := NewResourceConfig(m.Config.RawConfig) - if err := rc.interpolate(c, nil); err != nil { - return err - } - for k, v := range rc.Config { - wc.Variables[k] = v.(string) - } - for k, _ := range rc.Raw { - if _, ok := wc.Variables[k]; !ok { - wc.Variables[k] = config.UnknownVariableValue - } - } - } - - // Set the graph to specifically walk this subgraph - wc.graph = m.Graph - - // Preserve the meta - wc.Meta = c.Meta - - return wc.Walk() - case *GraphNodeResource: - // If we're expanding, then expand the nodes, and then rewalk the graph - if m.ExpandMode > ResourceExpandNone { - return c.genericWalkResource(m, walkFn) - } - - r := m.Resource - - if r.State != nil && r.State.ID != "" { - log.Printf("[DEBUG] %s: Making for destroy", r.Id) - - l.Lock() - defer l.Unlock() - md := result.Diff.ModuleByPath(c.Path) - if md == nil { - md = result.Diff.AddModule(c.Path) - } - md.Resources[r.Id] = &InstanceDiff{Destroy: true} - } else { - log.Printf("[DEBUG] %s: Not marking for destroy, no ID", r.Id) - } - } - - return nil - } - - return walkFn -} - -func (c *walkContext) refreshWalkFn() depgraph.WalkFunc { - cb := func(c *walkContext, r *Resource) error { - is := r.State - - if is == nil || is.ID == "" { - log.Printf("[DEBUG] %s: Not refreshing, ID is empty", r.Id) - return nil - } - - for _, h := range c.Context.hooks { - handleHook(h.PreRefresh(r.Info, is)) - } - - is, err := r.Provider.Refresh(r.Info, is) - if err != nil { - return err - } - if is == nil { - is = new(InstanceState) - is.init() - } - - // Set the updated state - r.State = is - c.persistState(r) - - for _, h := range c.Context.hooks { - handleHook(h.PostRefresh(r.Info, is)) - } - - return nil - } - - return c.genericWalkFn(cb) -} - -func (c *walkContext) validateWalkFn() depgraph.WalkFunc { - var l sync.Mutex - - meta := c.Meta.(*walkValidateMeta) - if meta.Children == nil { - meta.Children = make(map[string]*walkValidateMeta) - } - - var walkFn depgraph.WalkFunc - walkFn = func(n *depgraph.Noun) error { - // If it is the root node, ignore - if n.Name == GraphRootNode { - return nil - } - - switch rn := n.Meta.(type) { - case *GraphNodeModule: - // Build another walkContext for this module and walk it. - wc := c.Context.walkContext(walkValidate, rn.Path) - - // Set the graph to specifically walk this subgraph - wc.graph = rn.Graph - - // Build the meta parameter. Do this by sharing the Children - // reference but copying the rest into our own Children list. - newMeta := new(walkValidateMeta) - newMeta.Children = meta.Children - wc.Meta = newMeta - - if err := wc.Walk(); err != nil { - return err - } - - newMeta.Children = nil - meta.Children[strings.Join(rn.Path, ".")] = newMeta - return nil - case *GraphNodeResource: - if rn.Resource == nil { - panic("resource should never be nil") - } - - // If we're expanding, then expand the nodes, and then rewalk the graph - if rn.ExpandMode > ResourceExpandNone { - // Interpolate the count and verify it is non-negative - rc := NewResourceConfig(rn.Config.RawCount) - if err := rc.interpolate(c, rn.Resource); err != nil { - return err - } - if !rc.IsComputed(rn.Config.RawCount.Key) { - count, err := rn.Config.Count() - if err == nil { - if count < 0 { - err = fmt.Errorf( - "%s error: count must be positive", rn.Resource.Id) - } - } - if err != nil { - l.Lock() - defer l.Unlock() - meta.Errs = append(meta.Errs, err) - return nil - } - } - - return c.genericWalkResource(rn, walkFn) - } - - // If it doesn't have a provider, that is a different problem - if rn.Resource.Provider == nil { - return nil - } - - // Don't validate orphans or tainted since they never have a config - if rn.Resource.Flags&FlagOrphan != 0 { - return nil - } - if rn.Resource.Flags&FlagTainted != 0 { - return nil - } - - // If the resouce name doesn't match the name regular - // expression, show a warning. - if !config.NameRegexp.Match([]byte(rn.Config.Name)) { - l.Lock() - meta.Warns = append(meta.Warns, fmt.Sprintf( - "%s: module name can only contain letters, numbers, "+ - "dashes, and underscores.\n"+ - "This will be an error in Terraform 0.4", - rn.Resource.Id)) - l.Unlock() - } - - // Compute the variables in this resource - rn.Resource.Config.interpolate(c, rn.Resource) - - log.Printf("[INFO] Validating resource: %s", rn.Resource.Id) - ws, es := rn.Resource.Provider.ValidateResource( - rn.Resource.Info.Type, rn.Resource.Config) - for i, w := range ws { - ws[i] = fmt.Sprintf("'%s' warning: %s", rn.Resource.Id, w) - } - for i, e := range es { - es[i] = fmt.Errorf("'%s' error: %s", rn.Resource.Id, e) - } - - l.Lock() - meta.Warns = append(meta.Warns, ws...) - meta.Errs = append(meta.Errs, es...) - l.Unlock() - - for idx, p := range rn.Resource.Provisioners { - ws, es := p.Provisioner.Validate(p.Config) - for i, w := range ws { - ws[i] = fmt.Sprintf("'%s.provisioner.%d' warning: %s", rn.Resource.Id, idx, w) - } - for i, e := range es { - es[i] = fmt.Errorf("'%s.provisioner.%d' error: %s", rn.Resource.Id, idx, e) - } - - l.Lock() - meta.Warns = append(meta.Warns, ws...) - meta.Errs = append(meta.Errs, es...) - l.Unlock() - } - - case *GraphNodeResourceProvider: - sharedProvider := rn.Provider - - // Check if we have an override - cs, ok := c.Context.providerConfig[rn.ID] - if !ok { - cs = make(map[string]map[string]interface{}) - } - - for k, p := range sharedProvider.Providers { - // Merge the configurations to get what we use to configure with - rc := sharedProvider.MergeConfig(false, cs[k]) - if err := rc.interpolate(c, nil); err != nil { - return err - } - - log.Printf("[INFO] Validating provider: %s", k) - ws, es := p.Validate(rc) - for i, w := range ws { - ws[i] = fmt.Sprintf("Provider '%s' warning: %s", k, w) - } - for i, e := range es { - es[i] = fmt.Errorf("Provider '%s' error: %s", k, e) - } - - l.Lock() - meta.Warns = append(meta.Warns, ws...) - meta.Errs = append(meta.Errs, es...) - l.Unlock() - } - } - - return nil - } - - return walkFn -} - -func (c *walkContext) genericWalkFn(cb genericWalkFunc) depgraph.WalkFunc { - // This will keep track of whether we're stopped or not - var stop uint32 = 0 - - var walkFn depgraph.WalkFunc - walkFn = func(n *depgraph.Noun) error { - // If it is the root node, ignore - if n.Name == GraphRootNode { - return nil - } - - // If we're stopped, return right away - if atomic.LoadUint32(&stop) != 0 { - return nil - } - - switch m := n.Meta.(type) { - case *GraphNodeModule: - // Build another walkContext for this module and walk it. - wc := c.Context.walkContext(c.Operation, m.Path) - - // Set the graph to specifically walk this subgraph - wc.graph = m.Graph - - // Preserve the meta - wc.Meta = c.Meta - - // Set the variables - if m.Config != nil { - wc.Variables = make(map[string]string) - - rc := NewResourceConfig(m.Config.RawConfig) - if err := rc.interpolate(c, nil); err != nil { - return err - } - for k, v := range rc.Config { - wc.Variables[k] = v.(string) - } - for k, _ := range rc.Raw { - if _, ok := wc.Variables[k]; !ok { - wc.Variables[k] = config.UnknownVariableValue - } - } - } - - return wc.Walk() - case *GraphNodeResource: - // Continue, we care about this the most - case *GraphNodeResourceProvider: - sharedProvider := m.Provider - - // Check if we have an override - cs, ok := c.Context.providerConfig[m.ID] - if !ok { - cs = make(map[string]map[string]interface{}) - } - - for k, p := range sharedProvider.Providers { - // Interpolate our own configuration before merging - if sharedProvider.Config != nil { - rc := NewResourceConfig(sharedProvider.Config.RawConfig) - if err := rc.interpolate(c, nil); err != nil { - return err - } - } - - // Merge the configurations to get what we use to configure - // with. We don't need to interpolate this because the - // lines above verify that all parents are interpolated - // properly. - rc := sharedProvider.MergeConfig(false, cs[k]) - - log.Printf("[INFO] Configuring provider: %s", k) - err := p.Configure(rc) - if err != nil { - return err - } - } - - return nil - default: - panic(fmt.Sprintf("unknown graph node: %#v", n.Meta)) - } - - rn := n.Meta.(*GraphNodeResource) - - // If we're expanding, then expand the nodes, and then rewalk the graph - if rn.ExpandMode > ResourceExpandNone { - return c.genericWalkResource(rn, walkFn) - } - - // Make sure that at least some resource configuration is set - if rn.Config == nil { - rn.Resource.Config = new(ResourceConfig) - } else { - rn.Resource.Config = NewResourceConfig(rn.Config.RawConfig) - } - - // Handle recovery of special panic scenarios - defer func() { - if v := recover(); v != nil { - if v == HookActionHalt { - atomic.StoreUint32(&stop, 1) - } else { - panic(v) - } - } - }() - - // Limit parallelism - c.Context.parallelSem.Acquire() - defer c.Context.parallelSem.Release() - - // Call the callack - log.Printf( - "[INFO] Module %s walking: %s (Graph node: %s)", - strings.Join(c.Path, "."), - rn.Resource.Id, - n.Name) - if err := cb(c, rn.Resource); err != nil { - log.Printf("[ERROR] Error walking '%s': %s", rn.Resource.Id, err) - return err - } - - return nil - } - - return walkFn -} - -func (c *walkContext) genericWalkResource( - rn *GraphNodeResource, fn depgraph.WalkFunc) error { - // Interpolate the count - rc := NewResourceConfig(rn.Config.RawCount) - if err := rc.interpolate(c, rn.Resource); err != nil { - return err - } - - // If we're validating, then we set the count to 1 if it is computed - if c.Operation == walkValidate { - if key := rn.Config.RawCount.Key; rc.IsComputed(key) { - // Preserve the old value so that we reset it properly - old := rn.Config.RawCount.Raw[key] - defer func() { - rn.Config.RawCount.Raw[key] = old - }() - - // Set th count to 1 for validation purposes - rn.Config.RawCount.Raw[key] = "1" - } - } - - // Expand the node to the actual resources - g, err := rn.Expand() +func (c *Context) walk(operation walkOperation) (*ContextGraphWalker, error) { + // Build the graph + graph, err := c.GraphBuilder().Build(RootModulePath) if err != nil { - return err + return nil, err } - // Walk the graph with our function - if err := g.Walk(fn); err != nil { - return err - } - - return nil -} - -// applyProvisioners is used to run any provisioners a resource has -// defined after the resource creation has already completed. -func (c *walkContext) applyProvisioners(r *Resource, is *InstanceState) error { - // Store the original connection info, restore later - origConnInfo := is.Ephemeral.ConnInfo - defer func() { - is.Ephemeral.ConnInfo = origConnInfo - }() - - for _, prov := range r.Provisioners { - // Interpolate since we may have variables that depend on the - // local resource. - if err := prov.Config.interpolate(c, r); err != nil { - return err - } - - // Interpolate the conn info, since it may contain variables - connInfo := NewResourceConfig(prov.ConnInfo) - if err := connInfo.interpolate(c, r); err != nil { - return err - } - - // Merge the connection information - overlay := make(map[string]string) - if origConnInfo != nil { - for k, v := range origConnInfo { - overlay[k] = v - } - } - for k, v := range connInfo.Config { - switch vt := v.(type) { - case string: - overlay[k] = vt - case int64: - overlay[k] = strconv.FormatInt(vt, 10) - case int32: - overlay[k] = strconv.FormatInt(int64(vt), 10) - case int: - overlay[k] = strconv.FormatInt(int64(vt), 10) - case float32: - overlay[k] = strconv.FormatFloat(float64(vt), 'f', 3, 32) - case float64: - overlay[k] = strconv.FormatFloat(vt, 'f', 3, 64) - case bool: - overlay[k] = strconv.FormatBool(vt) - default: - overlay[k] = fmt.Sprintf("%v", vt) - } - } - is.Ephemeral.ConnInfo = overlay - - // Invoke the Provisioner - for _, h := range c.Context.hooks { - handleHook(h.PreProvision(r.Info, prov.Type)) - } - - output := ProvisionerUIOutput{ - Info: r.Info, - Type: prov.Type, - Hooks: c.Context.hooks, - } - err := prov.Provisioner.Apply(&output, is, prov.Config) - if err != nil { - return err - } - - for _, h := range c.Context.hooks { - handleHook(h.PostProvision(r.Info, prov.Type)) - } - } - - return nil -} - -// persistState persists the state in a Resource to the actual final -// state location. -func (c *walkContext) persistState(r *Resource) { - // Acquire a state lock around this whole thing since we're updating that - c.Context.sl.Lock() - defer c.Context.sl.Unlock() - - // If we have no state, then we don't persist. - if c.Context.state == nil { - return - } - - // Get the state for this resource. The resource state should always - // exist because we call graphInitState before anything that could - // potentially call this. - module := c.Context.state.ModuleByPath(c.Path) - if module == nil { - module = c.Context.state.AddModule(c.Path) - } - rs := module.Resources[r.Id] - if rs == nil { - rs = &ResourceState{Type: r.Info.Type} - rs.init() - module.Resources[r.Id] = rs - } - rs.Dependencies = r.Dependencies - - // Assign the instance state to the proper location - if r.Flags&FlagDeposed != 0 { - // We were previously the primary and have been deposed, so - // now we are the final tainted resource - r.TaintedIndex = len(rs.Tainted) - 1 - rs.Tainted[r.TaintedIndex] = r.State - - } else if r.Flags&FlagTainted != 0 { - if r.TaintedIndex >= 0 { - // Tainted with a pre-existing index, just update that spot - rs.Tainted[r.TaintedIndex] = r.State - - } else if r.Flags&FlagReplacePrimary != 0 { - // We just replaced the primary, so restore the primary - rs.Primary = rs.Tainted[len(rs.Tainted)-1] - - // Set ourselves as tainted - rs.Tainted[len(rs.Tainted)-1] = r.State - - } else { - // Newly tainted, so append it to the list, update the - // index, and remove the primary. - rs.Tainted = append(rs.Tainted, r.State) - r.TaintedIndex = len(rs.Tainted) - 1 - rs.Primary = nil - } - - } else if r.Flags&FlagReplacePrimary != 0 { - // If the ID is blank (there was an error), then we leave - // the primary that exists, and do not store this as a tainted - // instance - if r.State.ID == "" { - return - } - - // Push the old primary into the tainted state - rs.Tainted = append(rs.Tainted, rs.Primary) - - // Set this as the new primary - rs.Primary = r.State - - } else { - // The primary instance, so just set it directly - rs.Primary = r.State - } - - // Do a pruning so that empty resources are not saved - rs.prune() -} - -// computeVars takes the State and given RawConfig and processes all -// the variables. This dynamically discovers the attributes instead of -// using a static map[string]string that the genericWalkFn uses. -func (c *walkContext) computeVars( - raw *config.RawConfig, r *Resource) error { - // If there isn't a raw configuration, don't do anything - if raw == nil { - return nil - } - - // Copy the default variables - vs := make(map[string]ast.Variable) - for k, v := range c.defaultVariables { - vs[k] = ast.Variable{ - Value: v, - Type: ast.TypeString, - } - } - - // Next, the actual computed variables - for n, rawV := range raw.Variables { - switch v := rawV.(type) { - case *config.CountVariable: - switch v.Type { - case config.CountValueIndex: - if r != nil { - vs[n] = ast.Variable{ - Value: int(r.CountIndex), - Type: ast.TypeInt, - } - } - } - case *config.ModuleVariable: - if c.Operation == walkValidate { - vs[n] = ast.Variable{ - Value: config.UnknownVariableValue, - Type: ast.TypeString, - } - continue - } - - value, err := c.computeModuleVariable(v) - if err != nil { - return err - } - - vs[n] = ast.Variable{ - Value: value, - Type: ast.TypeString, - } - case *config.PathVariable: - switch v.Type { - case config.PathValueCwd: - wd, err := os.Getwd() - if err != nil { - return fmt.Errorf( - "Couldn't get cwd for var %s: %s", - v.FullKey(), err) - } - - vs[n] = ast.Variable{ - Value: wd, - Type: ast.TypeString, - } - case config.PathValueModule: - if t := c.Context.module.Child(c.Path[1:]); t != nil { - vs[n] = ast.Variable{ - Value: t.Config().Dir, - Type: ast.TypeString, - } - } - case config.PathValueRoot: - vs[n] = ast.Variable{ - Value: c.Context.module.Config().Dir, - Type: ast.TypeString, - } - } - case *config.ResourceVariable: - if c.Operation == walkValidate { - vs[n] = ast.Variable{ - Value: config.UnknownVariableValue, - Type: ast.TypeString, - } - continue - } - - var attr string - var err error - if v.Multi && v.Index == -1 { - attr, err = c.computeResourceMultiVariable(v) - } else { - attr, err = c.computeResourceVariable(v) - } - if err != nil { - return err - } - - vs[n] = ast.Variable{ - Value: attr, - Type: ast.TypeString, - } - case *config.UserVariable: - val, ok := c.Variables[v.Name] - if ok { - vs[n] = ast.Variable{ - Value: val, - Type: ast.TypeString, - } - continue - } - - if _, ok := vs[n]; !ok && c.Operation == walkValidate { - vs[n] = ast.Variable{ - Value: config.UnknownVariableValue, - Type: ast.TypeString, - } - continue - } - - // Look up if we have any variables with this prefix because - // those are map overrides. Include those. - for k, val := range c.Variables { - if strings.HasPrefix(k, v.Name+".") { - vs["var."+k] = ast.Variable{ - Value: val, - Type: ast.TypeString, - } - } - } - } - } - - // Interpolate the variables - return raw.Interpolate(vs) -} - -func (c *walkContext) computeModuleVariable( - v *config.ModuleVariable) (string, error) { - // Build the path to our child - path := make([]string, len(c.Path), len(c.Path)+1) - copy(path, c.Path) - path = append(path, v.Name) - - // Grab some locks - c.Context.sl.RLock() - defer c.Context.sl.RUnlock() - - // Get that module from our state - mod := c.Context.state.ModuleByPath(path) - if mod == nil { - // If the module doesn't exist, then we can return an empty string. - // This happens usually only in Refresh() when we haven't populated - // a state. During validation, we semantically verify that all - // modules reference other modules, and graph ordering should - // ensure that the module is in the state, so if we reach this - // point otherwise it really is a panic. - return config.UnknownVariableValue, nil - } - - value, ok := mod.Outputs[v.Field] - if !ok { - // Same reasons as the comment above. - return config.UnknownVariableValue, nil - } - - return value, nil -} - -func (c *walkContext) computeResourceVariable( - v *config.ResourceVariable) (string, error) { - id := v.ResourceId() - if v.Multi { - id = fmt.Sprintf("%s.%d", id, v.Index) - } - - c.Context.sl.RLock() - defer c.Context.sl.RUnlock() - - // Get the information about this resource variable, and verify - // that it exists and such. - module, _, err := c.resourceVariableInfo(v) - if err != nil { - return "", err - } - - // If we have no module in the state yet or count, return empty - if module == nil || len(module.Resources) == 0 { - return "", nil - } - - // Get the resource out from the state. We know the state exists - // at this point and if there is a state, we expect there to be a - // resource with the given name. - r, ok := module.Resources[id] - if !ok && v.Multi && v.Index == 0 { - r, ok = module.Resources[v.ResourceId()] - } - if !ok { - r = nil - } - if r == nil { - return "", fmt.Errorf( - "Resource '%s' not found for variable '%s'", - id, - v.FullKey()) - } - - if r.Primary == nil { - goto MISSING - } - - if attr, ok := r.Primary.Attributes[v.Field]; ok { - return attr, nil - } - - // At apply time, we can't do the "maybe has it" check below - // that we need for plans since parent elements might be computed. - // Therefore, it is an error and we're missing the key. - // - // TODO: test by creating a state and configuration that is referencing - // a non-existent variable "foo.bar" where the state only has "foo" - // and verify plan works, but apply doesn't. - if c.Operation == walkApply { - goto MISSING - } - - // We didn't find the exact field, so lets separate the dots - // and see if anything along the way is a computed set. i.e. if - // we have "foo.0.bar" as the field, check to see if "foo" is - // a computed list. If so, then the whole thing is computed. - if parts := strings.Split(v.Field, "."); len(parts) > 1 { - for i := 1; i < len(parts); i++ { - // Lists and sets make this - key := fmt.Sprintf("%s.#", strings.Join(parts[:i], ".")) - if attr, ok := r.Primary.Attributes[key]; ok { - return attr, nil - } - - // Maps make this - key = fmt.Sprintf("%s", strings.Join(parts[:i], ".")) - if attr, ok := r.Primary.Attributes[key]; ok { - return attr, nil - } - } - } - -MISSING: - return "", fmt.Errorf( - "Resource '%s' does not have attribute '%s' "+ - "for variable '%s'", - id, - v.Field, - v.FullKey()) -} - -func (c *walkContext) computeResourceMultiVariable( - v *config.ResourceVariable) (string, error) { - c.Context.sl.RLock() - defer c.Context.sl.RUnlock() - - // Get the information about this resource variable, and verify - // that it exists and such. - module, cr, err := c.resourceVariableInfo(v) - if err != nil { - return "", err - } - - // Get the count so we know how many to iterate over - count, err := cr.Count() - if err != nil { - return "", fmt.Errorf( - "Error reading %s count: %s", - v.ResourceId(), - err) - } - - // If we have no module in the state yet or count, return empty - if module == nil || len(module.Resources) == 0 || count == 0 { - return "", nil - } - - var values []string - for i := 0; i < count; i++ { - id := fmt.Sprintf("%s.%d", v.ResourceId(), i) - - // If we're dealing with only a single resource, then the - // ID doesn't have a trailing index. - if count == 1 { - id = v.ResourceId() - } - - r, ok := module.Resources[id] - if !ok { - continue - } - - if r.Primary == nil { - continue - } - - attr, ok := r.Primary.Attributes[v.Field] - if !ok { - continue - } - - values = append(values, attr) - } - - if len(values) == 0 { - return "", fmt.Errorf( - "Resource '%s' does not have attribute '%s' "+ - "for variable '%s'", - v.ResourceId(), - v.Field, - v.FullKey()) - } - - return strings.Join(values, config.InterpSplitDelim), nil -} - -func (c *walkContext) resourceVariableInfo( - v *config.ResourceVariable) (*ModuleState, *config.Resource, error) { - // Get the module tree that contains our current path. This is - // either the current module (path is empty) or a child. - var modTree *module.Tree - childPath := c.Path[1:len(c.Path)] - if len(childPath) == 0 { - modTree = c.Context.module - } else { - modTree = c.Context.module.Child(childPath) - } - - // Get the resource from the configuration so we can verify - // that the resource is in the configuration and so we can access - // the configuration if we need to. - var cr *config.Resource - for _, r := range modTree.Config().Resources { - if r.Id() == v.ResourceId() { - cr = r - break - } - } - if cr == nil { - return nil, nil, fmt.Errorf( - "Resource '%s' not found for variable '%s'", - v.ResourceId(), - v.FullKey()) - } - - // Get the relevant module - module := c.Context.state.ModuleByPath(c.Path) - return module, cr, nil -} - -type walkInputMeta struct { - sync.Mutex - - Done map[string]struct{} -} - -type walkValidateMeta struct { - Errs []error - Warns []string - Children map[string]*walkValidateMeta -} - -func (m *walkValidateMeta) Flatten() ([]string, []error) { - // Prune out the empty children - for k, m2 := range m.Children { - if len(m2.Errs) == 0 && len(m2.Warns) == 0 { - delete(m.Children, k) - } - } - - // If we have no children, then just return what we have - if len(m.Children) == 0 { - return m.Warns, m.Errs - } - - // Otherwise, copy the errors and warnings - errs := make([]error, len(m.Errs)) - warns := make([]string, len(m.Warns)) - for i, err := range m.Errs { - errs[i] = err - } - for i, warn := range m.Warns { - warns[i] = warn - } - - // Now go through each child and copy it in... - for k, c := range m.Children { - for _, err := range c.Errs { - errs = append(errs, fmt.Errorf( - "Module %s: %s", k, err)) - } - for _, warn := range c.Warns { - warns = append(warns, fmt.Sprintf( - "Module %s: %s", k, warn)) - } - } - - return warns, errs + // Walk the graph + log.Printf("[INFO] Starting graph walk: %s", operation.String()) + walker := &ContextGraphWalker{Context: c, Operation: operation} + return walker, graph.Walk(walker) } diff --git a/terraform/context_test.go b/terraform/context_test.go index 6bae0fdca..cb943c117 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -5,39 +5,1828 @@ import ( "fmt" "os" "reflect" - "runtime" "sort" "strings" "sync" "testing" ) -func TestContextGraph(t *testing.T) { +func TestContext2Plan(t *testing.T) { + m := testModule(t, "plan-good") p := testProvider("aws") - m := testModule(t, "validate-good") - c := testContext(t, &ContextOpts{ + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), }, }) - g, err := c.Graph() + plan, err := ctx.Plan(nil) if err != nil { t.Fatalf("err: %s", err) } - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testContextGraph) + if len(plan.Diff.RootModule().Resources) < 2 { + t.Fatalf("bad: %#v", plan.Diff.RootModule().Resources) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanStr) if actual != expected { - t.Fatalf("bad: %s", actual) + t.Fatalf("bad:\n%s", actual) } } -func TestContextValidate(t *testing.T) { +func TestContext2Plan_emptyDiff(t *testing.T) { + m := testModule(t, "plan-empty") + p := testProvider("aws") + p.DiffFn = func( + info *InstanceInfo, + s *InstanceState, + c *ResourceConfig) (*InstanceDiff, error) { + return nil, nil + } + + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanEmptyStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_minimal(t *testing.T) { + m := testModule(t, "plan-empty") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanEmptyStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_modules(t *testing.T) { + m := testModule(t, "plan-modules") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanModulesStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_moduleInput(t *testing.T) { + m := testModule(t, "plan-module-input") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanModuleInputStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_moduleInputComputed(t *testing.T) { + m := testModule(t, "plan-module-input-computed") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanModuleInputComputedStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_moduleInputFromVar(t *testing.T) { + m := testModule(t, "plan-module-input-var") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + Variables: map[string]string{ + "foo": "52", + }, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanModuleInputVarStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_moduleMultiVar(t *testing.T) { + m := testModule(t, "plan-module-multi-var") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanModuleMultiVarStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_moduleOrphans(t *testing.T) { + m := testModule(t, "plan-modules-remove") + p := testProvider("aws") + p.DiffFn = testDiffFn + s := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: []string{"root", "child"}, + Resources: map[string]*ResourceState{ + "aws_instance.foo": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "baz", + }, + }, + }, + }, + }, + } + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: s, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanModuleOrphansStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_moduleProviderInherit(t *testing.T) { + var l sync.Mutex + var calls []string + + m := testModule(t, "plan-module-provider-inherit") + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": func() (ResourceProvider, error) { + l.Lock() + defer l.Unlock() + + p := testProvider("aws") + p.ConfigureFn = func(c *ResourceConfig) error { + if v, ok := c.Get("from"); !ok || v.(string) != "root" { + return fmt.Errorf("bad") + } + + return nil + } + p.DiffFn = func( + info *InstanceInfo, + state *InstanceState, + c *ResourceConfig) (*InstanceDiff, error) { + v, _ := c.Get("from") + calls = append(calls, v.(string)) + return testDiffFn(info, state, c) + } + return p, nil + }, + }, + }) + + _, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := calls + sort.Strings(actual) + expected := []string{"child", "root"} + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) + } +} + +func TestContext2Plan_moduleProviderDefaults(t *testing.T) { + var l sync.Mutex + var calls []string + toCount := 0 + + m := testModule(t, "plan-module-provider-defaults") + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": func() (ResourceProvider, error) { + l.Lock() + defer l.Unlock() + + p := testProvider("aws") + p.ConfigureFn = func(c *ResourceConfig) error { + if v, ok := c.Get("from"); !ok || v.(string) != "root" { + return fmt.Errorf("bad") + } + if v, ok := c.Get("to"); ok && v.(string) == "child" { + toCount++ + } + + return nil + } + p.DiffFn = func( + info *InstanceInfo, + state *InstanceState, + c *ResourceConfig) (*InstanceDiff, error) { + v, _ := c.Get("from") + calls = append(calls, v.(string)) + return testDiffFn(info, state, c) + } + return p, nil + }, + }, + }) + + _, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + if toCount != 1 { + t.Fatalf( + "provider in child didn't set proper config\n\n"+ + "toCount: %d", toCount) + } + + actual := calls + sort.Strings(actual) + expected := []string{"child", "root"} + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) + } +} + +func TestContext2Plan_moduleProviderDefaultsVar(t *testing.T) { + var l sync.Mutex + var calls []string + + m := testModule(t, "plan-module-provider-defaults-var") + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": func() (ResourceProvider, error) { + l.Lock() + defer l.Unlock() + + p := testProvider("aws") + p.ConfigureFn = func(c *ResourceConfig) error { + var buf bytes.Buffer + if v, ok := c.Get("from"); ok { + buf.WriteString(v.(string) + "\n") + } + if v, ok := c.Get("to"); ok { + buf.WriteString(v.(string) + "\n") + } + + calls = append(calls, buf.String()) + return nil + } + p.DiffFn = testDiffFn + return p, nil + }, + }, + Variables: map[string]string{ + "foo": "root", + }, + }) + + _, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + expected := []string{ + "root\n", + "root\nchild\n", + } + if !reflect.DeepEqual(calls, expected) { + t.Fatalf("BAD: %#v", calls) + } +} + +func TestContext2Plan_moduleVar(t *testing.T) { + m := testModule(t, "plan-module-var") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanModuleVarStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_moduleVarComputed(t *testing.T) { + m := testModule(t, "plan-module-var-computed") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanModuleVarComputedStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_nil(t *testing.T) { + m := testModule(t, "plan-nil") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.foo": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "bar", + }, + }, + }, + }, + }, + }, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + if len(plan.Diff.RootModule().Resources) != 0 { + t.Fatalf("bad: %#v", plan.Diff.RootModule().Resources) + } +} + +func TestContext2Plan_computed(t *testing.T) { + m := testModule(t, "plan-computed") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanComputedStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_computedList(t *testing.T) { + m := testModule(t, "plan-computed-list") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanComputedListStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_count(t *testing.T) { + m := testModule(t, "plan-count") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + if len(plan.Diff.RootModule().Resources) < 6 { + t.Fatalf("bad: %#v", plan.Diff.RootModule().Resources) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanCountStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_countComputed(t *testing.T) { + m := testModule(t, "plan-count-computed") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + _, err := ctx.Plan(nil) + if err == nil { + t.Fatal("should error") + } +} + +func TestContext2Plan_countIndex(t *testing.T) { + m := testModule(t, "plan-count-index") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanCountIndexStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_countIndexZero(t *testing.T) { + m := testModule(t, "plan-count-index-zero") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanCountIndexZeroStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_countVar(t *testing.T) { + m := testModule(t, "plan-count-var") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + Variables: map[string]string{ + "count": "3", + }, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanCountVarStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_countZero(t *testing.T) { + m := testModule(t, "plan-count-zero") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanCountZeroStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_countOneIndex(t *testing.T) { + m := testModule(t, "plan-count-one-index") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanCountOneIndexStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_countDecreaseToOne(t *testing.T) { + m := testModule(t, "plan-count-dec") + p := testProvider("aws") + p.DiffFn = testDiffFn + s := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.foo.0": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "bar", + Attributes: map[string]string{ + "foo": "foo", + "type": "aws_instance", + }, + }, + }, + "aws_instance.foo.1": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "bar", + }, + }, + "aws_instance.foo.2": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "bar", + }, + }, + }, + }, + }, + } + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: s, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanCountDecreaseStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_countIncreaseFromNotSet(t *testing.T) { + m := testModule(t, "plan-count-inc") + p := testProvider("aws") + p.DiffFn = testDiffFn + s := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.foo": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "bar", + Attributes: map[string]string{ + "foo": "foo", + "type": "aws_instance", + }, + }, + }, + }, + }, + }, + } + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: s, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanCountIncreaseStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_countIncreaseFromOne(t *testing.T) { + m := testModule(t, "plan-count-inc") + p := testProvider("aws") + p.DiffFn = testDiffFn + s := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.foo.0": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "bar", + Attributes: map[string]string{ + "foo": "foo", + "type": "aws_instance", + }, + }, + }, + }, + }, + }, + } + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: s, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanCountIncreaseFromOneStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_destroy(t *testing.T) { + m := testModule(t, "plan-destroy") + p := testProvider("aws") + p.DiffFn = testDiffFn + s := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.one": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "bar", + }, + }, + "aws_instance.two": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "baz", + }, + }, + }, + }, + }, + } + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: s, + }) + + plan, err := ctx.Plan(&PlanOpts{Destroy: true}) + if err != nil { + t.Fatalf("err: %s", err) + } + + if len(plan.Diff.RootModule().Resources) != 2 { + t.Fatalf("bad: %#v", plan.Diff.RootModule().Resources) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanDestroyStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_moduleDestroy(t *testing.T) { + m := testModule(t, "plan-module-destroy") + p := testProvider("aws") + p.DiffFn = testDiffFn + s := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.foo": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "bar", + }, + }, + }, + }, + &ModuleState{ + Path: []string{"root", "child"}, + Resources: map[string]*ResourceState{ + "aws_instance.foo": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "bar", + }, + }, + }, + }, + }, + } + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: s, + }) + + plan, err := ctx.Plan(&PlanOpts{Destroy: true}) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanModuleDestroyStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_moduleDestroyMultivar(t *testing.T) { + m := testModule(t, "plan-module-destroy-multivar") + p := testProvider("aws") + p.DiffFn = testDiffFn + s := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{}, + }, + &ModuleState{ + Path: []string{"root", "child"}, + Resources: map[string]*ResourceState{ + "aws_instance.foo.0": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "bar0", + }, + }, + "aws_instance.foo.1": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "bar1", + }, + }, + }, + }, + }, + } + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: s, + }) + + plan, err := ctx.Plan(&PlanOpts{Destroy: true}) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanModuleDestroyMultivarStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_pathVar(t *testing.T) { + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("err: %s", err) + } + + m := testModule(t, "plan-path-var") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanPathVarStr) + + // Warning: this ordering REALLY matters for this test. The + // order is: cwd, module, root. + expected = fmt.Sprintf( + expected, + cwd, + m.Config().Dir, + m.Config().Dir) + + if actual != expected { + t.Fatalf("bad:\n%s\n\nexpected:\n\n%s", actual, expected) + } +} + +func TestContext2Plan_diffVar(t *testing.T) { + m := testModule(t, "plan-diffvar") + p := testProvider("aws") + s := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.foo": &ResourceState{ + Primary: &InstanceState{ + ID: "bar", + Attributes: map[string]string{ + "num": "2", + }, + }, + }, + }, + }, + }, + } + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: s, + }) + + p.DiffFn = func( + info *InstanceInfo, + s *InstanceState, + c *ResourceConfig) (*InstanceDiff, error) { + if s.ID != "bar" { + return testDiffFn(info, s, c) + } + + return &InstanceDiff{ + Attributes: map[string]*ResourceAttrDiff{ + "num": &ResourceAttrDiff{ + Old: "2", + New: "3", + }, + }, + }, nil + } + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanDiffVarStr) + if actual != expected { + t.Fatalf("actual:\n%s\n\nexpected:\n%s", actual, expected) + } +} + +func TestContext2Plan_hook(t *testing.T) { + m := testModule(t, "plan-good") + h := new(MockHook) + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Hooks: []Hook{h}, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + _, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + if !h.PreDiffCalled { + t.Fatal("should be called") + } + if !h.PostDiffCalled { + t.Fatal("should be called") + } +} + +func TestContext2Plan_orphan(t *testing.T) { + m := testModule(t, "plan-orphan") + p := testProvider("aws") + p.DiffFn = testDiffFn + s := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.baz": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "bar", + }, + }, + }, + }, + }, + } + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: s, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanOrphanStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_state(t *testing.T) { + m := testModule(t, "plan-good") + p := testProvider("aws") + p.DiffFn = testDiffFn + s := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.foo": &ResourceState{ + Primary: &InstanceState{ + ID: "bar", + }, + }, + }, + }, + }, + } + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: s, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + if len(plan.Diff.RootModule().Resources) < 2 { + t.Fatalf("bad: %#v", plan.Diff.RootModule().Resources) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanStateStr) + if actual != expected { + t.Fatalf("bad:\n%s\n\nexpected:\n\n%s", actual, expected) + } +} + +func TestContext2Plan_taint(t *testing.T) { + m := testModule(t, "plan-taint") + p := testProvider("aws") + p.DiffFn = testDiffFn + s := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.foo": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "bar", + Attributes: map[string]string{"num": "2"}, + }, + }, + "aws_instance.bar": &ResourceState{ + Type: "aws_instance", + Tainted: []*InstanceState{ + &InstanceState{ + ID: "baz", + }, + }, + }, + }, + }, + }, + } + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: s, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanTaintStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_multiple_taint(t *testing.T) { + m := testModule(t, "plan-taint") + p := testProvider("aws") + p.DiffFn = testDiffFn + s := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.foo": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "bar", + Attributes: map[string]string{"num": "2"}, + }, + }, + "aws_instance.bar": &ResourceState{ + Type: "aws_instance", + Tainted: []*InstanceState{ + &InstanceState{ + ID: "baz", + }, + &InstanceState{ + ID: "zip", + }, + }, + }, + }, + }, + }, + } + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: s, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanMultipleTaintStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_provider(t *testing.T) { + m := testModule(t, "plan-provider") + p := testProvider("aws") + p.DiffFn = testDiffFn + + var value interface{} + p.ConfigureFn = func(c *ResourceConfig) error { + value, _ = c.Get("foo") + return nil + } + + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + Variables: map[string]string{ + "foo": "bar", + }, + }) + + if _, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } + + if value != "bar" { + t.Fatalf("bad: %#v", value) + } +} + +func TestContext2Plan_varMultiCountOne(t *testing.T) { + m := testModule(t, "plan-var-multi-count-one") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanVarMultiCountOneStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContext2Plan_varListErr(t *testing.T) { + m := testModule(t, "plan-var-list-err") + p := testProvider("aws") + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + _, err := ctx.Plan(nil) + if err == nil { + t.Fatal("should error") + } +} + +func TestContext2Refresh(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "refresh-basic") + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.web": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + }, + }) + + p.RefreshFn = nil + p.RefreshReturn = &InstanceState{ + ID: "foo", + } + + s, err := ctx.Refresh() + mod := s.RootModule() + if err != nil { + t.Fatalf("err: %s", err) + } + if !p.RefreshCalled { + t.Fatal("refresh should be called") + } + if p.RefreshState.ID != "foo" { + t.Fatalf("bad: %#v", p.RefreshState) + } + if !reflect.DeepEqual(mod.Resources["aws_instance.web"].Primary, p.RefreshReturn) { + t.Fatalf("bad: %#v %#v", mod.Resources["aws_instance.web"], p.RefreshReturn) + } + + for _, r := range mod.Resources { + if r.Type == "" { + t.Fatalf("no type: %#v", r) + } + } +} + +func TestContext2Refresh_delete(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "refresh-basic") + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.web": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + }, + }) + + p.RefreshFn = nil + p.RefreshReturn = nil + + s, err := ctx.Refresh() + if err != nil { + t.Fatalf("err: %s", err) + } + + mod := s.RootModule() + if len(mod.Resources) > 0 { + t.Fatal("resources should be empty") + } +} + +func TestContext2Refresh_ignoreUncreated(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "refresh-basic") + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: nil, + }) + + p.RefreshFn = nil + p.RefreshReturn = &InstanceState{ + ID: "foo", + } + + _, err := ctx.Refresh() + if err != nil { + t.Fatalf("err: %s", err) + } + if p.RefreshCalled { + t.Fatal("refresh should not be called") + } +} + +func TestContext2Refresh_hook(t *testing.T) { + h := new(MockHook) + p := testProvider("aws") + m := testModule(t, "refresh-basic") + ctx := testContext2(t, &ContextOpts{ + Module: m, + Hooks: []Hook{h}, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.web": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + }, + }) + + if _, err := ctx.Refresh(); err != nil { + t.Fatalf("err: %s", err) + } + if !h.PreRefreshCalled { + t.Fatal("should be called") + } + if !h.PostRefreshCalled { + t.Fatal("should be called") + } +} + +func TestContext2Refresh_modules(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "refresh-modules") + state := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.web": &ResourceState{ + Type: "aws_instance", + Tainted: []*InstanceState{ + &InstanceState{ + ID: "bar", + }, + }, + }, + }, + }, + + &ModuleState{ + Path: []string{"root", "child"}, + Resources: map[string]*ResourceState{ + "aws_instance.web": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "baz", + }, + }, + }, + }, + }, + } + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: state, + }) + + p.RefreshFn = func(info *InstanceInfo, s *InstanceState) (*InstanceState, error) { + if s.ID != "baz" { + return s, nil + } + + s.ID = "new" + return s, nil + } + + s, err := ctx.Refresh() + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(s.String()) + expected := strings.TrimSpace(testContextRefreshModuleStr) + if actual != expected { + t.Fatalf("bad:\n\n%s\n\n%s", actual, expected) + } +} + +func TestContext2Refresh_moduleInputComputedOutput(t *testing.T) { + m := testModule(t, "refresh-module-input-computed-output") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + if _, err := ctx.Refresh(); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestContext2Refresh_moduleVarModule(t *testing.T) { + m := testModule(t, "refresh-module-var-module") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + if _, err := ctx.Refresh(); err != nil { + t.Fatalf("err: %s", err) + } +} + +// GH-70 +func TestContext2Refresh_noState(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "refresh-no-state") + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + p.RefreshFn = nil + p.RefreshReturn = &InstanceState{ + ID: "foo", + } + + if _, err := ctx.Refresh(); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestContext2Refresh_outputPartial(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "refresh-output-partial") + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.foo": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + }, + }) + + p.RefreshFn = nil + p.RefreshReturn = nil + + s, err := ctx.Refresh() + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(s.String()) + expected := strings.TrimSpace(testContextRefreshOutputPartialStr) + if actual != expected { + t.Fatalf("bad:\n\n%s\n\n%s", actual, expected) + } +} + +func TestContext2Refresh_state(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "refresh-basic") + state := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.web": &ResourceState{ + Primary: &InstanceState{ + ID: "bar", + }, + }, + }, + }, + }, + } + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: state, + }) + + p.RefreshFn = nil + p.RefreshReturn = &InstanceState{ + ID: "foo", + } + + s, err := ctx.Refresh() + if err != nil { + t.Fatalf("err: %s", err) + } + originalMod := state.RootModule() + mod := s.RootModule() + if !p.RefreshCalled { + t.Fatal("refresh should be called") + } + if !reflect.DeepEqual(p.RefreshState, originalMod.Resources["aws_instance.web"].Primary) { + t.Fatalf( + "bad:\n\n%#v\n\n%#v", + p.RefreshState, + originalMod.Resources["aws_instance.web"].Primary) + } + if !reflect.DeepEqual(mod.Resources["aws_instance.web"].Primary, p.RefreshReturn) { + t.Fatalf("bad: %#v", mod.Resources) + } +} + +func TestContext2Refresh_tainted(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "refresh-basic") + state := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.web": &ResourceState{ + Type: "aws_instance", + Tainted: []*InstanceState{ + &InstanceState{ + ID: "bar", + }, + }, + }, + }, + }, + }, + } + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: state, + }) + + p.RefreshFn = nil + p.RefreshReturn = &InstanceState{ + ID: "foo", + } + + s, err := ctx.Refresh() + if err != nil { + t.Fatalf("err: %s", err) + } + if !p.RefreshCalled { + t.Fatal("refresh should be called") + } + + actual := strings.TrimSpace(s.String()) + expected := strings.TrimSpace(testContextRefreshTaintedStr) + if actual != expected { + t.Fatalf("bad:\n\n%s\n\n%s", actual, expected) + } +} + +// Doing a Refresh (or any operation really, but Refresh usually +// happens first) with a config with an unknown provider should result in +// an error. The key bug this found was that this wasn't happening if +// Providers was _empty_. +func TestContext2Refresh_unknownProvider(t *testing.T) { + m := testModule(t, "refresh-unknown-provider") + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{}, + }) + + if _, err := ctx.Refresh(); err == nil { + t.Fatal("should error") + } +} + +func TestContext2Refresh_vars(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "refresh-vars") + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: &State{ + + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.web": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + }, + }) + + p.RefreshFn = nil + p.RefreshReturn = &InstanceState{ + ID: "foo", + } + + s, err := ctx.Refresh() + if err != nil { + t.Fatalf("err: %s", err) + } + mod := s.RootModule() + if !p.RefreshCalled { + t.Fatal("refresh should be called") + } + if p.RefreshState.ID != "foo" { + t.Fatalf("bad: %#v", p.RefreshState) + } + if !reflect.DeepEqual(mod.Resources["aws_instance.web"].Primary, p.RefreshReturn) { + t.Fatalf("bad: %#v", mod.Resources["aws_instance.web"]) + } + + for _, r := range mod.Resources { + if r.Type == "" { + t.Fatalf("no type: %#v", r) + } + } +} + +func TestContext2Validate(t *testing.T) { p := testProvider("aws") m := testModule(t, "validate-good") - c := testContext(t, &ContextOpts{ + c := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -49,52 +1838,14 @@ func TestContextValidate(t *testing.T) { t.Fatalf("bad: %#v", w) } if len(e) > 0 { - t.Fatalf("bad: %#v", e) + t.Fatalf("bad: %s", e) } } -func TestContextValidate_goodModule(t *testing.T) { - p := testProvider("aws") - m := testModule(t, "validate-good-module") - c := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - w, e := c.Validate() - if len(w) > 0 { - t.Fatalf("bad: %#v", w) - } - if len(e) > 0 { - t.Fatalf("bad: %#v", e) - } -} - -func TestContextValidate_badModuleOutput(t *testing.T) { - p := testProvider("aws") - m := testModule(t, "validate-bad-module-output") - c := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - w, e := c.Validate() - if len(w) > 0 { - t.Fatalf("bad: %#v", w) - } - if len(e) == 0 { - t.Fatalf("bad: %#v", e) - } -} - -func TestContextValidate_badVar(t *testing.T) { +func TestContext2Validate_badVar(t *testing.T) { p := testProvider("aws") m := testModule(t, "validate-bad-var") - c := testContext(t, &ContextOpts{ + c := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -110,10 +1861,10 @@ func TestContextValidate_badVar(t *testing.T) { } } -func TestContextValidate_countNegative(t *testing.T) { +func TestContext2Validate_countNegative(t *testing.T) { p := testProvider("aws") m := testModule(t, "validate-count-negative") - c := testContext(t, &ContextOpts{ + c := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -129,10 +1880,10 @@ func TestContextValidate_countNegative(t *testing.T) { } } -func TestContextValidate_countVariable(t *testing.T) { +func TestContext2Validate_countVariable(t *testing.T) { p := testProvider("aws") m := testModule(t, "apply-count-variable") - c := testContext(t, &ContextOpts{ + c := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -144,17 +1895,14 @@ func TestContextValidate_countVariable(t *testing.T) { t.Fatalf("bad: %#v", w) } if len(e) > 0 { - for _, err := range e { - t.Errorf("bad: %s", err) - } - t.FailNow() + t.Fatalf("bad: %s", e) } } -func TestContextValidate_countVariableNoDefault(t *testing.T) { +func TestContext2Validate_countVariableNoDefault(t *testing.T) { p := testProvider("aws") m := testModule(t, "validate-count-variable") - c := testContext(t, &ContextOpts{ + c := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -165,18 +1913,53 @@ func TestContextValidate_countVariableNoDefault(t *testing.T) { if len(w) > 0 { t.Fatalf("bad: %#v", w) } - if len(e) > 1 { - for _, err := range e { - t.Errorf("bad: %s", err) - } - t.FailNow() + if len(e) != 1 { + t.Fatalf("bad: %s", e) } } -func TestContextValidate_moduleBadResource(t *testing.T) { +func TestContext2Validate_moduleBadOutput(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "validate-bad-module-output") + c := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + w, e := c.Validate() + if len(w) > 0 { + t.Fatalf("bad: %#v", w) + } + if len(e) == 0 { + t.Fatalf("bad: %s", e) + } +} + +func TestContext2Validate_moduleGood(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "validate-good-module") + c := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + w, e := c.Validate() + if len(w) > 0 { + t.Fatalf("bad: %#v", w) + } + if len(e) > 0 { + t.Fatalf("bad: %#v", e) + } +} + +func TestContext2Validate_moduleBadResource(t *testing.T) { m := testModule(t, "validate-module-bad-rc") p := testProvider("aws") - c := testContext(t, &ContextOpts{ + c := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -194,10 +1977,10 @@ func TestContextValidate_moduleBadResource(t *testing.T) { } } -func TestContextValidate_moduleProviderInherit(t *testing.T) { +func TestContext2Validate_moduleProviderInherit(t *testing.T) { m := testModule(t, "validate-module-pc-inherit") p := testProvider("aws") - c := testContext(t, &ContextOpts{ + c := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -213,11 +1996,11 @@ func TestContextValidate_moduleProviderInherit(t *testing.T) { t.Fatalf("bad: %#v", w) } if len(e) > 0 { - t.Fatalf("bad: %#v", e) + t.Fatalf("bad: %s", e) } } -func TestContextValidate_orphans(t *testing.T) { +func TestContext2Validate_orphans(t *testing.T) { p := testProvider("aws") m := testModule(t, "validate-good") state := &State{ @@ -235,7 +2018,7 @@ func TestContextValidate_orphans(t *testing.T) { }, }, } - c := testContext(t, &ContextOpts{ + c := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -253,56 +2036,14 @@ func TestContextValidate_orphans(t *testing.T) { t.Fatalf("bad: %#v", w) } if len(e) > 0 { - t.Fatalf("bad: %#v", e) + t.Fatalf("bad: %s", e) } } -func TestContextValidate_tainted(t *testing.T) { - p := testProvider("aws") - m := testModule(t, "validate-good") - state := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.foo": &ResourceState{ - Type: "aws_instance", - Tainted: []*InstanceState{ - &InstanceState{ - ID: "bar", - }, - }, - }, - }, - }, - }, - } - c := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - State: state, - }) - - p.ValidateResourceFn = func( - t string, c *ResourceConfig) ([]string, []error) { - return nil, c.CheckSet([]string{"foo"}) - } - - w, e := c.Validate() - if len(w) > 0 { - t.Fatalf("bad: %#v", w) - } - if len(e) > 0 { - t.Fatalf("bad: %#v", e) - } -} - -func TestContextValidate_providerConfig_bad(t *testing.T) { +func TestContext2Validate_providerConfig_bad(t *testing.T) { m := testModule(t, "validate-bad-pc") p := testProvider("aws") - c := testContext(t, &ContextOpts{ + c := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -316,14 +2057,17 @@ func TestContextValidate_providerConfig_bad(t *testing.T) { t.Fatalf("bad: %#v", w) } if len(e) == 0 { - t.Fatalf("bad: %#v", e) + t.Fatalf("bad: %s", e) + } + if !strings.Contains(fmt.Sprintf("%s", e), "bad") { + t.Fatalf("bad: %s", e) } } -func TestContextValidate_providerConfig_badEmpty(t *testing.T) { +func TestContext2Validate_providerConfig_badEmpty(t *testing.T) { m := testModule(t, "validate-bad-pc-empty") p := testProvider("aws") - c := testContext(t, &ContextOpts{ + c := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -341,10 +2085,10 @@ func TestContextValidate_providerConfig_badEmpty(t *testing.T) { } } -func TestContextValidate_providerConfig_good(t *testing.T) { +func TestContext2Validate_providerConfig_good(t *testing.T) { m := testModule(t, "validate-bad-pc") p := testProvider("aws") - c := testContext(t, &ContextOpts{ + c := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -360,85 +2104,11 @@ func TestContextValidate_providerConfig_good(t *testing.T) { } } -func TestContextValidate_resourceConfig_bad(t *testing.T) { - m := testModule(t, "validate-bad-rc") - p := testProvider("aws") - c := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - p.ValidateResourceReturnErrors = []error{fmt.Errorf("bad")} - - w, e := c.Validate() - if len(w) > 0 { - t.Fatalf("bad: %#v", w) - } - if len(e) == 0 { - t.Fatalf("bad: %#v", e) - } -} - -func TestContextValidate_resourceConfig_good(t *testing.T) { - m := testModule(t, "validate-bad-rc") - p := testProvider("aws") - c := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - w, e := c.Validate() - if len(w) > 0 { - t.Fatalf("bad: %#v", w) - } - if len(e) > 0 { - t.Fatalf("bad: %#v", e) - } -} - -func TestContextValidate_resourceNameSymbol(t *testing.T) { - p := testProvider("aws") - m := testModule(t, "validate-resource-name-symbol") - c := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - w, e := c.Validate() - if len(w) == 0 { - t.Fatalf("bad: %#v", w) - } - if len(e) > 0 { - t.Fatalf("bad: %#v", e) - } -} - -func TestContextValidate_requiredVar(t *testing.T) { - m := testModule(t, "validate-required-var") - c := testContext(t, &ContextOpts{ - Module: m, - }) - - w, e := c.Validate() - if len(w) > 0 { - t.Fatalf("bad: %#v", w) - } - if len(e) == 0 { - t.Fatalf("bad: %#v", e) - } -} - -func TestContextValidate_provisionerConfig_bad(t *testing.T) { +func TestContext2Validate_provisionerConfig_bad(t *testing.T) { m := testModule(t, "validate-bad-prov-conf") p := testProvider("aws") pr := testProvisioner() - c := testContext(t, &ContextOpts{ + c := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -459,7 +2129,7 @@ func TestContextValidate_provisionerConfig_bad(t *testing.T) { } } -func TestContextValidate_provisionerConfig_good(t *testing.T) { +func TestContext2Validate_provisionerConfig_good(t *testing.T) { m := testModule(t, "validate-bad-prov-conf") p := testProvider("aws") pr := testProvisioner() @@ -467,9 +2137,9 @@ func TestContextValidate_provisionerConfig_good(t *testing.T) { if c == nil { t.Fatalf("missing resource config for provisioner") } - return nil, nil + return nil, c.CheckSet([]string{"command"}) } - c := testContext(t, &ContextOpts{ + c := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -488,10 +2158,88 @@ func TestContextValidate_provisionerConfig_good(t *testing.T) { } } -func TestContextValidate_selfRef(t *testing.T) { +func TestContext2Validate_requiredVar(t *testing.T) { + m := testModule(t, "validate-required-var") + p := testProvider("aws") + c := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + w, e := c.Validate() + if len(w) > 0 { + t.Fatalf("bad: %#v", w) + } + if len(e) == 0 { + t.Fatalf("bad: %s", e) + } +} + +func TestContext2Validate_resourceConfig_bad(t *testing.T) { + m := testModule(t, "validate-bad-rc") + p := testProvider("aws") + c := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + p.ValidateResourceReturnErrors = []error{fmt.Errorf("bad")} + + w, e := c.Validate() + if len(w) > 0 { + t.Fatalf("bad: %#v", w) + } + if len(e) == 0 { + t.Fatalf("bad: %s", e) + } +} + +func TestContext2Validate_resourceConfig_good(t *testing.T) { + m := testModule(t, "validate-bad-rc") + p := testProvider("aws") + c := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + w, e := c.Validate() + if len(w) > 0 { + t.Fatalf("bad: %#v", w) + } + if len(e) > 0 { + t.Fatalf("bad: %#v", e) + } +} + +func TestContext2Validate_resourceNameSymbol(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "validate-resource-name-symbol") + c := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + w, e := c.Validate() + if len(w) == 0 { + t.Fatalf("bad: %#v", w) + } + if len(e) > 0 { + t.Fatalf("bad: %s", e) + } +} + +func TestContext2Validate_selfRef(t *testing.T) { p := testProvider("aws") m := testModule(t, "validate-self-ref") - c := testContext(t, &ContextOpts{ + c := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -503,14 +2251,14 @@ func TestContextValidate_selfRef(t *testing.T) { t.Fatalf("bad: %#v", w) } if len(e) == 0 { - t.Fatalf("bad: %#v", e) + t.Fatalf("bad: %s", e) } } -func TestContextValidate_selfRefMulti(t *testing.T) { +func TestContext2Validate_selfRefMulti(t *testing.T) { p := testProvider("aws") m := testModule(t, "validate-self-ref-multi") - c := testContext(t, &ContextOpts{ + c := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -526,10 +2274,10 @@ func TestContextValidate_selfRefMulti(t *testing.T) { } } -func TestContextValidate_selfRefMultiAll(t *testing.T) { +func TestContext2Validate_selfRefMultiAll(t *testing.T) { p := testProvider("aws") m := testModule(t, "validate-self-ref-multi-all") - c := testContext(t, &ContextOpts{ + c := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -545,10 +2293,52 @@ func TestContextValidate_selfRefMultiAll(t *testing.T) { } } -func TestContextValidate_varRef(t *testing.T) { +func TestContext2Validate_tainted(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "validate-good") + state := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.foo": &ResourceState{ + Type: "aws_instance", + Tainted: []*InstanceState{ + &InstanceState{ + ID: "bar", + }, + }, + }, + }, + }, + }, + } + c := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: state, + }) + + p.ValidateResourceFn = func( + t string, c *ResourceConfig) ([]string, []error) { + return nil, c.CheckSet([]string{"foo"}) + } + + w, e := c.Validate() + if len(w) > 0 { + t.Fatalf("bad: %#v", w) + } + if len(e) > 0 { + t.Fatalf("bad: %#v", e) + } +} + +func TestContext2Validate_varRef(t *testing.T) { m := testModule(t, "validate-variable-ref") p := testProvider("aws") - c := testContext(t, &ContextOpts{ + c := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -567,10 +2357,10 @@ func TestContextValidate_varRef(t *testing.T) { } } -func TestContextValidate_varRefFilled(t *testing.T) { +func TestContext2Validate_varRefFilled(t *testing.T) { m := testModule(t, "validate-variable-ref") p := testProvider("aws") - c := testContext(t, &ContextOpts{ + c := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -592,13 +2382,13 @@ func TestContextValidate_varRefFilled(t *testing.T) { } } -func TestContextInput(t *testing.T) { +func TestContext2Input(t *testing.T) { input := new(MockUIInput) m := testModule(t, "input-vars") p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -634,12 +2424,12 @@ func TestContextInput(t *testing.T) { } } -func TestContextInput_provider(t *testing.T) { +func TestContext2Input_provider(t *testing.T) { m := testModule(t, "input-provider") p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -673,13 +2463,13 @@ func TestContextInput_provider(t *testing.T) { } } -func TestContextInput_providerId(t *testing.T) { +func TestContext2Input_providerId(t *testing.T) { input := new(MockUIInput) m := testModule(t, "input-provider") p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -723,13 +2513,13 @@ func TestContextInput_providerId(t *testing.T) { } } -func TestContextInput_providerOnly(t *testing.T) { +func TestContext2Input_providerOnly(t *testing.T) { input := new(MockUIInput) m := testModule(t, "input-provider-vars") p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -778,13 +2568,13 @@ func TestContextInput_providerOnly(t *testing.T) { } } -func TestContextInput_providerVars(t *testing.T) { +func TestContext2Input_providerVars(t *testing.T) { input := new(MockUIInput) m := testModule(t, "input-provider-with-vars") p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -826,13 +2616,13 @@ func TestContextInput_providerVars(t *testing.T) { } } -func TestContextInput_varOnly(t *testing.T) { +func TestContext2Input_varOnly(t *testing.T) { input := new(MockUIInput) m := testModule(t, "input-provider-vars") p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -881,12 +2671,12 @@ func TestContextInput_varOnly(t *testing.T) { } } -func TestContextApply(t *testing.T) { +func TestContext2Apply(t *testing.T) { m := testModule(t, "apply-good") p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -914,12 +2704,12 @@ func TestContextApply(t *testing.T) { } } -func TestContextApply_emptyModule(t *testing.T) { +func TestContext2Apply_emptyModule(t *testing.T) { m := testModule(t, "apply-empty-module") p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -943,7 +2733,7 @@ func TestContextApply_emptyModule(t *testing.T) { } } -func TestContextApply_createBeforeDestroy(t *testing.T) { +func TestContext2Apply_createBeforeDestroy(t *testing.T) { m := testModule(t, "apply-good-create-before") p := testProvider("aws") p.ApplyFn = testApplyFn @@ -966,7 +2756,7 @@ func TestContextApply_createBeforeDestroy(t *testing.T) { }, }, } - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -974,8 +2764,10 @@ func TestContextApply_createBeforeDestroy(t *testing.T) { State: state, }) - if _, err := ctx.Plan(nil); err != nil { + if p, err := ctx.Plan(nil); err != nil { t.Fatalf("err: %s", err) + } else { + t.Logf(p.String()) } state, err := ctx.Apply() @@ -985,7 +2777,7 @@ func TestContextApply_createBeforeDestroy(t *testing.T) { mod := state.RootModule() if len(mod.Resources) != 1 { - t.Fatalf("bad: %#v", mod.Resources) + t.Fatalf("bad: %s", state) } actual := strings.TrimSpace(state.String()) @@ -995,12 +2787,66 @@ func TestContextApply_createBeforeDestroy(t *testing.T) { } } -func TestContextApply_Minimal(t *testing.T) { +func TestContext2Apply_createBeforeDestroyUpdate(t *testing.T) { + m := testModule(t, "apply-good-create-before-update") + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + state := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.bar": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "bar", + Attributes: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + }, + }, + } + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: state, + }) + + if p, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } else { + t.Logf(p.String()) + } + + state, err := ctx.Apply() + if err != nil { + t.Fatalf("err: %s", err) + } + + mod := state.RootModule() + if len(mod.Resources) != 1 { + t.Fatalf("bad: %s", state) + } + + actual := strings.TrimSpace(state.String()) + expected := strings.TrimSpace(testTerraformApplyCreateBeforeUpdateStr) + if actual != expected { + t.Fatalf("bad: \n%s", actual) + } +} + +func TestContext2Apply_minimal(t *testing.T) { m := testModule(t, "apply-minimal") p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -1023,12 +2869,12 @@ func TestContextApply_Minimal(t *testing.T) { } } -func TestContextApply_badDiff(t *testing.T) { +func TestContext2Apply_badDiff(t *testing.T) { m := testModule(t, "apply-good") p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -1052,12 +2898,12 @@ func TestContextApply_badDiff(t *testing.T) { } } -func TestContextApply_cancel(t *testing.T) { +func TestContext2Apply_cancel(t *testing.T) { stopped := false m := testModule(t, "apply-cancel") p := testProvider("aws") - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -1112,7 +2958,7 @@ func TestContextApply_cancel(t *testing.T) { mod := state.RootModule() if len(mod.Resources) != 1 { - t.Fatalf("bad: %#v", mod.Resources) + t.Fatalf("bad: %s", state.String()) } actual := strings.TrimSpace(state.String()) @@ -1122,12 +2968,12 @@ func TestContextApply_cancel(t *testing.T) { } } -func TestContextApply_compute(t *testing.T) { +func TestContext2Apply_compute(t *testing.T) { m := testModule(t, "apply-compute") p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -1152,7 +2998,7 @@ func TestContextApply_compute(t *testing.T) { } } -func TestContextApply_countDecrease(t *testing.T) { +func TestContext2Apply_countDecrease(t *testing.T) { m := testModule(t, "apply-count-dec") p := testProvider("aws") p.DiffFn = testDiffFn @@ -1195,7 +3041,7 @@ func TestContextApply_countDecrease(t *testing.T) { }, }, } - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -1219,7 +3065,7 @@ func TestContextApply_countDecrease(t *testing.T) { } } -func TestContextApply_countDecreaseToOne(t *testing.T) { +func TestContext2Apply_countDecreaseToOne(t *testing.T) { m := testModule(t, "apply-count-dec-one") p := testProvider("aws") p.DiffFn = testDiffFn @@ -1254,7 +3100,7 @@ func TestContextApply_countDecreaseToOne(t *testing.T) { }, }, } - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -1278,7 +3124,7 @@ func TestContextApply_countDecreaseToOne(t *testing.T) { } } -func TestContextApply_countTainted(t *testing.T) { +func TestContext2Apply_countTainted(t *testing.T) { m := testModule(t, "apply-count-tainted") p := testProvider("aws") p.DiffFn = testDiffFn @@ -1303,7 +3149,7 @@ func TestContextApply_countTainted(t *testing.T) { }, }, } - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -1327,12 +3173,12 @@ func TestContextApply_countTainted(t *testing.T) { } } -func TestContextApply_countVariable(t *testing.T) { +func TestContext2Apply_countVariable(t *testing.T) { m := testModule(t, "apply-count-variable") p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -1355,12 +3201,12 @@ func TestContextApply_countVariable(t *testing.T) { } } -func TestContextApply_module(t *testing.T) { +func TestContext2Apply_module(t *testing.T) { m := testModule(t, "apply-module") p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -1383,12 +3229,51 @@ func TestContextApply_module(t *testing.T) { } } -func TestContextApply_nilDiff(t *testing.T) { +func TestContext2Apply_multiProvider(t *testing.T) { + m := testModule(t, "apply-multi-provider") + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + + pDO := testProvider("do") + pDO.ApplyFn = testApplyFn + pDO.DiffFn = testDiffFn + + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + "do": testProviderFuncFixed(pDO), + }, + }) + + if _, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } + + state, err := ctx.Apply() + if err != nil { + t.Fatalf("err: %s", err) + } + + mod := state.RootModule() + if len(mod.Resources) < 2 { + t.Fatalf("bad: %#v", mod.Resources) + } + + actual := strings.TrimSpace(state.String()) + expected := strings.TrimSpace(testTerraformApplyMultiProviderStr) + if actual != expected { + t.Fatalf("bad: \n%s", actual) + } +} + +func TestContext2Apply_nilDiff(t *testing.T) { m := testModule(t, "apply-good") p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -1408,7 +3293,7 @@ func TestContextApply_nilDiff(t *testing.T) { } } -func TestContextApply_Provisioner_compute(t *testing.T) { +func TestContext2Apply_Provisioner_compute(t *testing.T) { m := testModule(t, "apply-provisioner-compute") p := testProvider("aws") pr := testProvisioner() @@ -1422,7 +3307,7 @@ func TestContextApply_Provisioner_compute(t *testing.T) { return nil } - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -1456,7 +3341,7 @@ func TestContextApply_Provisioner_compute(t *testing.T) { } } -func TestContextApply_provisionerCreateFail(t *testing.T) { +func TestContext2Apply_provisionerCreateFail(t *testing.T) { m := testModule(t, "apply-provisioner-fail-create") p := testProvider("aws") pr := testProvisioner() @@ -1470,7 +3355,7 @@ func TestContextApply_provisionerCreateFail(t *testing.T) { return is, fmt.Errorf("error") } - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -1496,7 +3381,7 @@ func TestContextApply_provisionerCreateFail(t *testing.T) { } } -func TestContextApply_provisionerCreateFailNoId(t *testing.T) { +func TestContext2Apply_provisionerCreateFailNoId(t *testing.T) { m := testModule(t, "apply-provisioner-fail-create") p := testProvider("aws") pr := testProvisioner() @@ -1509,7 +3394,7 @@ func TestContextApply_provisionerCreateFailNoId(t *testing.T) { return nil, fmt.Errorf("error") } - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -1535,7 +3420,7 @@ func TestContextApply_provisionerCreateFailNoId(t *testing.T) { } } -func TestContextApply_provisionerFail(t *testing.T) { +func TestContext2Apply_provisionerFail(t *testing.T) { m := testModule(t, "apply-provisioner-fail") p := testProvider("aws") pr := testProvisioner() @@ -1546,7 +3431,7 @@ func TestContextApply_provisionerFail(t *testing.T) { return fmt.Errorf("EXPLOSION") } - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -1575,7 +3460,7 @@ func TestContextApply_provisionerFail(t *testing.T) { } } -func TestContextApply_provisionerFail_createBeforeDestroy(t *testing.T) { +func TestContext2Apply_provisionerFail_createBeforeDestroy(t *testing.T) { m := testModule(t, "apply-provisioner-fail-create-before") p := testProvider("aws") pr := testProvisioner() @@ -1603,7 +3488,7 @@ func TestContextApply_provisionerFail_createBeforeDestroy(t *testing.T) { }, }, } - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -1630,7 +3515,7 @@ func TestContextApply_provisionerFail_createBeforeDestroy(t *testing.T) { } } -func TestContextApply_error_createBeforeDestroy(t *testing.T) { +func TestContext2Apply_error_createBeforeDestroy(t *testing.T) { m := testModule(t, "apply-error-create-before") p := testProvider("aws") state := &State{ @@ -1651,7 +3536,7 @@ func TestContextApply_error_createBeforeDestroy(t *testing.T) { }, }, } - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -1675,11 +3560,11 @@ func TestContextApply_error_createBeforeDestroy(t *testing.T) { actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyErrorCreateBeforeDestroyStr) if actual != expected { - t.Fatalf("bad: \n%s\n\n\n%s", actual, expected) + t.Fatalf("bad: \n%s\n\nExpected:\n\n%s", actual, expected) } } -func TestContextApply_errorDestroy_createBeforeDestroy(t *testing.T) { +func TestContext2Apply_errorDestroy_createBeforeDestroy(t *testing.T) { m := testModule(t, "apply-error-create-before") p := testProvider("aws") state := &State{ @@ -1700,7 +3585,7 @@ func TestContextApply_errorDestroy_createBeforeDestroy(t *testing.T) { }, }, } - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -1737,7 +3622,7 @@ func TestContextApply_errorDestroy_createBeforeDestroy(t *testing.T) { } } -func TestContextApply_provisionerResourceRef(t *testing.T) { +func TestContext2Apply_provisionerResourceRef(t *testing.T) { m := testModule(t, "apply-provisioner-resource-ref") p := testProvider("aws") pr := testProvisioner() @@ -1752,7 +3637,7 @@ func TestContextApply_provisionerResourceRef(t *testing.T) { return nil } - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -1784,7 +3669,7 @@ func TestContextApply_provisionerResourceRef(t *testing.T) { } // Provisioner should NOT run on a diff, only create -func TestContextApply_Provisioner_Diff(t *testing.T) { +func TestContext2Apply_Provisioner_Diff(t *testing.T) { m := testModule(t, "apply-provisioner-diff") p := testProvider("aws") pr := testProvisioner() @@ -1793,7 +3678,7 @@ func TestContextApply_Provisioner_Diff(t *testing.T) { pr.ApplyFn = func(rs *InstanceState, c *ResourceConfig) error { return nil } - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -1829,7 +3714,7 @@ func TestContextApply_Provisioner_Diff(t *testing.T) { mod.Resources["aws_instance.bar"].Primary.Attributes["foo"] = "baz" // Re-create context with state - ctx = testContext(t, &ContextOpts{ + ctx = testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -1860,7 +3745,7 @@ func TestContextApply_Provisioner_Diff(t *testing.T) { } } -func TestContextApply_outputDiffVars(t *testing.T) { +func TestContext2Apply_outputDiffVars(t *testing.T) { m := testModule(t, "apply-good") p := testProvider("aws") s := &State{ @@ -1878,7 +3763,7 @@ func TestContextApply_outputDiffVars(t *testing.T) { }, }, } - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -1919,7 +3804,7 @@ func TestContextApply_outputDiffVars(t *testing.T) { } } -func TestContextApply_Provisioner_ConnInfo(t *testing.T) { +func TestContext2Apply_Provisioner_ConnInfo(t *testing.T) { m := testModule(t, "apply-provisioner-conninfo") p := testProvider("aws") pr := testProvisioner() @@ -1960,7 +3845,7 @@ func TestContextApply_Provisioner_ConnInfo(t *testing.T) { return nil } - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -1995,13 +3880,13 @@ func TestContextApply_Provisioner_ConnInfo(t *testing.T) { } } -func TestContextApply_destroy(t *testing.T) { +func TestContext2Apply_destroy(t *testing.T) { m := testModule(t, "apply-destroy") h := new(HookRecordApplyOrder) p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Hooks: []Hook{h}, Providers: map[string]ResourceProviderFactory{ @@ -2045,13 +3930,13 @@ func TestContextApply_destroy(t *testing.T) { } } -func TestContextApply_destroyOutputs(t *testing.T) { +func TestContext2Apply_destroyOutputs(t *testing.T) { m := testModule(t, "apply-destroy-outputs") h := new(HookRecordApplyOrder) p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Hooks: []Hook{h}, Providers: map[string]ResourceProviderFactory{ @@ -2086,7 +3971,7 @@ func TestContextApply_destroyOutputs(t *testing.T) { } } -func TestContextApply_destroyOrphan(t *testing.T) { +func TestContext2Apply_destroyOrphan(t *testing.T) { m := testModule(t, "apply-error") p := testProvider("aws") s := &State{ @@ -2104,7 +3989,7 @@ func TestContextApply_destroyOrphan(t *testing.T) { }, }, } - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -2146,7 +4031,7 @@ func TestContextApply_destroyOrphan(t *testing.T) { } } -func TestContextApply_destroyTaintedProvisioner(t *testing.T) { +func TestContext2Apply_destroyTaintedProvisioner(t *testing.T) { m := testModule(t, "apply-destroy-provisioner") p := testProvider("aws") pr := testProvisioner() @@ -2180,7 +4065,7 @@ func TestContextApply_destroyTaintedProvisioner(t *testing.T) { }, } - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -2211,12 +4096,12 @@ func TestContextApply_destroyTaintedProvisioner(t *testing.T) { } } -func TestContextApply_error(t *testing.T) { +func TestContext2Apply_error(t *testing.T) { errored := false m := testModule(t, "apply-error") p := testProvider("aws") - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -2265,7 +4150,7 @@ func TestContextApply_error(t *testing.T) { } } -func TestContextApply_errorPartial(t *testing.T) { +func TestContext2Apply_errorPartial(t *testing.T) { errored := false m := testModule(t, "apply-error") @@ -2285,7 +4170,7 @@ func TestContextApply_errorPartial(t *testing.T) { }, }, } - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -2337,13 +4222,13 @@ func TestContextApply_errorPartial(t *testing.T) { } } -func TestContextApply_hook(t *testing.T) { +func TestContext2Apply_hook(t *testing.T) { m := testModule(t, "apply-good") h := new(MockHook) p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Hooks: []Hook{h}, Providers: map[string]ResourceProviderFactory{ @@ -2367,10 +4252,10 @@ func TestContextApply_hook(t *testing.T) { } } -func TestContextApply_idAttr(t *testing.T) { +func TestContext2Apply_idAttr(t *testing.T) { m := testModule(t, "apply-idattr") p := testProvider("aws") - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -2418,12 +4303,12 @@ func TestContextApply_idAttr(t *testing.T) { } } -func TestContextApply_output(t *testing.T) { +func TestContext2Apply_output(t *testing.T) { m := testModule(t, "apply-output") p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -2446,12 +4331,12 @@ func TestContextApply_output(t *testing.T) { } } -func TestContextApply_outputInvalid(t *testing.T) { +func TestContext2Apply_outputInvalid(t *testing.T) { m := testModule(t, "apply-output-invalid") p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -2467,12 +4352,12 @@ func TestContextApply_outputInvalid(t *testing.T) { } } -func TestContextApply_outputList(t *testing.T) { +func TestContext2Apply_outputList(t *testing.T) { m := testModule(t, "apply-output-list") p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -2495,12 +4380,12 @@ func TestContextApply_outputList(t *testing.T) { } } -func TestContextApply_outputMulti(t *testing.T) { +func TestContext2Apply_outputMulti(t *testing.T) { m := testModule(t, "apply-output-multi") p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -2523,12 +4408,12 @@ func TestContextApply_outputMulti(t *testing.T) { } } -func TestContextApply_outputMultiIndex(t *testing.T) { +func TestContext2Apply_outputMultiIndex(t *testing.T) { m := testModule(t, "apply-output-multi-index") p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -2551,7 +4436,7 @@ func TestContextApply_outputMultiIndex(t *testing.T) { } } -func TestContextApply_taint(t *testing.T) { +func TestContext2Apply_taint(t *testing.T) { m := testModule(t, "apply-taint") p := testProvider("aws") p.ApplyFn = testApplyFn @@ -2577,7 +4462,7 @@ func TestContextApply_taint(t *testing.T) { }, }, } - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -2601,12 +4486,12 @@ func TestContextApply_taint(t *testing.T) { } } -func TestContextApply_unknownAttribute(t *testing.T) { +func TestContext2Apply_unknownAttribute(t *testing.T) { m := testModule(t, "apply-unknown") p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -2629,12 +4514,12 @@ func TestContextApply_unknownAttribute(t *testing.T) { } } -func TestContextApply_vars(t *testing.T) { +func TestContext2Apply_vars(t *testing.T) { m := testModule(t, "apply-vars") p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -2669,7 +4554,7 @@ func TestContextApply_vars(t *testing.T) { } } -func TestContextApply_createBefore_depends(t *testing.T) { +func TestContext2Apply_createBefore_depends(t *testing.T) { m := testModule(t, "apply-depends-create-before") h := new(HookRecordApplyOrder) p := testProvider("aws") @@ -2702,7 +4587,7 @@ func TestContextApply_createBefore_depends(t *testing.T) { }, }, } - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Hooks: []Hook{h}, Providers: map[string]ResourceProviderFactory{ @@ -2748,7 +4633,7 @@ func TestContextApply_createBefore_depends(t *testing.T) { } } -func TestContextApply_singleDestroy(t *testing.T) { +func TestContext2Apply_singleDestroy(t *testing.T) { m := testModule(t, "apply-depends-create-before") h := new(HookRecordApplyOrder) p := testProvider("aws") @@ -2811,7 +4696,7 @@ func TestContextApply_singleDestroy(t *testing.T) { }, }, } - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Hooks: []Hook{h}, Providers: map[string]ResourceProviderFactory{ @@ -2835,1832 +4720,7 @@ func TestContextApply_singleDestroy(t *testing.T) { } } -func TestContextPlan(t *testing.T) { - m := testModule(t, "plan-good") - p := testProvider("aws") - p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - if len(plan.Diff.RootModule().Resources) < 2 { - t.Fatalf("bad: %#v", plan.Diff.RootModule().Resources) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestContextPlan_emptyDiff(t *testing.T) { - m := testModule(t, "plan-empty") - p := testProvider("aws") - p.DiffFn = func( - info *InstanceInfo, - s *InstanceState, - c *ResourceConfig) (*InstanceDiff, error) { - return nil, nil - } - - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanEmptyStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestContextPlan_minimal(t *testing.T) { - m := testModule(t, "plan-empty") - p := testProvider("aws") - p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanEmptyStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestContextPlan_modules(t *testing.T) { - m := testModule(t, "plan-modules") - p := testProvider("aws") - p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanModulesStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestContextPlan_moduleInput(t *testing.T) { - m := testModule(t, "plan-module-input") - p := testProvider("aws") - p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanModuleInputStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestContextPlan_moduleInputComputed(t *testing.T) { - m := testModule(t, "plan-module-input-computed") - p := testProvider("aws") - p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanModuleInputComputedStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestContextPlan_moduleInputFromVar(t *testing.T) { - m := testModule(t, "plan-module-input-var") - p := testProvider("aws") - p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - Variables: map[string]string{ - "foo": "52", - }, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanModuleInputVarStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} -func TestContextPlan_moduleMultiVar(t *testing.T) { - m := testModule(t, "plan-module-multi-var") - p := testProvider("aws") - p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanModuleMultiVarStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} -func TestContextPlan_moduleOrphans(t *testing.T) { - m := testModule(t, "plan-modules-remove") - p := testProvider("aws") - p.DiffFn = testDiffFn - s := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: []string{"root", "child"}, - Resources: map[string]*ResourceState{ - "aws_instance.foo": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "baz", - }, - }, - }, - }, - }, - } - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - State: s, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanModuleOrphansStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestContextPlan_moduleProviderInherit(t *testing.T) { - var l sync.Mutex - var calls []string - - m := testModule(t, "plan-module-provider-inherit") - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": func() (ResourceProvider, error) { - l.Lock() - defer l.Unlock() - - p := testProvider("aws") - p.ConfigureFn = func(c *ResourceConfig) error { - if v, ok := c.Get("from"); !ok || v.(string) != "root" { - return fmt.Errorf("bad") - } - - return nil - } - p.DiffFn = func( - info *InstanceInfo, - state *InstanceState, - c *ResourceConfig) (*InstanceDiff, error) { - v, _ := c.Get("from") - calls = append(calls, v.(string)) - return testDiffFn(info, state, c) - } - return p, nil - }, - }, - }) - - _, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := calls - sort.Strings(actual) - expected := []string{"child", "root"} - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) - } -} - -func TestContextPlan_moduleProviderDefaults(t *testing.T) { - var l sync.Mutex - var calls []string - toCount := 0 - - m := testModule(t, "plan-module-provider-defaults") - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": func() (ResourceProvider, error) { - l.Lock() - defer l.Unlock() - - p := testProvider("aws") - p.ConfigureFn = func(c *ResourceConfig) error { - if v, ok := c.Get("from"); !ok || v.(string) != "root" { - return fmt.Errorf("bad") - } - if v, ok := c.Get("to"); ok && v.(string) == "child" { - toCount++ - } - - return nil - } - p.DiffFn = func( - info *InstanceInfo, - state *InstanceState, - c *ResourceConfig) (*InstanceDiff, error) { - v, _ := c.Get("from") - calls = append(calls, v.(string)) - return testDiffFn(info, state, c) - } - return p, nil - }, - }, - }) - - _, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - if toCount != 1 { - t.Fatal("provider in child didn't set proper config") - } - - actual := calls - sort.Strings(actual) - expected := []string{"child", "root"} - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) - } -} - -func TestContextPlan_moduleProviderDefaultsVar(t *testing.T) { - var l sync.Mutex - var calls []string - - m := testModule(t, "plan-module-provider-defaults-var") - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": func() (ResourceProvider, error) { - l.Lock() - defer l.Unlock() - - p := testProvider("aws") - p.ConfigureFn = func(c *ResourceConfig) error { - var buf bytes.Buffer - if v, ok := c.Get("from"); ok { - buf.WriteString(v.(string) + "\n") - } - if v, ok := c.Get("to"); ok { - buf.WriteString(v.(string) + "\n") - } - - calls = append(calls, buf.String()) - return nil - } - p.DiffFn = testDiffFn - return p, nil - }, - }, - Variables: map[string]string{ - "foo": "root", - }, - }) - - _, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := []string{ - "root\n", - "root\nchild\n", - } - if !reflect.DeepEqual(calls, expected) { - t.Fatalf("BAD: %#v", calls) - } -} - -func TestContextPlan_moduleVar(t *testing.T) { - m := testModule(t, "plan-module-var") - p := testProvider("aws") - p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanModuleVarStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestContextPlan_moduleVarComputed(t *testing.T) { - m := testModule(t, "plan-module-var-computed") - p := testProvider("aws") - p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanModuleVarComputedStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestContextPlan_nil(t *testing.T) { - m := testModule(t, "plan-nil") - p := testProvider("aws") - p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - State: &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.foo": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "bar", - }, - }, - }, - }, - }, - }, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - if len(plan.Diff.RootModule().Resources) != 0 { - t.Fatalf("bad: %#v", plan.Diff.RootModule().Resources) - } -} - -func TestContextPlan_computed(t *testing.T) { - m := testModule(t, "plan-computed") - p := testProvider("aws") - p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanComputedStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestContextPlan_computedList(t *testing.T) { - m := testModule(t, "plan-computed-list") - p := testProvider("aws") - p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanComputedListStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestContextPlan_count(t *testing.T) { - m := testModule(t, "plan-count") - p := testProvider("aws") - p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - if len(plan.Diff.RootModule().Resources) < 6 { - t.Fatalf("bad: %#v", plan.Diff.RootModule().Resources) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanCountStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestContextPlan_countComputed(t *testing.T) { - m := testModule(t, "plan-count-computed") - p := testProvider("aws") - p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - _, err := ctx.Plan(nil) - if err == nil { - t.Fatal("should error") - } -} - -func TestContextPlan_countIndex(t *testing.T) { - m := testModule(t, "plan-count-index") - p := testProvider("aws") - p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanCountIndexStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestContextPlan_countIndexZero(t *testing.T) { - m := testModule(t, "plan-count-index-zero") - p := testProvider("aws") - p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanCountIndexZeroStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestContextPlan_countVar(t *testing.T) { - m := testModule(t, "plan-count-var") - p := testProvider("aws") - p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - Variables: map[string]string{ - "count": "3", - }, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanCountVarStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestContextPlan_countZero(t *testing.T) { - m := testModule(t, "plan-count-zero") - p := testProvider("aws") - p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanCountZeroStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestContextPlan_countOneIndex(t *testing.T) { - m := testModule(t, "plan-count-one-index") - p := testProvider("aws") - p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanCountOneIndexStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestContextPlan_countDecreaseToOne(t *testing.T) { - m := testModule(t, "plan-count-dec") - p := testProvider("aws") - p.DiffFn = testDiffFn - s := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.foo.0": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "foo": "foo", - "type": "aws_instance", - }, - }, - }, - "aws_instance.foo.1": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "bar", - }, - }, - "aws_instance.foo.2": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "bar", - }, - }, - }, - }, - }, - } - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - State: s, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanCountDecreaseStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestContextPlan_countIncreaseFromNotSet(t *testing.T) { - m := testModule(t, "plan-count-inc") - p := testProvider("aws") - p.DiffFn = testDiffFn - s := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.foo": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "foo": "foo", - "type": "aws_instance", - }, - }, - }, - }, - }, - }, - } - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - State: s, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanCountIncreaseStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestContextPlan_countIncreaseFromOne(t *testing.T) { - m := testModule(t, "plan-count-inc") - p := testProvider("aws") - p.DiffFn = testDiffFn - s := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.foo.0": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "foo": "foo", - "type": "aws_instance", - }, - }, - }, - }, - }, - }, - } - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - State: s, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanCountIncreaseFromOneStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestContextPlan_destroy(t *testing.T) { - m := testModule(t, "plan-destroy") - p := testProvider("aws") - p.DiffFn = testDiffFn - s := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.one": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "bar", - }, - }, - "aws_instance.two": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "baz", - }, - }, - }, - }, - }, - } - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - State: s, - }) - - plan, err := ctx.Plan(&PlanOpts{Destroy: true}) - if err != nil { - t.Fatalf("err: %s", err) - } - - if len(plan.Diff.RootModule().Resources) != 2 { - t.Fatalf("bad: %#v", plan.Diff.RootModule().Resources) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanDestroyStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestContextPlan_moduleDestroy(t *testing.T) { - m := testModule(t, "plan-module-destroy") - p := testProvider("aws") - p.DiffFn = testDiffFn - s := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.foo": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "bar", - }, - }, - }, - }, - &ModuleState{ - Path: []string{"root", "child"}, - Resources: map[string]*ResourceState{ - "aws_instance.foo": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "bar", - }, - }, - }, - }, - }, - } - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - State: s, - }) - - plan, err := ctx.Plan(&PlanOpts{Destroy: true}) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanModuleDestroyStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestContextPlan_moduleDestroyMultivar(t *testing.T) { - m := testModule(t, "plan-module-destroy-multivar") - p := testProvider("aws") - p.DiffFn = testDiffFn - s := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{}, - }, - &ModuleState{ - Path: []string{"root", "child"}, - Resources: map[string]*ResourceState{ - "aws_instance.foo.0": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "bar0", - }, - }, - "aws_instance.foo.1": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "bar1", - }, - }, - }, - }, - }, - } - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - State: s, - }) - - plan, err := ctx.Plan(&PlanOpts{Destroy: true}) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanModuleDestroyMultivarStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestContextPlan_pathVar(t *testing.T) { - cwd, err := os.Getwd() - if err != nil { - t.Fatalf("err: %s", err) - } - - m := testModule(t, "plan-path-var") - p := testProvider("aws") - p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanPathVarStr) - - module := m.Config().Dir - root := m.Config().Dir - if runtime.GOOS == "windows" { - // The attributes in the diff are %#v-formatted. This means - // that all `\` characters in the Windows paths are escaped - // with a `\`. We need to escape the `\` characters in cwd, - // module, and root before doing any comparison work. - cwd = strings.Replace(cwd, `\`, `\\`, -1) - module = strings.Replace(module, `\`, `\\`, -1) - root = strings.Replace(root, `\`, `\\`, -1) - } - - // Warning: this ordering REALLY matters for this test. The - // order is: cwd, module, root. - expected = fmt.Sprintf(expected, cwd, module, root) - - if actual != expected { - t.Fatalf("bad:\n%s\n\nexpected:\n\n%s", actual, expected) - } -} - -func TestContextPlan_diffVar(t *testing.T) { - m := testModule(t, "plan-diffvar") - p := testProvider("aws") - s := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.foo": &ResourceState{ - Primary: &InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "num": "2", - }, - }, - }, - }, - }, - }, - } - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - State: s, - }) - - p.DiffFn = func( - info *InstanceInfo, - s *InstanceState, - c *ResourceConfig) (*InstanceDiff, error) { - if s.ID != "bar" { - return testDiffFn(info, s, c) - } - - return &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "num": &ResourceAttrDiff{ - Old: "2", - New: "3", - }, - }, - }, nil - } - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanDiffVarStr) - if actual != expected { - t.Fatalf("actual:\n%s\n\nexpected:\n%s", actual, expected) - } -} - -func TestContextPlan_hook(t *testing.T) { - m := testModule(t, "plan-good") - h := new(MockHook) - p := testProvider("aws") - p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ - Module: m, - Hooks: []Hook{h}, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - _, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - if !h.PreDiffCalled { - t.Fatal("should be called") - } - if !h.PostDiffCalled { - t.Fatal("should be called") - } -} - -func TestContextPlan_orphan(t *testing.T) { - m := testModule(t, "plan-orphan") - p := testProvider("aws") - p.DiffFn = testDiffFn - s := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.baz": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "bar", - }, - }, - }, - }, - }, - } - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - State: s, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanOrphanStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestContextPlan_state(t *testing.T) { - m := testModule(t, "plan-good") - p := testProvider("aws") - p.DiffFn = testDiffFn - s := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.foo": &ResourceState{ - Primary: &InstanceState{ - ID: "bar", - }, - }, - }, - }, - }, - } - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - State: s, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - if len(plan.Diff.RootModule().Resources) < 2 { - t.Fatalf("bad: %#v", plan.Diff.RootModule().Resources) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanStateStr) - if actual != expected { - t.Fatalf("bad:\n%s\n\nexpected:\n\n%s", actual, expected) - } -} - -func TestContextPlan_taint(t *testing.T) { - m := testModule(t, "plan-taint") - p := testProvider("aws") - p.DiffFn = testDiffFn - s := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.foo": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "bar", - Attributes: map[string]string{"num": "2"}, - }, - }, - "aws_instance.bar": &ResourceState{ - Type: "aws_instance", - Tainted: []*InstanceState{ - &InstanceState{ - ID: "baz", - }, - }, - }, - }, - }, - }, - } - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - State: s, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanTaintStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -// Doing a Refresh (or any operation really, but Refresh usually -// happens first) with a config with an unknown provider should result in -// an error. The key bug this found was that this wasn't happening if -// Providers was _empty_. -func TestContextRefresh_unknownProvider(t *testing.T) { - m := testModule(t, "refresh-unknown-provider") - p := testProvider("aws") - p.ApplyFn = testApplyFn - p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{}, - }) - - if _, err := ctx.Refresh(); err == nil { - t.Fatal("should error") - } -} - -func TestContextPlan_multiple_taint(t *testing.T) { - m := testModule(t, "plan-taint") - p := testProvider("aws") - p.DiffFn = testDiffFn - s := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.foo": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "bar", - Attributes: map[string]string{"num": "2"}, - }, - }, - "aws_instance.bar": &ResourceState{ - Type: "aws_instance", - Tainted: []*InstanceState{ - &InstanceState{ - ID: "baz", - }, - &InstanceState{ - ID: "zip", - }, - }, - }, - }, - }, - }, - } - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - State: s, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanMultipleTaintStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestContextPlan_provider(t *testing.T) { - m := testModule(t, "plan-provider") - p := testProvider("aws") - p.DiffFn = testDiffFn - - var value interface{} - p.ConfigureFn = func(c *ResourceConfig) error { - value, _ = c.Get("foo") - return nil - } - - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - Variables: map[string]string{ - "foo": "bar", - }, - }) - - if _, err := ctx.Plan(nil); err != nil { - t.Fatalf("err: %s", err) - } - - if value != "bar" { - t.Fatalf("bad: %#v", value) - } -} - -func TestContextPlan_varMultiCountOne(t *testing.T) { - m := testModule(t, "plan-var-multi-count-one") - p := testProvider("aws") - p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - plan, err := ctx.Plan(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanVarMultiCountOneStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestContextPlan_varListErr(t *testing.T) { - m := testModule(t, "plan-var-list-err") - p := testProvider("aws") - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - _, err := ctx.Plan(nil) - if err == nil { - t.Fatal("should error") - } -} - -func TestContextRefresh(t *testing.T) { - p := testProvider("aws") - m := testModule(t, "refresh-basic") - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - State: &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.web": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - }, - }, - }, - }, - }) - - p.RefreshFn = nil - p.RefreshReturn = &InstanceState{ - ID: "foo", - } - - s, err := ctx.Refresh() - mod := s.RootModule() - if err != nil { - t.Fatalf("err: %s", err) - } - if !p.RefreshCalled { - t.Fatal("refresh should be called") - } - if p.RefreshState.ID != "foo" { - t.Fatalf("bad: %#v", p.RefreshState) - } - if !reflect.DeepEqual(mod.Resources["aws_instance.web"].Primary, p.RefreshReturn) { - t.Fatalf("bad: %#v %#v", mod.Resources["aws_instance.web"], p.RefreshReturn) - } - - for _, r := range mod.Resources { - if r.Type == "" { - t.Fatalf("no type: %#v", r) - } - } -} - -func TestContextRefresh_delete(t *testing.T) { - p := testProvider("aws") - m := testModule(t, "refresh-basic") - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - State: &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.web": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - }, - }, - }, - }, - }) - - p.RefreshFn = nil - p.RefreshReturn = nil - - s, err := ctx.Refresh() - if err != nil { - t.Fatalf("err: %s", err) - } - - mod := s.RootModule() - if len(mod.Resources) > 0 { - t.Fatal("resources should be empty") - } -} - -func TestContextRefresh_ignoreUncreated(t *testing.T) { - p := testProvider("aws") - m := testModule(t, "refresh-basic") - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - State: nil, - }) - - p.RefreshFn = nil - p.RefreshReturn = &InstanceState{ - ID: "foo", - } - - _, err := ctx.Refresh() - if err != nil { - t.Fatalf("err: %s", err) - } - if p.RefreshCalled { - t.Fatal("refresh should not be called") - } -} - -func TestContextRefresh_hook(t *testing.T) { - h := new(MockHook) - p := testProvider("aws") - m := testModule(t, "refresh-basic") - ctx := testContext(t, &ContextOpts{ - Module: m, - Hooks: []Hook{h}, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - State: &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.web": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - }, - }, - }, - }, - }) - - if _, err := ctx.Refresh(); err != nil { - t.Fatalf("err: %s", err) - } - if !h.PreRefreshCalled { - t.Fatal("should be called") - } - /* - TODO(mitchcellh): remove when we add InstanceInfo param - if h.PreRefreshState.Type != "aws_instance" { - t.Fatalf("bad: %#v", h.PreRefreshState) - } - */ - if !h.PostRefreshCalled { - t.Fatal("should be called") - } - /* - TODO(mitchcellh): remove when we add InstanceInfo param - if h.PostRefreshState.Type != "aws_instance" { - t.Fatalf("bad: %#v", h.PostRefreshState) - } - */ -} - -func TestContextRefresh_modules(t *testing.T) { - p := testProvider("aws") - m := testModule(t, "refresh-modules") - state := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.web": &ResourceState{ - Type: "aws_instance", - Tainted: []*InstanceState{ - &InstanceState{ - ID: "bar", - }, - }, - }, - }, - }, - - &ModuleState{ - Path: []string{"root", "child"}, - Resources: map[string]*ResourceState{ - "aws_instance.web": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "baz", - }, - }, - }, - }, - }, - } - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - State: state, - }) - - p.RefreshFn = func(info *InstanceInfo, s *InstanceState) (*InstanceState, error) { - if s.ID != "baz" { - return s, nil - } - - s.ID = "new" - return s, nil - } - - s, err := ctx.Refresh() - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(s.String()) - expected := strings.TrimSpace(testContextRefreshModuleStr) - if actual != expected { - t.Fatalf("bad:\n\n%s\n\n%s", actual, expected) - } -} - -func TestContextRefresh_moduleInputComputedOutput(t *testing.T) { - m := testModule(t, "refresh-module-input-computed-output") - p := testProvider("aws") - p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - if _, err := ctx.Refresh(); err != nil { - t.Fatalf("err: %s", err) - } -} - -func TestContextRefresh_moduleVarModule(t *testing.T) { - m := testModule(t, "refresh-module-var-module") - p := testProvider("aws") - p.DiffFn = testDiffFn - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - if _, err := ctx.Refresh(); err != nil { - t.Fatalf("err: %s", err) - } -} - -// GH-70 -func TestContextRefresh_noState(t *testing.T) { - p := testProvider("aws") - m := testModule(t, "refresh-no-state") - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - }) - - p.RefreshFn = nil - p.RefreshReturn = &InstanceState{ - ID: "foo", - } - - if _, err := ctx.Refresh(); err != nil { - t.Fatalf("err: %s", err) - } -} - -func TestContextRefresh_outputPartial(t *testing.T) { - p := testProvider("aws") - m := testModule(t, "refresh-output-partial") - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - State: &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.foo": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - }, - }, - }, - }, - }) - - p.RefreshFn = nil - p.RefreshReturn = nil - - s, err := ctx.Refresh() - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(s.String()) - expected := strings.TrimSpace(testContextRefreshOutputPartialStr) - if actual != expected { - t.Fatalf("bad:\n\n%s\n\n%s", actual, expected) - } -} - -func TestContextRefresh_state(t *testing.T) { - p := testProvider("aws") - m := testModule(t, "refresh-basic") - state := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.web": &ResourceState{ - Primary: &InstanceState{ - ID: "bar", - }, - }, - }, - }, - }, - } - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - State: state, - }) - - p.RefreshFn = nil - p.RefreshReturn = &InstanceState{ - ID: "foo", - } - - s, err := ctx.Refresh() - if err != nil { - t.Fatalf("err: %s", err) - } - originalMod := state.RootModule() - mod := s.RootModule() - if !p.RefreshCalled { - t.Fatal("refresh should be called") - } - if !reflect.DeepEqual(p.RefreshState, originalMod.Resources["aws_instance.web"].Primary) { - t.Fatalf("bad: %#v %#v", p.RefreshState, originalMod.Resources["aws_instance.web"].Primary) - } - if !reflect.DeepEqual(mod.Resources["aws_instance.web"].Primary, p.RefreshReturn) { - t.Fatalf("bad: %#v", mod.Resources) - } -} - -func TestContextRefresh_tainted(t *testing.T) { - p := testProvider("aws") - m := testModule(t, "refresh-basic") - state := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.web": &ResourceState{ - Type: "aws_instance", - Tainted: []*InstanceState{ - &InstanceState{ - ID: "bar", - }, - }, - }, - }, - }, - }, - } - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - State: state, - }) - - p.RefreshFn = nil - p.RefreshReturn = &InstanceState{ - ID: "foo", - } - - s, err := ctx.Refresh() - if err != nil { - t.Fatalf("err: %s", err) - } - if !p.RefreshCalled { - t.Fatal("refresh should be called") - } - - actual := strings.TrimSpace(s.String()) - expected := strings.TrimSpace(testContextRefreshTaintedStr) - if actual != expected { - t.Fatalf("bad:\n\n%s\n\n%s", actual, expected) - } -} - -func TestContextRefresh_vars(t *testing.T) { - p := testProvider("aws") - m := testModule(t, "refresh-vars") - ctx := testContext(t, &ContextOpts{ - Module: m, - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - State: &State{ - - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.web": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - }, - }, - }, - }, - }) - - p.RefreshFn = nil - p.RefreshReturn = &InstanceState{ - ID: "foo", - } - - s, err := ctx.Refresh() - if err != nil { - t.Fatalf("err: %s", err) - } - mod := s.RootModule() - if !p.RefreshCalled { - t.Fatal("refresh should be called") - } - if p.RefreshState.ID != "foo" { - t.Fatalf("bad: %#v", p.RefreshState) - } - if !reflect.DeepEqual(mod.Resources["aws_instance.web"].Primary, p.RefreshReturn) { - t.Fatalf("bad: %#v", mod.Resources["aws_instance.web"]) - } - - for _, r := range mod.Resources { - if r.Type == "" { - t.Fatalf("no type: %#v", r) - } - } -} - -func testContext(t *testing.T, opts *ContextOpts) *Context { +func testContext2(t *testing.T, opts *ContextOpts) *Context { return NewContext(opts) } diff --git a/terraform/diff.go b/terraform/diff.go index f9a0e1887..dbaf37e02 100644 --- a/terraform/diff.go +++ b/terraform/diff.go @@ -333,6 +333,10 @@ func (d *InstanceDiff) Empty() bool { return !d.Destroy && len(d.Attributes) == 0 } +func (d *InstanceDiff) GoString() string { + return fmt.Sprintf("*%#v", *d) +} + // RequiresNew returns true if the diff requires the creation of a new // resource (implying the destruction of the old). func (d *InstanceDiff) RequiresNew() bool { diff --git a/terraform/eval.go b/terraform/eval.go new file mode 100644 index 000000000..73a323a67 --- /dev/null +++ b/terraform/eval.go @@ -0,0 +1,59 @@ +package terraform + +import ( + "log" + "strings" +) + +// EvalNode is the interface that must be implemented by graph nodes to +// evaluate/execute. +type EvalNode interface { + // Eval evaluates this node with the given context. The second parameter + // are the argument values. These will match in order and 1-1 with the + // results of the Args() return value. + Eval(EvalContext) (interface{}, error) +} + +// GraphNodeEvalable is the interface that graph nodes must implement +// to enable valuation. +type GraphNodeEvalable interface { + EvalTree() EvalNode +} + +// EvalEarlyExitError is a special error return value that can be returned +// by eval nodes that does an early exit. +type EvalEarlyExitError struct{} + +func (EvalEarlyExitError) Error() string { return "early exit" } + +// Eval evaluates the given EvalNode with the given context, properly +// evaluating all args in the correct order. +func Eval(n EvalNode, ctx EvalContext) (interface{}, error) { + // Call the lower level eval which doesn't understand early exit, + // and if we early exit, it isn't an error. + result, err := EvalRaw(n, ctx) + if err != nil { + if _, ok := err.(EvalEarlyExitError); ok { + return nil, nil + } + } + + return result, err +} + +// EvalRaw is like Eval except that it returns all errors, even if they +// signal something normal such as EvalEarlyExitError. +func EvalRaw(n EvalNode, ctx EvalContext) (interface{}, error) { + path := "unknown" + if ctx != nil { + path = strings.Join(ctx.Path(), ".") + } + + log.Printf("[DEBUG] %s: eval: %T", path, n) + output, err := n.Eval(ctx) + if err != nil { + log.Printf("[ERROR] %s: eval: %T, err: %s", path, n, err) + } + + return output, err +} diff --git a/terraform/eval_apply.go b/terraform/eval_apply.go new file mode 100644 index 000000000..cebf68265 --- /dev/null +++ b/terraform/eval_apply.go @@ -0,0 +1,297 @@ +package terraform + +import ( + "fmt" + "log" + "strconv" + + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform/config" +) + +// EvalApply is an EvalNode implementation that writes the diff to +// the full diff. +type EvalApply struct { + Info *InstanceInfo + State **InstanceState + Diff **InstanceDiff + Provider *ResourceProvider + Output **InstanceState + CreateNew *bool + Error *error +} + +// TODO: test +func (n *EvalApply) Eval(ctx EvalContext) (interface{}, error) { + diff := *n.Diff + provider := *n.Provider + state := *n.State + + // If we have no diff, we have nothing to do! + if diff.Empty() { + log.Printf( + "[DEBUG] apply: %s: diff is empty, doing nothing.", n.Info.Id) + return nil, nil + } + + // Remove any output values from the diff + for k, ad := range diff.Attributes { + if ad.Type == DiffAttrOutput { + delete(diff.Attributes, k) + } + } + + // If the state is nil, make it non-nil + if state == nil { + state = new(InstanceState) + } + state.init() + + // Flag if we're creating a new instance + if n.CreateNew != nil { + *n.CreateNew = (state.ID == "" && !diff.Destroy) || diff.RequiresNew() + } + + { + // Call pre-apply hook + err := ctx.Hook(func(h Hook) (HookAction, error) { + return h.PreApply(n.Info, state, diff) + }) + if err != nil { + return nil, err + } + } + + // With the completed diff, apply! + log.Printf("[DEBUG] apply: %s: executing Apply", n.Info.Id) + state, err := provider.Apply(n.Info, state, diff) + if state == nil { + state = new(InstanceState) + } + state.init() + + // Force the "id" attribute to be our ID + if state.ID != "" { + state.Attributes["id"] = state.ID + } + + // If the value is the unknown variable value, then it is an error. + // In this case we record the error and remove it from the state + for ak, av := range state.Attributes { + if av == config.UnknownVariableValue { + err = multierror.Append(err, fmt.Errorf( + "Attribute with unknown value: %s", ak)) + delete(state.Attributes, ak) + } + } + + // Write the final state + if n.Output != nil { + *n.Output = state + } + + // If there are no errors, then we append it to our output error + // if we have one, otherwise we just output it. + if err != nil { + if n.Error != nil { + *n.Error = multierror.Append(*n.Error, err) + } else { + return nil, err + } + } + + return nil, nil +} + +// EvalApplyPost is an EvalNode implementation that does the post-Apply work +type EvalApplyPost struct { + Info *InstanceInfo + State **InstanceState + Error *error +} + +// TODO: test +func (n *EvalApplyPost) Eval(ctx EvalContext) (interface{}, error) { + state := *n.State + + { + // Call post-apply hook + err := ctx.Hook(func(h Hook) (HookAction, error) { + return h.PostApply(n.Info, state, *n.Error) + }) + if err != nil { + return nil, err + } + } + + return nil, *n.Error +} + +// EvalApplyProvisioners is an EvalNode implementation that executes +// the provisioners for a resource. +// +// TODO(mitchellh): This should probably be split up into a more fine-grained +// ApplyProvisioner (single) that is looped over. +type EvalApplyProvisioners struct { + Info *InstanceInfo + State **InstanceState + Resource *config.Resource + InterpResource *Resource + CreateNew *bool + Tainted *bool + Error *error +} + +// TODO: test +func (n *EvalApplyProvisioners) Eval(ctx EvalContext) (interface{}, error) { + state := *n.State + + if !*n.CreateNew { + // If we're not creating a new resource, then don't run provisioners + return nil, nil + } + + if len(n.Resource.Provisioners) == 0 { + // We have no provisioners, so don't do anything + return nil, nil + } + + if n.Error != nil && *n.Error != nil { + // We're already errored creating, so mark as tainted and continue + if n.Tainted != nil { + *n.Tainted = true + } + + // We're already tainted, so just return out + return nil, nil + } + + { + // Call pre hook + err := ctx.Hook(func(h Hook) (HookAction, error) { + return h.PreProvisionResource(n.Info, state) + }) + if err != nil { + return nil, err + } + } + + // If there are no errors, then we append it to our output error + // if we have one, otherwise we just output it. + err := n.apply(ctx) + if n.Tainted != nil { + *n.Tainted = err != nil + } + if err != nil { + if n.Error != nil { + *n.Error = multierror.Append(*n.Error, err) + } else { + return nil, err + } + } + + { + // Call post hook + err := ctx.Hook(func(h Hook) (HookAction, error) { + return h.PostProvisionResource(n.Info, state) + }) + if err != nil { + return nil, err + } + } + + return nil, nil +} + +func (n *EvalApplyProvisioners) apply(ctx EvalContext) error { + state := *n.State + + // Store the original connection info, restore later + origConnInfo := state.Ephemeral.ConnInfo + defer func() { + state.Ephemeral.ConnInfo = origConnInfo + }() + + for _, prov := range n.Resource.Provisioners { + // Get the provisioner + provisioner := ctx.Provisioner(prov.Type) + + // Interpolate the provisioner config + provConfig, err := ctx.Interpolate(prov.RawConfig, n.InterpResource) + if err != nil { + return err + } + + // Interpolate the conn info, since it may contain variables + connInfo, err := ctx.Interpolate(prov.ConnInfo, n.InterpResource) + if err != nil { + return err + } + + // Merge the connection information + overlay := make(map[string]string) + if origConnInfo != nil { + for k, v := range origConnInfo { + overlay[k] = v + } + } + for k, v := range connInfo.Config { + switch vt := v.(type) { + case string: + overlay[k] = vt + case int64: + overlay[k] = strconv.FormatInt(vt, 10) + case int32: + overlay[k] = strconv.FormatInt(int64(vt), 10) + case int: + overlay[k] = strconv.FormatInt(int64(vt), 10) + case float32: + overlay[k] = strconv.FormatFloat(float64(vt), 'f', 3, 32) + case float64: + overlay[k] = strconv.FormatFloat(vt, 'f', 3, 64) + case bool: + overlay[k] = strconv.FormatBool(vt) + default: + overlay[k] = fmt.Sprintf("%v", vt) + } + } + state.Ephemeral.ConnInfo = overlay + + { + // Call pre hook + err := ctx.Hook(func(h Hook) (HookAction, error) { + return h.PreProvision(n.Info, prov.Type) + }) + if err != nil { + return err + } + } + + // The output function + outputFn := func(msg string) { + ctx.Hook(func(h Hook) (HookAction, error) { + h.ProvisionOutput(n.Info, prov.Type, msg) + return HookActionContinue, nil + }) + } + + // Invoke the Provisioner + output := CallbackUIOutput{OutputFn: outputFn} + if err := provisioner.Apply(&output, state, provConfig); err != nil { + return err + } + + { + // Call post hook + err := ctx.Hook(func(h Hook) (HookAction, error) { + return h.PostProvision(n.Info, prov.Type) + }) + if err != nil { + return err + } + } + } + + return nil + +} diff --git a/terraform/eval_context.go b/terraform/eval_context.go new file mode 100644 index 000000000..e1d4eef51 --- /dev/null +++ b/terraform/eval_context.go @@ -0,0 +1,224 @@ +package terraform + +import ( + "sync" + + "github.com/hashicorp/terraform/config" +) + +// EvalContext is the interface that is given to eval nodes to execute. +type EvalContext interface { + // Path is the current module path. + Path() []string + + // Hook is used to call hook methods. The callback is called for each + // hook and should return the hook action to take and the error. + Hook(func(Hook) (HookAction, error)) error + + // Input is the UIInput object for interacting with the UI. + Input() UIInput + + // InitProvider initializes the provider with the given name and + // returns the implementation of the resource provider or an error. + // + // It is an error to initialize the same provider more than once. + InitProvider(string) (ResourceProvider, error) + + // Provider gets the provider instance with the given name (already + // initialized) or returns nil if the provider isn't initialized. + Provider(string) ResourceProvider + + // ConfigureProvider configures the provider with the given + // configuration. This is a separate context call because this call + // is used to store the provider configuration for inheritance lookups + // with ParentProviderConfig(). + ConfigureProvider(string, *ResourceConfig) error + ParentProviderConfig(string) *ResourceConfig + + // ProviderInput and SetProviderInput are used to configure providers + // from user input. + ProviderInput(string) map[string]interface{} + SetProviderInput(string, map[string]interface{}) + + // InitProvisioner initializes the provisioner with the given name and + // returns the implementation of the resource provisioner or an error. + // + // It is an error to initialize the same provisioner more than once. + InitProvisioner(string) (ResourceProvisioner, error) + + // Provisioner gets the provisioner instance with the given name (already + // initialized) or returns nil if the provisioner isn't initialized. + Provisioner(string) ResourceProvisioner + + // Interpolate takes the given raw configuration and completes + // the interpolations, returning the processed ResourceConfig. + // + // The resource argument is optional. If given, it is the resource + // that is currently being acted upon. + Interpolate(*config.RawConfig, *Resource) (*ResourceConfig, error) + + // SetVariables sets the variables for interpolation. These variables + // should not have a "var." prefix. For example: "var.foo" should be + // "foo" as the key. + SetVariables(map[string]string) + + // Diff returns the global diff as well as the lock that should + // be used to modify that diff. + Diff() (*Diff, *sync.RWMutex) + + // State returns the global state as well as the lock that should + // be used to modify that state. + State() (*State, *sync.RWMutex) +} + +// MockEvalContext is a mock version of EvalContext that can be used +// for tests. +type MockEvalContext struct { + HookCalled bool + HookError error + + InputCalled bool + InputInput UIInput + + InitProviderCalled bool + InitProviderName string + InitProviderProvider ResourceProvider + InitProviderError error + + ProviderCalled bool + ProviderName string + ProviderProvider ResourceProvider + + ProviderInputCalled bool + ProviderInputName string + ProviderInputConfig map[string]interface{} + + SetProviderInputCalled bool + SetProviderInputName string + SetProviderInputConfig map[string]interface{} + + ConfigureProviderCalled bool + ConfigureProviderName string + ConfigureProviderConfig *ResourceConfig + ConfigureProviderError error + + ParentProviderConfigCalled bool + ParentProviderConfigName string + ParentProviderConfigConfig *ResourceConfig + + InitProvisionerCalled bool + InitProvisionerName string + InitProvisionerProvisioner ResourceProvisioner + InitProvisionerError error + + ProvisionerCalled bool + ProvisionerName string + ProvisionerProvisioner ResourceProvisioner + + InterpolateCalled bool + InterpolateConfig *config.RawConfig + InterpolateResource *Resource + InterpolateConfigResult *ResourceConfig + InterpolateError error + + PathCalled bool + PathPath []string + + SetVariablesCalled bool + SetVariablesVariables map[string]string + + DiffCalled bool + DiffDiff *Diff + DiffLock *sync.RWMutex + + StateCalled bool + StateState *State + StateLock *sync.RWMutex +} + +func (c *MockEvalContext) Hook(fn func(Hook) (HookAction, error)) error { + c.HookCalled = true + return c.HookError +} + +func (c *MockEvalContext) Input() UIInput { + c.InputCalled = true + return c.InputInput +} + +func (c *MockEvalContext) InitProvider(n string) (ResourceProvider, error) { + c.InitProviderCalled = true + c.InitProviderName = n + return c.InitProviderProvider, c.InitProviderError +} + +func (c *MockEvalContext) Provider(n string) ResourceProvider { + c.ProviderCalled = true + c.ProviderName = n + return c.ProviderProvider +} + +func (c *MockEvalContext) ConfigureProvider(n string, cfg *ResourceConfig) error { + c.ConfigureProviderCalled = true + c.ConfigureProviderName = n + c.ConfigureProviderConfig = cfg + return c.ConfigureProviderError +} + +func (c *MockEvalContext) ParentProviderConfig(n string) *ResourceConfig { + c.ParentProviderConfigCalled = true + c.ParentProviderConfigName = n + return c.ParentProviderConfigConfig +} + +func (c *MockEvalContext) ProviderInput(n string) map[string]interface{} { + c.ProviderInputCalled = true + c.ProviderInputName = n + return c.ProviderInputConfig +} + +func (c *MockEvalContext) SetProviderInput(n string, cfg map[string]interface{}) { + c.SetProviderInputCalled = true + c.SetProviderInputName = n + c.SetProviderInputConfig = cfg +} + +func (c *MockEvalContext) InitProvisioner(n string) (ResourceProvisioner, error) { + c.InitProvisionerCalled = true + c.InitProvisionerName = n + return c.InitProvisionerProvisioner, c.InitProvisionerError +} + +func (c *MockEvalContext) Provisioner(n string) ResourceProvisioner { + c.ProvisionerCalled = true + c.ProvisionerName = n + return c.ProvisionerProvisioner +} + +func (c *MockEvalContext) Interpolate( + config *config.RawConfig, resource *Resource) (*ResourceConfig, error) { + c.InterpolateCalled = true + c.InterpolateConfig = config + c.InterpolateResource = resource + return c.InterpolateConfigResult, c.InterpolateError +} + +func (c *MockEvalContext) Path() []string { + c.PathCalled = true + return c.PathPath +} + +func (c *MockEvalContext) SetVariables(vs map[string]string) { + c.SetVariablesCalled = true + c.SetVariablesVariables = vs +} + +func (c *MockEvalContext) Diff() (*Diff, *sync.RWMutex) { + c.DiffCalled = true + return c.DiffDiff, c.DiffLock +} + +func (c *MockEvalContext) State() (*State, *sync.RWMutex) { + c.StateCalled = true + return c.StateState, c.StateLock +} diff --git a/terraform/eval_context_builtin.go b/terraform/eval_context_builtin.go new file mode 100644 index 000000000..f5c96bd30 --- /dev/null +++ b/terraform/eval_context_builtin.go @@ -0,0 +1,230 @@ +package terraform + +import ( + "fmt" + "log" + "sync" + + "github.com/hashicorp/terraform/config" +) + +// BuiltinEvalContext is an EvalContext implementation that is used by +// Terraform by default. +type BuiltinEvalContext struct { + PathValue []string + Interpolater *Interpolater + Hooks []Hook + InputValue UIInput + Providers map[string]ResourceProviderFactory + ProviderCache map[string]ResourceProvider + ProviderConfigCache map[string]*ResourceConfig + ProviderInputConfig map[string]map[string]interface{} + ProviderLock *sync.Mutex + Provisioners map[string]ResourceProvisionerFactory + ProvisionerCache map[string]ResourceProvisioner + ProvisionerLock *sync.Mutex + DiffValue *Diff + DiffLock *sync.RWMutex + StateValue *State + StateLock *sync.RWMutex + + once sync.Once +} + +func (ctx *BuiltinEvalContext) Hook(fn func(Hook) (HookAction, error)) error { + for _, h := range ctx.Hooks { + action, err := fn(h) + if err != nil { + return err + } + + switch action { + case HookActionContinue: + continue + case HookActionHalt: + // Return an early exit error to trigger an early exit + log.Printf("[WARN] Early exit triggered by hook: %T", h) + return EvalEarlyExitError{} + } + } + + return nil +} + +func (ctx *BuiltinEvalContext) Input() UIInput { + return ctx.InputValue +} + +func (ctx *BuiltinEvalContext) InitProvider(n string) (ResourceProvider, error) { + ctx.once.Do(ctx.init) + + // If we already initialized, it is an error + if p := ctx.Provider(n); p != nil { + return nil, fmt.Errorf("Provider '%s' already initialized", n) + } + + // Warning: make sure to acquire these locks AFTER the call to Provider + // above, since it also acquires locks. + ctx.ProviderLock.Lock() + defer ctx.ProviderLock.Unlock() + + f, ok := ctx.Providers[n] + if !ok { + return nil, fmt.Errorf("Provider '%s' not found", n) + } + + p, err := f() + if err != nil { + return nil, err + } + + providerPath := make([]string, len(ctx.Path())+1) + copy(providerPath, ctx.Path()) + providerPath[len(providerPath)-1] = n + + ctx.ProviderCache[PathCacheKey(providerPath)] = p + return p, nil +} + +func (ctx *BuiltinEvalContext) Provider(n string) ResourceProvider { + ctx.once.Do(ctx.init) + + ctx.ProviderLock.Lock() + defer ctx.ProviderLock.Unlock() + + providerPath := make([]string, len(ctx.Path())+1) + copy(providerPath, ctx.Path()) + providerPath[len(providerPath)-1] = n + + return ctx.ProviderCache[PathCacheKey(providerPath)] +} + +func (ctx *BuiltinEvalContext) ConfigureProvider( + n string, cfg *ResourceConfig) error { + p := ctx.Provider(n) + if p == nil { + return fmt.Errorf("Provider '%s' not initialized", n) + } + + // Save the configuration + ctx.ProviderLock.Lock() + ctx.ProviderConfigCache[PathCacheKey(ctx.Path())] = cfg + ctx.ProviderLock.Unlock() + + return p.Configure(cfg) +} + +func (ctx *BuiltinEvalContext) ProviderInput(n string) map[string]interface{} { + ctx.ProviderLock.Lock() + defer ctx.ProviderLock.Unlock() + + return ctx.ProviderInputConfig[n] +} + +func (ctx *BuiltinEvalContext) SetProviderInput(n string, c map[string]interface{}) { + ctx.ProviderLock.Lock() + defer ctx.ProviderLock.Unlock() + + ctx.ProviderInputConfig[n] = c +} + +func (ctx *BuiltinEvalContext) ParentProviderConfig(n string) *ResourceConfig { + ctx.ProviderLock.Lock() + defer ctx.ProviderLock.Unlock() + + path := ctx.Path() + for i := len(path) - 1; i >= 1; i-- { + k := PathCacheKey(path[:i]) + if v, ok := ctx.ProviderConfigCache[k]; ok { + return v + } + } + + return nil +} + +func (ctx *BuiltinEvalContext) InitProvisioner( + n string) (ResourceProvisioner, error) { + ctx.once.Do(ctx.init) + + // If we already initialized, it is an error + if p := ctx.Provisioner(n); p != nil { + return nil, fmt.Errorf("Provisioner '%s' already initialized", n) + } + + // Warning: make sure to acquire these locks AFTER the call to Provisioner + // above, since it also acquires locks. + ctx.ProvisionerLock.Lock() + defer ctx.ProvisionerLock.Unlock() + + f, ok := ctx.Provisioners[n] + if !ok { + return nil, fmt.Errorf("Provisioner '%s' not found", n) + } + + p, err := f() + if err != nil { + return nil, err + } + + ctx.ProvisionerCache[PathCacheKey(ctx.Path())] = p + return p, nil +} + +func (ctx *BuiltinEvalContext) Provisioner(n string) ResourceProvisioner { + ctx.once.Do(ctx.init) + + ctx.ProvisionerLock.Lock() + defer ctx.ProvisionerLock.Unlock() + + return ctx.ProvisionerCache[PathCacheKey(ctx.Path())] +} + +func (ctx *BuiltinEvalContext) Interpolate( + cfg *config.RawConfig, r *Resource) (*ResourceConfig, error) { + if cfg != nil { + scope := &InterpolationScope{ + Path: ctx.Path(), + Resource: r, + } + vs, err := ctx.Interpolater.Values(scope, cfg.Variables) + if err != nil { + return nil, err + } + + // Do the interpolation + if err := cfg.Interpolate(vs); err != nil { + return nil, err + } + } + + result := NewResourceConfig(cfg) + result.interpolateForce() + return result, nil +} + +func (ctx *BuiltinEvalContext) Path() []string { + return ctx.PathValue +} + +func (ctx *BuiltinEvalContext) SetVariables(vs map[string]string) { + for k, v := range vs { + ctx.Interpolater.Variables[k] = v + } +} + +func (ctx *BuiltinEvalContext) Diff() (*Diff, *sync.RWMutex) { + return ctx.DiffValue, ctx.DiffLock +} + +func (ctx *BuiltinEvalContext) State() (*State, *sync.RWMutex) { + return ctx.StateValue, ctx.StateLock +} + +func (ctx *BuiltinEvalContext) init() { + // We nil-check the things below because they're meant to be configured, + // and we just default them to non-nil. + if ctx.Providers == nil { + ctx.Providers = make(map[string]ResourceProviderFactory) + } +} diff --git a/terraform/eval_count.go b/terraform/eval_count.go new file mode 100644 index 000000000..f7886b8da --- /dev/null +++ b/terraform/eval_count.go @@ -0,0 +1,50 @@ +package terraform + +import ( + "github.com/hashicorp/terraform/config" +) + +// EvalCountFixZeroOneBoundary is an EvalNode that fixes up the state +// when there is a resource count with zero/one boundary, i.e. fixing +// a resource named "aws_instance.foo" to "aws_instance.foo.0" and vice-versa. +type EvalCountFixZeroOneBoundary struct { + Resource *config.Resource +} + +// TODO: test +func (n *EvalCountFixZeroOneBoundary) Eval(ctx EvalContext) (interface{}, error) { + // Get the count, important for knowing whether we're supposed to + // be adding the zero, or trimming it. + count, err := n.Resource.Count() + if err != nil { + return nil, err + } + + // Figure what to look for and what to replace it with + hunt := n.Resource.Id() + replace := hunt + ".0" + if count < 2 { + hunt, replace = replace, hunt + } + + state, lock := ctx.State() + + // Get a lock so we can access this instance and potentially make + // changes to it. + lock.Lock() + defer lock.Unlock() + + // Look for the module state. If we don't have one, then it doesn't matter. + mod := state.ModuleByPath(ctx.Path()) + if mod == nil { + return nil, nil + } + + // Look for the resource state. If we don't have one, then it is okay. + if rs, ok := mod.Resources[hunt]; ok { + mod.Resources[replace] = rs + delete(mod.Resources, hunt) + } + + return nil, nil +} diff --git a/terraform/eval_diff.go b/terraform/eval_diff.go new file mode 100644 index 000000000..89a583918 --- /dev/null +++ b/terraform/eval_diff.go @@ -0,0 +1,299 @@ +package terraform + +import ( + "fmt" + "log" +) + +// EvalCompareDiff is an EvalNode implementation that compares two diffs +// and errors if the diffs are not equal. +type EvalCompareDiff struct { + Info *InstanceInfo + One, Two **InstanceDiff +} + +// TODO: test +func (n *EvalCompareDiff) Eval(ctx EvalContext) (interface{}, error) { + one, two := *n.One, *n.Two + + // If either are nil, let them be empty + if one == nil { + one = new(InstanceDiff) + one.init() + } + if two == nil { + two = new(InstanceDiff) + two.init() + } + oneId := one.Attributes["id"] + twoId := two.Attributes["id"] + delete(one.Attributes, "id") + delete(two.Attributes, "id") + defer func() { + if oneId != nil { + one.Attributes["id"] = oneId + } + if twoId != nil { + two.Attributes["id"] = twoId + } + }() + + if !one.Same(two) { + log.Printf("[ERROR] %s: diff's didn't match", n.Info.Id) + log.Printf("[ERROR] %s: diff one: %#v", n.Info.Id, one) + log.Printf("[ERROR] %s: diff two: %#v", n.Info.Id, two) + return nil, fmt.Errorf( + "%s: diffs didn't match during apply. This is a bug with "+ + "Terraform and should be reported.", n.Info.Id) + } + + return nil, nil +} + +// EvalDiff is an EvalNode implementation that does a refresh for +// a resource. +type EvalDiff struct { + Info *InstanceInfo + Config **ResourceConfig + Provider *ResourceProvider + State **InstanceState + Output **InstanceDiff + OutputState **InstanceState +} + +// TODO: test +func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) { + state := *n.State + config := *n.Config + provider := *n.Provider + + // Call pre-diff hook + err := ctx.Hook(func(h Hook) (HookAction, error) { + return h.PreDiff(n.Info, state) + }) + if err != nil { + return nil, err + } + + // The state for the diff must never be nil + diffState := state + if diffState == nil { + diffState = new(InstanceState) + } + diffState.init() + + // Diff! + diff, err := provider.Diff(n.Info, diffState, config) + if err != nil { + return nil, err + } + if diff == nil { + diff = new(InstanceDiff) + } + + // Require a destroy if there is no ID and it requires new. + if diff.RequiresNew() && state != nil && state.ID != "" { + diff.Destroy = true + } + + // If we're creating a new resource, compute its ID + if diff.RequiresNew() || state == nil || state.ID == "" { + var oldID string + if state != nil { + oldID = state.Attributes["id"] + } + + // Add diff to compute new ID + diff.init() + diff.Attributes["id"] = &ResourceAttrDiff{ + Old: oldID, + NewComputed: true, + RequiresNew: true, + Type: DiffAttrOutput, + } + } + + // Call post-refresh hook + err = ctx.Hook(func(h Hook) (HookAction, error) { + return h.PostDiff(n.Info, diff) + }) + if err != nil { + return nil, err + } + + // Update our output + *n.Output = diff + + // Update the state if we care + if n.OutputState != nil { + *n.OutputState = state + + // Merge our state so that the state is updated with our plan + if !diff.Empty() && n.OutputState != nil { + *n.OutputState = state.MergeDiff(diff) + } + } + + return nil, nil +} + +// EvalDiffDestroy is an EvalNode implementation that returns a plain +// destroy diff. +type EvalDiffDestroy struct { + Info *InstanceInfo + State **InstanceState + Output **InstanceDiff +} + +// TODO: test +func (n *EvalDiffDestroy) Eval(ctx EvalContext) (interface{}, error) { + state := *n.State + + // If there is no state or we don't have an ID, we're already destroyed + if state == nil || state.ID == "" { + return nil, nil + } + + // Call pre-diff hook + err := ctx.Hook(func(h Hook) (HookAction, error) { + return h.PreDiff(n.Info, state) + }) + if err != nil { + return nil, err + } + + // The diff + diff := &InstanceDiff{Destroy: true} + + // Call post-diff hook + err = ctx.Hook(func(h Hook) (HookAction, error) { + return h.PostDiff(n.Info, diff) + }) + if err != nil { + return nil, err + } + + // Update our output + *n.Output = diff + + return nil, nil +} + +// EvalDiffDestroyModule is an EvalNode implementation that writes the diff to +// the full diff. +type EvalDiffDestroyModule struct { + Path []string +} + +// TODO: test +func (n *EvalDiffDestroyModule) Eval(ctx EvalContext) (interface{}, error) { + diff, lock := ctx.Diff() + + // Acquire the lock so that we can do this safely concurrently + lock.Lock() + defer lock.Unlock() + + // Write the diff + modDiff := diff.ModuleByPath(n.Path) + if modDiff == nil { + modDiff = diff.AddModule(n.Path) + } + modDiff.Destroy = true + + return nil, nil +} + +// EvalDiffTainted is an EvalNode implementation that writes the diff to +// the full diff. +type EvalDiffTainted struct { + Name string + Diff **InstanceDiff +} + +// TODO: test +func (n *EvalDiffTainted) Eval(ctx EvalContext) (interface{}, error) { + state, lock := ctx.State() + + // Get a read lock so we can access this instance + lock.RLock() + defer lock.RUnlock() + + // Look for the module state. If we don't have one, then it doesn't matter. + mod := state.ModuleByPath(ctx.Path()) + if mod == nil { + return nil, nil + } + + // Look for the resource state. If we don't have one, then it is okay. + rs := mod.Resources[n.Name] + if rs == nil { + return nil, nil + } + + // If we have tainted, then mark it on the diff + if len(rs.Tainted) > 0 { + (*n.Diff).DestroyTainted = true + } + + return nil, nil +} + +// EvalReadDiff is an EvalNode implementation that writes the diff to +// the full diff. +type EvalReadDiff struct { + Name string + Diff **InstanceDiff +} + +func (n *EvalReadDiff) Eval(ctx EvalContext) (interface{}, error) { + diff, lock := ctx.Diff() + + // Acquire the lock so that we can do this safely concurrently + lock.Lock() + defer lock.Unlock() + + // Write the diff + modDiff := diff.ModuleByPath(ctx.Path()) + if modDiff == nil { + return nil, nil + } + + *n.Diff = modDiff.Resources[n.Name] + + return nil, nil +} + +// EvalWriteDiff is an EvalNode implementation that writes the diff to +// the full diff. +type EvalWriteDiff struct { + Name string + Diff **InstanceDiff +} + +// TODO: test +func (n *EvalWriteDiff) Eval(ctx EvalContext) (interface{}, error) { + diff, lock := ctx.Diff() + + // The diff to write, if its empty it should write nil + diffVal := *n.Diff + if diffVal.Empty() { + diffVal = nil + } + + // Acquire the lock so that we can do this safely concurrently + lock.Lock() + defer lock.Unlock() + + // Write the diff + modDiff := diff.ModuleByPath(ctx.Path()) + if modDiff == nil { + modDiff = diff.AddModule(ctx.Path()) + } + if diffVal != nil { + modDiff.Resources[n.Name] = diffVal + } else { + delete(modDiff.Resources, n.Name) + } + + return nil, nil +} diff --git a/terraform/eval_filter.go b/terraform/eval_filter.go new file mode 100644 index 000000000..711c625c8 --- /dev/null +++ b/terraform/eval_filter.go @@ -0,0 +1,25 @@ +package terraform + +// EvalNodeFilterFunc is the callback used to replace a node with +// another to node. To not do the replacement, just return the input node. +type EvalNodeFilterFunc func(EvalNode) EvalNode + +// EvalNodeFilterable is an interface that can be implemented by +// EvalNodes to allow filtering of sub-elements. Note that this isn't +// a common thing to implement and you probably don't need it. +type EvalNodeFilterable interface { + EvalNode + Filter(EvalNodeFilterFunc) +} + +// EvalFilter runs the filter on the given node and returns the +// final filtered value. This should be called rather than checking +// the EvalNode directly since this will properly handle EvalNodeFilterables. +func EvalFilter(node EvalNode, fn EvalNodeFilterFunc) EvalNode { + if f, ok := node.(EvalNodeFilterable); ok { + f.Filter(fn) + return node + } + + return fn(node) +} diff --git a/terraform/eval_filter_operation.go b/terraform/eval_filter_operation.go new file mode 100644 index 000000000..1a55f024a --- /dev/null +++ b/terraform/eval_filter_operation.go @@ -0,0 +1,49 @@ +package terraform + +// EvalNodeOpFilterable is an interface that EvalNodes can implement +// to be filterable by the operation that is being run on Terraform. +type EvalNodeOpFilterable interface { + IncludeInOp(walkOperation) bool +} + +// EvalNodeFilterOp returns a filter function that filters nodes that +// include themselves in specific operations. +func EvalNodeFilterOp(op walkOperation) EvalNodeFilterFunc { + return func(n EvalNode) EvalNode { + include := true + if of, ok := n.(EvalNodeOpFilterable); ok { + include = of.IncludeInOp(op) + } + if include { + return n + } + + return EvalNoop{} + } +} + +// EvalOpFilter is an EvalNode implementation that is a proxy to +// another node but filters based on the operation. +type EvalOpFilter struct { + // Ops is the list of operations to include this node in. + Ops []walkOperation + + // Node is the node to execute + Node EvalNode +} + +// TODO: test +func (n *EvalOpFilter) Eval(ctx EvalContext) (interface{}, error) { + return EvalRaw(n.Node, ctx) +} + +// EvalNodeOpFilterable impl. +func (n *EvalOpFilter) IncludeInOp(op walkOperation) bool { + for _, v := range n.Ops { + if v == op { + return true + } + } + + return false +} diff --git a/terraform/eval_if.go b/terraform/eval_if.go new file mode 100644 index 000000000..c96e13229 --- /dev/null +++ b/terraform/eval_if.go @@ -0,0 +1,21 @@ +package terraform + +// EvalIf is an EvalNode that is a conditional. +type EvalIf struct { + If func(EvalContext) (bool, error) + Node EvalNode +} + +// TODO: test +func (n *EvalIf) Eval(ctx EvalContext) (interface{}, error) { + yes, err := n.If(ctx) + if err != nil { + return nil, err + } + + if yes { + return EvalRaw(n.Node, ctx) + } + + return nil, nil +} diff --git a/terraform/eval_interpolate.go b/terraform/eval_interpolate.go new file mode 100644 index 000000000..063473079 --- /dev/null +++ b/terraform/eval_interpolate.go @@ -0,0 +1,26 @@ +package terraform + +import ( + "github.com/hashicorp/terraform/config" +) + +// EvalInterpolate is an EvalNode implementation that takes a raw +// configuration and interpolates it. +type EvalInterpolate struct { + Config *config.RawConfig + Resource *Resource + Output **ResourceConfig +} + +func (n *EvalInterpolate) Eval(ctx EvalContext) (interface{}, error) { + rc, err := ctx.Interpolate(n.Config, n.Resource) + if err != nil { + return nil, err + } + + if n.Output != nil { + *n.Output = rc + } + + return nil, nil +} diff --git a/terraform/eval_interpolate_test.go b/terraform/eval_interpolate_test.go new file mode 100644 index 000000000..4b01ea462 --- /dev/null +++ b/terraform/eval_interpolate_test.go @@ -0,0 +1,37 @@ +package terraform + +import ( + "reflect" + "testing" + + "github.com/hashicorp/terraform/config" +) + +func TestEvalInterpolate_impl(t *testing.T) { + var _ EvalNode = new(EvalInterpolate) +} + +func TestEvalInterpolate(t *testing.T) { + config, err := config.NewRawConfig(map[string]interface{}{}) + if err != nil { + t.Fatalf("err: %s", err) + } + + var actual *ResourceConfig + n := &EvalInterpolate{Config: config, Output: &actual} + result := testResourceConfig(t, map[string]interface{}{}) + ctx := &MockEvalContext{InterpolateConfigResult: result} + if _, err := n.Eval(ctx); err != nil { + t.Fatalf("err: %s", err) + } + if actual != result { + t.Fatalf("bad: %#v", actual) + } + + if !ctx.InterpolateCalled { + t.Fatal("should be called") + } + if !reflect.DeepEqual(ctx.InterpolateConfig, config) { + t.Fatalf("bad: %#v", ctx.InterpolateConfig) + } +} diff --git a/terraform/eval_noop.go b/terraform/eval_noop.go new file mode 100644 index 000000000..f4bc8225c --- /dev/null +++ b/terraform/eval_noop.go @@ -0,0 +1,8 @@ +package terraform + +// EvalNoop is an EvalNode that does nothing. +type EvalNoop struct{} + +func (EvalNoop) Eval(EvalContext) (interface{}, error) { + return nil, nil +} diff --git a/terraform/eval_output.go b/terraform/eval_output.go new file mode 100644 index 000000000..0d9056d70 --- /dev/null +++ b/terraform/eval_output.go @@ -0,0 +1,63 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/config" +) + +// EvalWriteOutput is an EvalNode implementation that writes the output +// for the given name to the current state. +type EvalWriteOutput struct { + Name string + Value *config.RawConfig +} + +// TODO: test +func (n *EvalWriteOutput) Eval(ctx EvalContext) (interface{}, error) { + cfg, err := ctx.Interpolate(n.Value, nil) + if err != nil { + // Ignore it + } + + state, lock := ctx.State() + if state == nil { + return nil, fmt.Errorf("cannot write state to nil state") + } + + // Get a write lock so we can access this instance + lock.Lock() + defer lock.Unlock() + + // Look for the module state. If we don't have one, create it. + mod := state.ModuleByPath(ctx.Path()) + if mod == nil { + mod = state.AddModule(ctx.Path()) + } + + // Get the value from the config + var valueRaw interface{} = config.UnknownVariableValue + if cfg != nil { + var ok bool + valueRaw, ok = cfg.Get("value") + if !ok { + valueRaw = "" + } + if cfg.IsComputed("value") { + valueRaw = config.UnknownVariableValue + } + } + + // If it is a list of values, get the first one + if list, ok := valueRaw.([]interface{}); ok { + valueRaw = list[0] + } + if _, ok := valueRaw.(string); !ok { + return nil, fmt.Errorf("output %s is not a string", n.Name) + } + + // Write the output + mod.Outputs[n.Name] = valueRaw.(string) + + return nil, nil +} diff --git a/terraform/eval_provider.go b/terraform/eval_provider.go new file mode 100644 index 000000000..bc9a2652c --- /dev/null +++ b/terraform/eval_provider.go @@ -0,0 +1,108 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/config" +) + +// EvalConfigProvider is an EvalNode implementation that configures +// a provider that is already initialized and retrieved. +type EvalConfigProvider struct { + Provider string + Config **ResourceConfig +} + +func (n *EvalConfigProvider) Eval(ctx EvalContext) (interface{}, error) { + cfg := *n.Config + + // If we have a configuration set, then use that + if input := ctx.ProviderInput(n.Provider); input != nil { + rc, err := config.NewRawConfig(input) + if err != nil { + return nil, err + } + + merged := cfg.raw.Merge(rc) + cfg = NewResourceConfig(merged) + } + + // Get the parent configuration if there is one + if parent := ctx.ParentProviderConfig(n.Provider); parent != nil { + merged := cfg.raw.Merge(parent.raw) + cfg = NewResourceConfig(merged) + } + + return nil, ctx.ConfigureProvider(n.Provider, cfg) +} + +// EvalInitProvider is an EvalNode implementation that initializes a provider +// and returns nothing. The provider can be retrieved again with the +// EvalGetProvider node. +type EvalInitProvider struct { + Name string +} + +func (n *EvalInitProvider) Eval(ctx EvalContext) (interface{}, error) { + return ctx.InitProvider(n.Name) +} + +// EvalGetProvider is an EvalNode implementation that retrieves an already +// initialized provider instance for the given name. +type EvalGetProvider struct { + Name string + Output *ResourceProvider +} + +func (n *EvalGetProvider) Eval(ctx EvalContext) (interface{}, error) { + result := ctx.Provider(n.Name) + if result == nil { + return nil, fmt.Errorf("provider %s not initialized", n.Name) + } + + if n.Output != nil { + *n.Output = result + } + + return nil, nil +} + +// EvalInputProvider is an EvalNode implementation that asks for input +// for the given provider configurations. +type EvalInputProvider struct { + Name string + Provider *ResourceProvider + Config *config.RawConfig +} + +func (n *EvalInputProvider) Eval(ctx EvalContext) (interface{}, error) { + // If we already configured this provider, then don't do this again + if v := ctx.ProviderInput(n.Name); v != nil { + return nil, nil + } + + rc := NewResourceConfig(n.Config) + rc.Config = make(map[string]interface{}) + + // Wrap the input into a namespace + input := &PrefixUIInput{ + IdPrefix: fmt.Sprintf("provider.%s", n.Name), + QueryPrefix: fmt.Sprintf("provider.%s.", n.Name), + UIInput: ctx.Input(), + } + + // Go through each provider and capture the input necessary + // to satisfy it. + config, err := (*n.Provider).Input(input, rc) + if err != nil { + return nil, fmt.Errorf( + "Error configuring %s: %s", n.Name, err) + } + + if config != nil && len(config.Config) > 0 { + // Set the configuration + ctx.SetProviderInput(n.Name, config.Config) + } + + return nil, nil +} diff --git a/terraform/eval_provider_test.go b/terraform/eval_provider_test.go new file mode 100644 index 000000000..849e434a6 --- /dev/null +++ b/terraform/eval_provider_test.go @@ -0,0 +1,72 @@ +package terraform + +import ( + "reflect" + "testing" +) + +func TestEvalConfigProvider_impl(t *testing.T) { + var _ EvalNode = new(EvalConfigProvider) +} + +func TestEvalConfigProvider(t *testing.T) { + config := testResourceConfig(t, map[string]interface{}{}) + provider := &MockResourceProvider{} + n := &EvalConfigProvider{Config: &config} + + ctx := &MockEvalContext{ProviderProvider: provider} + if _, err := n.Eval(ctx); err != nil { + t.Fatalf("err: %s", err) + } + + if !ctx.ConfigureProviderCalled { + t.Fatal("should be called") + } + if !reflect.DeepEqual(ctx.ConfigureProviderConfig, config) { + t.Fatalf("bad: %#v", ctx.ConfigureProviderConfig) + } +} + +func TestEvalInitProvider_impl(t *testing.T) { + var _ EvalNode = new(EvalInitProvider) +} + +func TestEvalInitProvider(t *testing.T) { + n := &EvalInitProvider{Name: "foo"} + provider := &MockResourceProvider{} + ctx := &MockEvalContext{InitProviderProvider: provider} + if _, err := n.Eval(ctx); err != nil { + t.Fatalf("err: %s", err) + } + + if !ctx.InitProviderCalled { + t.Fatal("should be called") + } + if ctx.InitProviderName != "foo" { + t.Fatalf("bad: %#v", ctx.InitProviderName) + } +} + +func TestEvalGetProvider_impl(t *testing.T) { + var _ EvalNode = new(EvalGetProvider) +} + +func TestEvalGetProvider(t *testing.T) { + var actual ResourceProvider + n := &EvalGetProvider{Name: "foo", Output: &actual} + provider := &MockResourceProvider{} + ctx := &MockEvalContext{ProviderProvider: provider} + if _, err := n.Eval(ctx); err != nil { + t.Fatalf("err: %s", err) + } + if actual != provider { + t.Fatalf("bad: %#v", actual) + } + + if !ctx.ProviderCalled { + t.Fatal("should be called") + } + if ctx.ProviderName != "foo" { + t.Fatalf("bad: %#v", ctx.ProviderName) + } +} diff --git a/terraform/eval_provisioner.go b/terraform/eval_provisioner.go new file mode 100644 index 000000000..362a26e84 --- /dev/null +++ b/terraform/eval_provisioner.go @@ -0,0 +1,36 @@ +package terraform + +import ( + "fmt" +) + +// EvalInitProvisioner is an EvalNode implementation that initializes a provisioner +// and returns nothing. The provisioner can be retrieved again with the +// EvalGetProvisioner node. +type EvalInitProvisioner struct { + Name string +} + +func (n *EvalInitProvisioner) Eval(ctx EvalContext) (interface{}, error) { + return ctx.InitProvisioner(n.Name) +} + +// EvalGetProvisioner is an EvalNode implementation that retrieves an already +// initialized provisioner instance for the given name. +type EvalGetProvisioner struct { + Name string + Output *ResourceProvisioner +} + +func (n *EvalGetProvisioner) Eval(ctx EvalContext) (interface{}, error) { + result := ctx.Provisioner(n.Name) + if result == nil { + return nil, fmt.Errorf("provisioner %s not initialized", n.Name) + } + + if n.Output != nil { + *n.Output = result + } + + return result, nil +} diff --git a/terraform/eval_provisioner_test.go b/terraform/eval_provisioner_test.go new file mode 100644 index 000000000..1225c5abb --- /dev/null +++ b/terraform/eval_provisioner_test.go @@ -0,0 +1,49 @@ +package terraform + +import ( + "testing" +) + +func TestEvalInitProvisioner_impl(t *testing.T) { + var _ EvalNode = new(EvalInitProvisioner) +} + +func TestEvalInitProvisioner(t *testing.T) { + n := &EvalInitProvisioner{Name: "foo"} + provisioner := &MockResourceProvisioner{} + ctx := &MockEvalContext{InitProvisionerProvisioner: provisioner} + if _, err := n.Eval(ctx); err != nil { + t.Fatalf("err: %s", err) + } + + if !ctx.InitProvisionerCalled { + t.Fatal("should be called") + } + if ctx.InitProvisionerName != "foo" { + t.Fatalf("bad: %#v", ctx.InitProvisionerName) + } +} + +func TestEvalGetProvisioner_impl(t *testing.T) { + var _ EvalNode = new(EvalGetProvisioner) +} + +func TestEvalGetProvisioner(t *testing.T) { + var actual ResourceProvisioner + n := &EvalGetProvisioner{Name: "foo", Output: &actual} + provisioner := &MockResourceProvisioner{} + ctx := &MockEvalContext{ProvisionerProvisioner: provisioner} + if _, err := n.Eval(ctx); err != nil { + t.Fatalf("err: %s", err) + } + if actual != provisioner { + t.Fatalf("bad: %#v", actual) + } + + if !ctx.ProvisionerCalled { + t.Fatal("should be called") + } + if ctx.ProvisionerName != "foo" { + t.Fatalf("bad: %#v", ctx.ProvisionerName) + } +} diff --git a/terraform/eval_refresh.go b/terraform/eval_refresh.go new file mode 100644 index 000000000..3d25ecc8b --- /dev/null +++ b/terraform/eval_refresh.go @@ -0,0 +1,54 @@ +package terraform + +import ( + "log" +) + +// EvalRefresh is an EvalNode implementation that does a refresh for +// a resource. +type EvalRefresh struct { + Provider *ResourceProvider + State **InstanceState + Info *InstanceInfo + Output **InstanceState +} + +// TODO: test +func (n *EvalRefresh) Eval(ctx EvalContext) (interface{}, error) { + provider := *n.Provider + state := *n.State + + // If we have no state, we don't do any refreshing + if state == nil { + log.Printf("[DEBUG] refresh: %s: no state, not refreshing", n.Info.Id) + return nil, nil + } + + // Call pre-refresh hook + err := ctx.Hook(func(h Hook) (HookAction, error) { + return h.PreRefresh(n.Info, state) + }) + if err != nil { + return nil, err + } + + // Refresh! + state, err = provider.Refresh(n.Info, state) + if err != nil { + return nil, err + } + + // Call post-refresh hook + err = ctx.Hook(func(h Hook) (HookAction, error) { + return h.PostRefresh(n.Info, state) + }) + if err != nil { + return nil, err + } + + if n.Output != nil { + *n.Output = state + } + + return nil, nil +} diff --git a/terraform/eval_resource.go b/terraform/eval_resource.go new file mode 100644 index 000000000..5eca6782a --- /dev/null +++ b/terraform/eval_resource.go @@ -0,0 +1,13 @@ +package terraform + +// EvalInstanceInfo is an EvalNode implementation that fills in the +// InstanceInfo as much as it can. +type EvalInstanceInfo struct { + Info *InstanceInfo +} + +// TODO: test +func (n *EvalInstanceInfo) Eval(ctx EvalContext) (interface{}, error) { + n.Info.ModulePath = ctx.Path() + return nil, nil +} diff --git a/terraform/eval_sequence.go b/terraform/eval_sequence.go new file mode 100644 index 000000000..6c3c6a620 --- /dev/null +++ b/terraform/eval_sequence.go @@ -0,0 +1,23 @@ +package terraform + +// EvalSequence is an EvalNode that evaluates in sequence. +type EvalSequence struct { + Nodes []EvalNode +} + +func (n *EvalSequence) Eval(ctx EvalContext) (interface{}, error) { + for _, n := range n.Nodes { + if _, err := EvalRaw(n, ctx); err != nil { + return nil, err + } + } + + return nil, nil +} + +// EvalNodeFilterable impl. +func (n *EvalSequence) Filter(fn EvalNodeFilterFunc) { + for i, node := range n.Nodes { + n.Nodes[i] = fn(node) + } +} diff --git a/terraform/eval_sequence_test.go b/terraform/eval_sequence_test.go new file mode 100644 index 000000000..972f0cd6f --- /dev/null +++ b/terraform/eval_sequence_test.go @@ -0,0 +1,9 @@ +package terraform + +import ( + "testing" +) + +func TestEvalSequence_impl(t *testing.T) { + var _ EvalNodeFilterable = new(EvalSequence) +} diff --git a/terraform/eval_state.go b/terraform/eval_state.go new file mode 100644 index 000000000..bf8fec080 --- /dev/null +++ b/terraform/eval_state.go @@ -0,0 +1,196 @@ +package terraform + +import ( + "fmt" +) + +// EvalReadState is an EvalNode implementation that reads the +// InstanceState for a specific resource out of the state. +type EvalReadState struct { + Name string + Tainted bool + TaintedIndex int + Output **InstanceState +} + +// TODO: test +func (n *EvalReadState) Eval(ctx EvalContext) (interface{}, error) { + state, lock := ctx.State() + + // Get a read lock so we can access this instance + lock.RLock() + defer lock.RUnlock() + + // Look for the module state. If we don't have one, then it doesn't matter. + mod := state.ModuleByPath(ctx.Path()) + if mod == nil { + return nil, nil + } + + // Look for the resource state. If we don't have one, then it is okay. + rs := mod.Resources[n.Name] + if rs == nil { + return nil, nil + } + + var result *InstanceState + if !n.Tainted { + // Return the primary + result = rs.Primary + } else { + // Get the index. If it is negative, then we get the last one + idx := n.TaintedIndex + if idx < 0 { + idx = len(rs.Tainted) - 1 + } + + if idx < len(rs.Tainted) { + // Return the proper tainted resource + result = rs.Tainted[n.TaintedIndex] + } + } + + // Write the result to the output pointer + if n.Output != nil { + *n.Output = result + } + + return result, nil +} + +// EvalWriteState is an EvalNode implementation that reads the +// InstanceState for a specific resource out of the state. +type EvalWriteState struct { + Name string + ResourceType string + Dependencies []string + State **InstanceState + Tainted *bool + TaintedIndex int + TaintedClearPrimary bool +} + +// TODO: test +func (n *EvalWriteState) Eval(ctx EvalContext) (interface{}, error) { + state, lock := ctx.State() + if state == nil { + return nil, fmt.Errorf("cannot write state to nil state") + } + + // Get a write lock so we can access this instance + lock.Lock() + defer lock.Unlock() + + // Look for the module state. If we don't have one, create it. + mod := state.ModuleByPath(ctx.Path()) + if mod == nil { + mod = state.AddModule(ctx.Path()) + } + + // Look for the resource state. + rs := mod.Resources[n.Name] + if rs == nil { + rs = &ResourceState{} + rs.init() + mod.Resources[n.Name] = rs + } + rs.Type = n.ResourceType + rs.Dependencies = n.Dependencies + + if n.Tainted != nil && *n.Tainted { + if n.TaintedIndex != -1 { + rs.Tainted[n.TaintedIndex] = *n.State + } else { + rs.Tainted = append(rs.Tainted, *n.State) + } + + if n.TaintedClearPrimary { + rs.Primary = nil + } + } else { + // Set the primary state + rs.Primary = *n.State + } + println(fmt.Sprintf("%#v", rs)) + + return nil, nil +} + +// EvalDeposeState is an EvalNode implementation that takes the primary +// out of a state and makes it tainted. This is done at the beggining of +// create-before-destroy calls so that the create can create while preserving +// the old state of the to-be-destroyed resource. +type EvalDeposeState struct { + Name string +} + +// TODO: test +func (n *EvalDeposeState) Eval(ctx EvalContext) (interface{}, error) { + state, lock := ctx.State() + + // Get a read lock so we can access this instance + lock.RLock() + defer lock.RUnlock() + + // Look for the module state. If we don't have one, then it doesn't matter. + mod := state.ModuleByPath(ctx.Path()) + if mod == nil { + return nil, nil + } + + // Look for the resource state. If we don't have one, then it is okay. + rs := mod.Resources[n.Name] + if rs == nil { + return nil, nil + } + + // If we don't have a primary, we have nothing to depose + if rs.Primary == nil { + return nil, nil + } + + // Depose to the tainted + rs.Tainted = append(rs.Tainted, rs.Primary) + rs.Primary = nil + + return nil, nil +} + +// EvalUndeposeState is an EvalNode implementation that reads the +// InstanceState for a specific resource out of the state. +type EvalUndeposeState struct { + Name string +} + +// TODO: test +func (n *EvalUndeposeState) Eval(ctx EvalContext) (interface{}, error) { + state, lock := ctx.State() + + // Get a read lock so we can access this instance + lock.RLock() + defer lock.RUnlock() + + // Look for the module state. If we don't have one, then it doesn't matter. + mod := state.ModuleByPath(ctx.Path()) + if mod == nil { + return nil, nil + } + + // Look for the resource state. If we don't have one, then it is okay. + rs := mod.Resources[n.Name] + if rs == nil { + return nil, nil + } + + // If we don't have any tainted, then we don't have anything to do + if len(rs.Tainted) == 0 { + return nil, nil + } + + // Undepose to the tainted + idx := len(rs.Tainted) - 1 + rs.Primary = rs.Tainted[idx] + rs.Tainted[idx] = nil + + return nil, nil +} diff --git a/terraform/eval_test.go b/terraform/eval_test.go new file mode 100644 index 000000000..29ae25843 --- /dev/null +++ b/terraform/eval_test.go @@ -0,0 +1,40 @@ +package terraform + +import ( + "testing" +) + +func TestMockEvalContext_impl(t *testing.T) { + var _ EvalContext = new(MockEvalContext) +} + +func TestEval(t *testing.T) { + var result int + n := &testEvalAdd{ + Items: []int{10, 32}, + Result: &result, + } + + if _, err := Eval(n, nil); err != nil { + t.Fatalf("err: %s", err) + } + + if result != 42 { + t.Fatalf("bad: %#v", result) + } +} + +type testEvalAdd struct { + Items []int + Result *int +} + +func (n *testEvalAdd) Eval(ctx EvalContext) (interface{}, error) { + result := 0 + for _, item := range n.Items { + result += item + } + + *n.Result = result + return nil, nil +} diff --git a/terraform/eval_validate.go b/terraform/eval_validate.go new file mode 100644 index 000000000..c6c1f20ba --- /dev/null +++ b/terraform/eval_validate.go @@ -0,0 +1,141 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/config" +) + +// EvalValidateError is the error structure returned if there were +// validation errors. +type EvalValidateError struct { + Warnings []string + Errors []error +} + +func (e *EvalValidateError) Error() string { + return fmt.Sprintf("Warnings: %s. Errors: %s", e.Warnings, e.Errors) +} + +// EvalValidateCount is an EvalNode implementation that validates +// the count of a resource. +type EvalValidateCount struct { + Resource *config.Resource +} + +// TODO: test +func (n *EvalValidateCount) Eval(ctx EvalContext) (interface{}, error) { + var count int + var errs []error + var err error + if _, err := ctx.Interpolate(n.Resource.RawCount, nil); err != nil { + errs = append(errs, fmt.Errorf( + "Failed to interpolate count: %s", err)) + goto RETURN + } + + count, err = n.Resource.Count() + if err != nil { + // If we can't get the count during validation, then + // just replace it with the number 1. + c := n.Resource.RawCount.Config() + c[n.Resource.RawCount.Key] = "1" + count = 1 + } + + if count < 0 { + errs = append(errs, fmt.Errorf( + "Count is less than zero: %d", count)) + } + +RETURN: + return nil, &EvalValidateError{ + Errors: errs, + } +} + +// EvalValidateProvider is an EvalNode implementation that validates +// the configuration of a resource. +type EvalValidateProvider struct { + ProviderName string + Provider *ResourceProvider + Config **ResourceConfig +} + +func (n *EvalValidateProvider) Eval(ctx EvalContext) (interface{}, error) { + provider := *n.Provider + config := *n.Config + + // Get the parent configuration if there is one + if parent := ctx.ParentProviderConfig(n.ProviderName); parent != nil { + merged := parent.raw.Merge(config.raw) + config = NewResourceConfig(merged) + } + + warns, errs := provider.Validate(config) + if len(warns) == 0 && len(errs) == 0 { + return nil, nil + } + + return nil, &EvalValidateError{ + Warnings: warns, + Errors: errs, + } +} + +// EvalValidateProvisioner is an EvalNode implementation that validates +// the configuration of a resource. +type EvalValidateProvisioner struct { + Provisioner *ResourceProvisioner + Config **ResourceConfig +} + +func (n *EvalValidateProvisioner) Eval(ctx EvalContext) (interface{}, error) { + provisioner := *n.Provisioner + config := *n.Config + warns, errs := provisioner.Validate(config) + if len(warns) == 0 && len(errs) == 0 { + return nil, nil + } + + return nil, &EvalValidateError{ + Warnings: warns, + Errors: errs, + } +} + +// EvalValidateResource is an EvalNode implementation that validates +// the configuration of a resource. +type EvalValidateResource struct { + Provider *ResourceProvider + Config **ResourceConfig + ResourceName string + ResourceType string +} + +func (n *EvalValidateResource) Eval(ctx EvalContext) (interface{}, error) { + // TODO: test + + provider := *n.Provider + cfg := *n.Config + warns, errs := provider.ValidateResource(n.ResourceType, cfg) + + // If the resouce name doesn't match the name regular + // expression, show a warning. + if !config.NameRegexp.Match([]byte(n.ResourceName)) { + warns = append(warns, fmt.Sprintf( + "%s: resource name can only contain letters, numbers, "+ + "dashes, and underscores.\n"+ + "This will be an error in Terraform 0.4", + n.ResourceName)) + } + + if len(warns) == 0 && len(errs) == 0 { + return nil, nil + } + + return nil, &EvalValidateError{ + Warnings: warns, + Errors: errs, + } +} diff --git a/terraform/eval_variable.go b/terraform/eval_variable.go new file mode 100644 index 000000000..ebaf2eabb --- /dev/null +++ b/terraform/eval_variable.go @@ -0,0 +1,46 @@ +package terraform + +import ( + "github.com/hashicorp/terraform/config" +) + +// EvalSetVariables is an EvalNode implementation that sets the variables +// explicitly for interpolation later. +type EvalSetVariables struct { + Variables map[string]string +} + +// TODO: test +func (n *EvalSetVariables) Eval(ctx EvalContext) (interface{}, error) { + ctx.SetVariables(n.Variables) + return nil, nil +} + +// EvalVariableBlock is an EvalNode implementation that evaluates the +// given configuration, and uses the final values as a way to set the +// mapping. +type EvalVariableBlock struct { + Config **ResourceConfig + Variables map[string]string +} + +// TODO: test +func (n *EvalVariableBlock) Eval(ctx EvalContext) (interface{}, error) { + // Clear out the existing mapping + for k, _ := range n.Variables { + delete(n.Variables, k) + } + + // Get our configuration + rc := *n.Config + for k, v := range rc.Config { + n.Variables[k] = v.(string) + } + for k, _ := range rc.Raw { + if _, ok := n.Variables[k]; !ok { + n.Variables[k] = config.UnknownVariableValue + } + } + + return nil, nil +} diff --git a/terraform/evaltree_provider.go b/terraform/evaltree_provider.go new file mode 100644 index 000000000..89937d562 --- /dev/null +++ b/terraform/evaltree_provider.go @@ -0,0 +1,61 @@ +package terraform + +import ( + "github.com/hashicorp/terraform/config" +) + +// ProviderEvalTree returns the evaluation tree for initializing and +// configuring providers. +func ProviderEvalTree(n string, config *config.RawConfig) EvalNode { + var provider ResourceProvider + var resourceConfig *ResourceConfig + + seq := make([]EvalNode, 0, 5) + seq = append(seq, &EvalInitProvider{Name: n}) + + // Input stuff + seq = append(seq, &EvalOpFilter{ + Ops: []walkOperation{walkInput}, + Node: &EvalSequence{ + Nodes: []EvalNode{ + &EvalGetProvider{ + Name: n, + Output: &provider, + }, + &EvalInputProvider{ + Name: n, + Provider: &provider, + Config: config, + }, + }, + }, + }) + + // Apply stuff + seq = append(seq, &EvalOpFilter{ + Ops: []walkOperation{walkValidate, walkRefresh, walkPlan, walkApply}, + Node: &EvalSequence{ + Nodes: []EvalNode{ + &EvalGetProvider{ + Name: n, + Output: &provider, + }, + &EvalInterpolate{ + Config: config, + Output: &resourceConfig, + }, + &EvalValidateProvider{ + ProviderName: n, + Provider: &provider, + Config: &resourceConfig, + }, + &EvalConfigProvider{ + Provider: n, + Config: &resourceConfig, + }, + }, + }, + }) + + return &EvalSequence{Nodes: seq} +} diff --git a/terraform/graph.go b/terraform/graph.go index 90db2f35a..cf931fb1f 100644 --- a/terraform/graph.go +++ b/terraform/graph.go @@ -1,2038 +1,215 @@ package terraform import ( - "errors" "fmt" "log" - "sort" "strings" + "sync" - "github.com/hashicorp/terraform/config" - "github.com/hashicorp/terraform/config/module" - "github.com/hashicorp/terraform/depgraph" - "github.com/hashicorp/terraform/helper/multierror" + "github.com/hashicorp/terraform/dag" ) -// GraphOpts are options used to create the resource graph that Terraform -// walks to make changes to infrastructure. -// -// Depending on what options are set, the resulting graph will come in -// varying degrees of completeness. -type GraphOpts struct { - // Config is the configuration from which to build the basic graph. - // This is the only required item. - //Config *config.Config +// RootModuleName is the name given to the root module implicitly. +const RootModuleName = "root" - // Module is the relative root of a module tree for this graph. This - // is the only required item. This should always be the absolute root - // of the tree. ModulePath below should be used to constrain the depth. - // - // ModulePath specifies the place in the tree where Module exists. - // This is used for State lookups. - Module *module.Tree - ModulePath []string +// RootModulePath is the path for the root module. +var RootModulePath = []string{RootModuleName} - // Diff of changes that will be applied to the given state. This will - // associate a ResourceDiff with applicable resources. Additionally, - // new resource nodes representing resource destruction may be inserted - // into the graph. - Diff *Diff +// Graph represents the graph that Terraform uses to represent resources +// and their dependencies. Each graph represents only one module, but it +// can contain further modules, which themselves have their own graph. +type Graph struct { + // Graph is the actual DAG. This is embedded so you can call the DAG + // methods directly. + dag.AcyclicGraph - // State, if present, will make the ResourceState available on each - // resource node. Additionally, any orphans will be added automatically - // to the graph. - // - // Note: the state will be modified so it is initialized with basic - // empty states for all modules/resources in this graph. If you call prune - // later, these will be removed, but the graph adds important metadata. - State *State + // Path is the path in the module tree that this Graph represents. + // The root is represented by a single element list containing + // RootModuleName + Path []string - // Providers is a mapping of prefixes to a resource provider. If given, - // resource providers will be found, initialized, and associated to the - // resources in the graph. - // - // This will also potentially insert new nodes into the graph for - // the configuration of resource providers. - Providers map[string]ResourceProviderFactory + // dependableMap is a lookaside table for fast lookups for connecting + // dependencies by their GraphNodeDependable value to avoid O(n^3)-like + // situations and turn them into O(1) with respect to the number of new + // edges. + dependableMap map[string]dag.Vertex - // Provisioners is a mapping of names to a resource provisioner. - // These must be provided to support resource provisioners. - Provisioners map[string]ResourceProvisionerFactory - - // parent specifies the parent graph if there is one. This should not be - // set manually. - parent *depgraph.Graph + once sync.Once } -// GraphRootNode is the name of the root node in the Terraform resource -// graph. This node is just a placemarker and has no associated functionality. -const GraphRootNode = "root" +// Add is the same as dag.Graph.Add. +func (g *Graph) Add(v dag.Vertex) dag.Vertex { + g.once.Do(g.init) -// GraphMeta is the metadata attached to the graph itself. -type GraphMeta struct { - // ModulePath is the path of the module that this graph represents. - ModulePath []string -} + // Call upwards to add it to the actual graph + g.Graph.Add(v) -// GraphNodeModule is a node type in the graph that represents a module -// that will be created/managed. -type GraphNodeModule struct { - Config *config.Module - Path []string - Graph *depgraph.Graph - State *ModuleState - Flags ResourceFlag -} - -// GraphNodeResource is a node type in the graph that represents a resource -// that will be created or managed. Unlike the GraphNodeResourceMeta node, -// this represents a _single_, _resource_ to be managed, not a set of resources -// or a component of a resource. -type GraphNodeResource struct { - Index int - Config *config.Resource - Resource *Resource - ResourceProviderNode string - - // All the fields below are related to expansion. These are set by - // the graph but aren't useful individually. - ExpandMode ResourceExpandMode - Diff *ModuleDiff - State *ModuleState -} - -// GraphNodeResourceProvider is a node type in the graph that represents -// the configuration for a resource provider. -type GraphNodeResourceProvider struct { - ID string - Provider *graphSharedProvider -} - -// graphSharedProvider is a structure that stores a configuration -// with initialized providers and might be shared across different -// graphs in order to have only one instance of a provider. -type graphSharedProvider struct { - Config *config.ProviderConfig - Providers map[string]ResourceProvider - ProviderKeys []string - Parent *graphSharedProvider - - overrideConfig map[string]map[string]interface{} - parentNoun *depgraph.Noun -} - -// ResourceExpandMode specifies the expand behavior of the GraphNodeResource -// node. -type ResourceExpandMode byte - -const ( - ResourceExpandNone ResourceExpandMode = iota - ResourceExpandApply - ResourceExpandDestroy -) - -// Graph builds a dependency graph of all the resources for infrastructure -// change. -// -// This dependency graph shows the correct order that any resources need -// to be operated on. -// -// The Meta field of a graph Noun can contain one of the follow types. A -// description is next to each type to explain what it is. -// -// *GraphNodeResource - A resource. See the documentation of this -// struct for more details. -// *GraphNodeResourceProvider - A resource provider that needs to be -// configured at this point. -// -func Graph(opts *GraphOpts) (*depgraph.Graph, error) { - if opts.Module == nil { - return nil, errors.New("Module is required for Graph") - } - if opts.ModulePath == nil { - opts.ModulePath = rootModulePath - } - if !opts.Module.Loaded() { - return nil, errors.New("Module must be loaded") - } - - // Get the correct module in the tree that we're looking for. - currentModule := opts.Module - for _, n := range opts.ModulePath[1:] { - children := currentModule.Children() - currentModule = children[n] - } - - var conf *config.Config - if currentModule != nil { - conf = currentModule.Config() - } else { - conf = new(config.Config) - } - - // Get the state and diff of the module that we're working with. - var modDiff *ModuleDiff - var modState *ModuleState - if opts.Diff != nil { - modDiff = opts.Diff.ModuleByPath(opts.ModulePath) - } - if opts.State != nil { - modState = opts.State.ModuleByPath(opts.ModulePath) - } - - log.Printf("[DEBUG] Creating graph for path: %v", opts.ModulePath) - - g := new(depgraph.Graph) - g.Meta = &GraphMeta{ - ModulePath: opts.ModulePath, - } - - // First, build the initial resource graph. This only has the resources - // and no dependencies. This only adds resources that are in the config - // and not "orphans" (that are in the state, but not in the config). - graphAddConfigResources(g, conf, modState) - - if modState != nil { - // Next, add the state orphans if we have any - graphAddOrphans(g, conf, modState) - - // Add tainted resources if we have any. - graphAddTainted(g, modState) - } - - // Create the resource provider nodes for explicitly configured - // providers within our graph. - graphAddConfigProviderConfigs(g, conf) - - if opts.parent != nil { - // Add/merge the provider configurations from the parent so that - // we properly "inherit" providers. - graphAddParentProviderConfigs(g, opts.parent) - } - - // First pass matching resources to providers. This will allow us to - // determine what providers are missing. - graphMapResourceProviderId(g) - - if opts.Providers != nil { - // Add missing providers from the mapping. - if err := graphAddMissingResourceProviders(g, opts.Providers); err != nil { - return nil, err - } - - // Initialize all the providers - if err := graphInitResourceProviders(g, opts.Providers); err != nil { - return nil, err - } - - // Map the providers to resources - if err := graphMapResourceProviders(g); err != nil { - return nil, err + // If this is a depend-able node, then store the lookaside info + if dv, ok := v.(GraphNodeDependable); ok { + for _, n := range dv.DependableName() { + g.dependableMap[n] = v } } - // Add the modules that are in the configuration. - if err := graphAddConfigModules(g, conf, opts); err != nil { - return nil, err - } - - if opts.State != nil { - // Add module orphans if we have any of those - if ms := opts.State.Children(opts.ModulePath); len(ms) > 0 { - if err := graphAddModuleOrphans(g, conf, ms, opts); err != nil { - return nil, err - } - } - } - - // Add the orphan dependencies - graphAddOrphanDeps(g, modState) - - // Add the orphan module dependencies - graphAddOrphanModuleDeps(g, modState) - - // Add the provider dependencies - graphAddResourceProviderDeps(g) - - // Now, prune the providers that we aren't using. - graphPruneResourceProviders(g) - - // Add explicit dependsOn dependencies to the graph - graphAddExplicitDeps(g) - - // Setup the provisioners. These may have variable dependencies, - // and must be done before dependency setup - if err := graphMapResourceProvisioners(g, opts.Provisioners); err != nil { - return nil, err - } - - // Add all the variable dependencies - graphAddVariableDeps(g) - - // Build the root so that we have a single valid root - graphAddRoot(g) - - // If we have a diff, then make sure to add that in - if modDiff != nil { - if err := graphAddDiff(g, opts.Diff, modDiff); err != nil { - return nil, err - } - } - - // Encode the dependencies - graphEncodeDependencies(g) - - // Validate - if err := g.Validate(); err != nil { - return nil, err - } - - log.Printf( - "[DEBUG] Graph %v created and valid. %d nouns.", - opts.ModulePath, - len(g.Nouns)) - - return g, nil + return v } -// graphEncodeDependencies is used to initialize a State with a ResourceState -// for every resource. -// -// This method is very important to call because it will properly setup -// the ResourceState dependency information with data from the graph. This -// allows orphaned resources to be destroyed in the proper order. -func graphEncodeDependencies(g *depgraph.Graph) { - for _, n := range g.Nouns { - switch rn := n.Meta.(type) { - case *GraphNodeResource: - // If we are using create-before-destroy, there - // are some special depedencies injected on the - // deposed node that would cause a circular depedency - // chain if persisted. We must only handle the new node, - // node the deposed node. - r := rn.Resource - if r.Flags&FlagDeposed != 0 { - continue - } - - // Update the dependencies - var inject []string - for _, dep := range n.Deps { - switch target := dep.Target.Meta.(type) { - case *GraphNodeModule: - inject = append(inject, dep.Target.Name) - - case *GraphNodeResource: - if target.Resource.Id == r.Id { - continue - } - inject = append(inject, target.Resource.Id) - - case *GraphNodeResourceProvider: - // Do nothing - - default: - panic(fmt.Sprintf("Unknown graph node: %#v", dep.Target)) - } - } - - // Update the dependencies - r.Dependencies = inject - - case *GraphNodeModule: - // Update the dependencies - var inject []string - for _, dep := range n.Deps { - switch target := dep.Target.Meta.(type) { - case *GraphNodeModule: - if dep.Target.Name == n.Name { - continue - } - inject = append(inject, dep.Target.Name) - - case *GraphNodeResource: - inject = append(inject, target.Resource.Id) - - case *GraphNodeResourceProvider: - // Do nothing - - default: - panic(fmt.Sprintf("Unknown graph node: %#v", dep.Target)) - } - - } - - // Update the dependencies - if rn.State != nil { - rn.State.Dependencies = inject - } - } - } -} - -// graphAddConfigModules adds the modules from a configuration structure -// into the graph, expanding each to their own sub-graph. -func graphAddConfigModules( - g *depgraph.Graph, - c *config.Config, - opts *GraphOpts) error { - // Just short-circuit the whole thing if we don't have modules - if len(c.Modules) == 0 { +// ConnectDependent connects a GraphNodeDependent to all of its +// GraphNodeDependables. It returns the list of dependents it was +// unable to connect to. +func (g *Graph) ConnectDependent(raw dag.Vertex) []string { + v, ok := raw.(GraphNodeDependent) + if !ok { return nil } - // Build the list of nouns to add to the graph - nounsList := make([]*depgraph.Noun, 0, len(c.Modules)) - for _, m := range c.Modules { - if n, err := graphModuleNoun(m.Name, m, g, opts); err != nil { - return err - } else { - // Attach the module state if any - if opts.State != nil { - module := n.Meta.(*GraphNodeModule) - module.State = opts.State.ModuleByPath(module.Path) - if module.State == nil { - module.State = opts.State.AddModule(module.Path) - } - } - nounsList = append(nounsList, n) - } - } - - g.Nouns = append(g.Nouns, nounsList...) - return nil + return g.ConnectTo(v, v.DependentOn()) } -// configGraph turns a configuration structure into a dependency graph. -func graphAddConfigResources( - g *depgraph.Graph, c *config.Config, mod *ModuleState) { - meta := g.Meta.(*GraphMeta) - - // This tracks all the resource nouns - nounsList := make([]*depgraph.Noun, len(c.Resources)) - for i, r := range c.Resources { - name := r.Id() - - // Build the noun - nounsList[i] = &depgraph.Noun{ - Name: name, - Meta: &GraphNodeResource{ - Index: -1, - Config: r, - Resource: &Resource{ - Id: name, - Info: &InstanceInfo{ - Id: name, - ModulePath: meta.ModulePath, - Type: r.Type, - }, - }, - State: mod.View(name), - ExpandMode: ResourceExpandApply, - }, - } - - /* - TODO: probably did something important, bring it back somehow - resourceNouns := make([]*depgraph.Noun, r.Count) - for i := 0; i < r.Count; i++ { - name := r.Id() - index := -1 - - // If we have a count that is more than one, then make sure - // we suffix with the number of the resource that this is. - if r.Count > 1 { - name = fmt.Sprintf("%s.%d", name, i) - index = i - } - - var state *ResourceState - if mod != nil { - // Lookup the resource state - state = mod.Resources[name] - if state == nil { - if r.Count == 1 { - // If the count is one, check the state for ".0" - // appended, which might exist if we go from - // count > 1 to count == 1. - state = mod.Resources[r.Id()+".0"] - } else if i == 0 { - // If count is greater than one, check for state - // with just the ID, which might exist if we go - // from count == 1 to count > 1 - state = mod.Resources[r.Id()] - } - - // TODO(mitchellh): If one of the above works, delete - // the old style and just copy it to the new style. - } - } - - if state == nil { - state = &ResourceState{ - Type: r.Type, - } - } - - flags := FlagPrimary - if len(state.Tainted) > 0 { - flags |= FlagHasTainted - } - - resourceNouns[i] = &depgraph.Noun{ - Name: name, - Meta: &GraphNodeResource{ - Index: index, - Config: r, - Resource: &Resource{ - Id: name, - Info: &InstanceInfo{ - Id: name, - ModulePath: meta.ModulePath, - Type: r.Type, - }, - State: state.Primary, - Config: NewResourceConfig(r.RawConfig), - Flags: flags, - }, - }, - } - } - - // If we have more than one, then create a meta node to track - // the resources. - if r.Count > 1 { - metaNoun := &depgraph.Noun{ - Name: r.Id(), - Meta: &GraphNodeResourceMeta{ - ID: r.Id(), - Name: r.Name, - Type: r.Type, - Count: r.Count, - }, - } - - // Create the dependencies on this noun - for _, n := range resourceNouns { - metaNoun.Deps = append(metaNoun.Deps, &depgraph.Dependency{ - Name: n.Name, - Source: metaNoun, - Target: n, - }) - } - - // Assign it to the map so that we have it - nouns[metaNoun.Name] = metaNoun - } - - for _, n := range resourceNouns { - nouns[n.Name] = n - } - */ - } - - g.Name = "terraform" - g.Nouns = append(g.Nouns, nounsList...) -} - -// graphAddDiff takes an already-built graph of resources and adds the -// diffs to the resource nodes themselves. +// ConnectDependents goes through the graph, connecting all the +// GraphNodeDependents to GraphNodeDependables. This is safe to call +// multiple times. // -// This may also introduces new graph elements. If there are diffs that -// require a destroy, new elements may be introduced since destroy order -// is different than create order. For example, destroying a VPC requires -// destroying the VPC's subnets first, whereas creating a VPC requires -// doing it before the subnets are created. This function handles inserting -// these nodes for you. -func graphAddDiff(g *depgraph.Graph, gDiff *Diff, d *ModuleDiff) error { - var nlist []*depgraph.Noun - var modules []*depgraph.Noun - injected := make(map[*depgraph.Dependency]struct{}) - for _, n := range g.Nouns { - // A module is being destroyed if all it's resources are being - // destroyed (via a destroy plan) or if it is orphaned. Only in - // those cases do we need to handle depedency inversion. - if mod, ok := n.Meta.(*GraphNodeModule); ok { - md := gDiff.ModuleByPath(mod.Path) - if mod.Flags&FlagOrphan != 0 || (md != nil && md.Destroy) { - modules = append(modules, n) - } - continue - } - - rn, ok := n.Meta.(*GraphNodeResource) - if !ok { - continue - } - if rn.Resource.Flags&FlagTainted != 0 { - continue - } - - change := false - destroy := false - diffs := d.Instances(rn.Resource.Id) - if len(diffs) == 0 { - continue - } - for _, d := range diffs { - if d.Destroy { - destroy = true - } - - if len(d.Attributes) > 0 { - change = true - } - } - - // If we're expanding, save the diff so we can add it on later - if rn.ExpandMode > ResourceExpandNone { - rn.Diff = d - } - - // If we are not expanding, then we assign the - // instance diff to the resource. - var rd *InstanceDiff - if rn.ExpandMode == ResourceExpandNone { - rd = diffs[0] - } - - if destroy { - // If we're destroying, we create a new destroy node with - // the proper dependencies. Perform a dirty copy operation. - newNode := new(GraphNodeResource) - *newNode = *rn - newNode.Resource = new(Resource) - *newNode.Resource = *rn.Resource - - // Make the diff _just_ the destroy. - newNode.Resource.Diff = &InstanceDiff{Destroy: true} - - // Make sure ExpandDestroy is set if Expand - if newNode.ExpandMode == ResourceExpandApply { - newNode.ExpandMode = ResourceExpandDestroy - } - - // Create the new node - newN := &depgraph.Noun{ - Name: fmt.Sprintf("%s (destroy)", newNode.Resource.Id), - Meta: newNode, - } - newN.Deps = make([]*depgraph.Dependency, len(n.Deps)) - - // Copy all the dependencies and do a fixup later - copy(newN.Deps, n.Deps) - - // Append it to the list so we handle it later - nlist = append(nlist, newN) - - if rd != nil { - // Mark the old diff to not destroy since we handle that in - // the dedicated node. - newDiff := new(InstanceDiff) - *newDiff = *rd - newDiff.Destroy = false - rd = newDiff - } - - // The dependency ordering depends on if the CreateBeforeDestroy - // flag is enabled. If so, we must create the replacement first, - // and then destroy the old instance. - if rn.Config != nil && rn.Config.Lifecycle.CreateBeforeDestroy && change { - dep := &depgraph.Dependency{ - Name: n.Name, - Source: newN, - Target: n, - } - - // Add the old noun to the new noun dependencies so that - // the create happens before the destroy. - newN.Deps = append(newN.Deps, dep) - - // Mark that this dependency has been injected so that - // we do not invert the direction below. - injected[dep] = struct{}{} - - // Add a depedency from the root, since the create node - // does not depend on us - if g.Root != nil { - g.Root.Deps = append(g.Root.Deps, &depgraph.Dependency{ - Name: newN.Name, - Source: g.Root, - Target: newN, - }) - } - - // Set the ReplacePrimary flag on the new instance so that - // it will become the new primary, and Diposed flag on the - // existing instance so that it will step down - rn.Resource.Flags |= FlagReplacePrimary - newNode.Resource.Flags |= FlagDeposed - - // This logic is not intuitive, but we need to make the - // destroy depend upon any resources that depend on the - // create. The reason is suppose you have a LB depend on - // a web server. You need the order to be create, update LB, - // destroy. Without this, the update LB and destroy can - // be executed in an arbitrary order (likely in parallel). - incoming := g.DependsOn(n) - for _, inc := range incoming { - // Ignore the root... - if inc == g.Root { - continue - } - dep := &depgraph.Dependency{ - Name: inc.Name, - Source: newN, - Target: inc, - } - injected[dep] = struct{}{} - newN.Deps = append(newN.Deps, dep) - } - - } else { - dep := &depgraph.Dependency{ - Name: newN.Name, - Source: n, - Target: newN, - } - - // Add the new noun to our dependencies so that - // the destroy happens before the apply. - n.Deps = append(n.Deps, dep) - } - } - - rn.Resource.Diff = rd - } - - // Go through each resource and module and make sure we - // calculate all the dependencies properly. - invertDeps := [][]*depgraph.Noun{nlist, modules} - for _, list := range invertDeps { - for _, n := range list { - deps := n.Deps - num := len(deps) - for i := 0; i < num; i++ { - dep := deps[i] - - // Check if this dependency was just injected, otherwise - // we will incorrectly flip the depedency twice. - if _, ok := injected[dep]; ok { - continue - } - - switch target := dep.Target.Meta.(type) { - case *GraphNodeResource: - // If the other node is also being deleted, - // we must be deleted first. E.g. if A -> B, - // then when we create, B is created first then A. - // On teardown, A is destroyed first, then B. - // Thus we must flip our depedency and instead inject - // it on B. - for _, n2 := range nlist { - rn2 := n2.Meta.(*GraphNodeResource) - if target.Resource.Id == rn2.Resource.Id { - newDep := &depgraph.Dependency{ - Name: n.Name, - Source: n2, - Target: n, - } - injected[newDep] = struct{}{} - n2.Deps = append(n2.Deps, newDep) - break - } - } - - // Drop the dependency. We may have created - // an inverse depedency if the dependent resource - // is also being deleted, but this dependence is - // no longer required. - deps[i], deps[num-1] = deps[num-1], nil - num-- - i-- - - case *GraphNodeModule: - // We invert any module dependencies so we're destroyed - // first, before any modules are applied. - newDep := &depgraph.Dependency{ - Name: n.Name, - Source: dep.Target, - Target: n, - } - dep.Target.Deps = append(dep.Target.Deps, newDep) - - // Drop the dependency. We may have created - // an inverse depedency if the dependent resource - // is also being deleted, but this dependence is - // no longer required. - deps[i], deps[num-1] = deps[num-1], nil - num-- - i-- - case *GraphNodeResourceProvider: - // Keep these around, but fix up the source to be ourselves - // rather than the old node. - newDep := *dep - newDep.Source = n - deps[i] = &newDep - default: - panic(fmt.Errorf("Unhandled depedency type: %#v", dep.Target.Meta)) - } - } - n.Deps = deps[:num] - } - } - - // Add the nouns to the graph - g.Nouns = append(g.Nouns, nlist...) - - return nil -} - -// graphAddExplicitDeps adds the dependencies to the graph for the explicit -// dependsOn configurations. -func graphAddExplicitDeps(g *depgraph.Graph) { - depends := false - - rs := make(map[string]*depgraph.Noun) - for _, n := range g.Nouns { - rn, ok := n.Meta.(*GraphNodeResource) - if !ok { - continue - } - if rn.Config == nil { - // Orphan. It can't be depended on or have depends (explicit) - // anyways. - continue - } - - rs[rn.Resource.Id] = n - if rn.Config != nil && len(rn.Config.DependsOn) > 0 { - depends = true - } - } - - // If we didn't have any dependsOn, just return - if !depends { - return - } - - for _, n1 := range rs { - rn1 := n1.Meta.(*GraphNodeResource) - for _, d := range rn1.Config.DependsOn { - for _, n2 := range rs { - rn2 := n2.Meta.(*GraphNodeResource) - if rn2.Config.Id() != d { - continue - } - - n1.Deps = append(n1.Deps, &depgraph.Dependency{ - Name: d, - Source: n1, - Target: n2, - }) - } +// To get details on whether dependencies could be found/made, the more +// specific ConnectDependent should be used. +func (g *Graph) ConnectDependents() { + for _, v := range g.Vertices() { + if dv, ok := v.(GraphNodeDependent); ok { + g.ConnectDependent(dv) } } } -// graphAddMissingResourceProviders adds GraphNodeResourceProvider nodes for -// the resources that do not have an explicit resource provider specified -// because no provider configuration was given. -func graphAddMissingResourceProviders( - g *depgraph.Graph, - ps map[string]ResourceProviderFactory) error { - var errs []error +// ConnectFrom creates an edge by finding the source from a DependableName +// and connecting it to the specific vertex. +func (g *Graph) ConnectFrom(source string, target dag.Vertex) { + g.once.Do(g.init) - for _, n := range g.Nouns { - rn, ok := n.Meta.(*GraphNodeResource) - if !ok { - continue - } - if rn.ResourceProviderNode != "" { - continue - } - - prefixes := matchingPrefixes(rn.Resource.Info.Type, ps) - if len(prefixes) == 0 { - errs = append(errs, fmt.Errorf( - "No matching provider for type: %s", - rn.Resource.Info.Type)) - continue - } - - // The resource provider ID is simply the shortest matching - // prefix, since that'll give us the most resource providers - // to choose from. - id := prefixes[len(prefixes)-1] - rn.ResourceProviderNode = fmt.Sprintf("provider.%s", id) - - // If we don't have a matching noun for this yet, insert it. - if g.Noun(rn.ResourceProviderNode) == nil { - pn := &depgraph.Noun{ - Name: rn.ResourceProviderNode, - Meta: &GraphNodeResourceProvider{ - ID: id, - Provider: new(graphSharedProvider), - }, - } - g.Nouns = append(g.Nouns, pn) - } + if source := g.dependableMap[source]; source != nil { + g.Connect(dag.BasicEdge(source, target)) } - - if len(errs) > 0 { - return &multierror.Error{Errors: errs} - } - - return nil } -func graphAddModuleOrphans( - g *depgraph.Graph, - config *config.Config, - ms []*ModuleState, - opts *GraphOpts) error { - // Build a lookup map for the modules we do have defined - childrenKeys := make(map[string]struct{}) - for _, m := range config.Modules { - childrenKeys[m.Name] = struct{}{} - } +// ConnectTo connects a vertex to a raw string of targets that are the +// result of DependableName, and returns the list of targets that are missing. +func (g *Graph) ConnectTo(v dag.Vertex, targets []string) []string { + g.once.Do(g.init) - // Go through each of the child modules. If we don't have it in our - // config, it is an orphan. - var nounsList []*depgraph.Noun - for _, m := range ms { - k := m.Path[len(m.Path)-1] - if _, ok := childrenKeys[k]; ok { - // We have this module configured - continue - } - - if n, err := graphModuleNoun(k, nil, g, opts); err != nil { - return err + var missing []string + for _, t := range targets { + if dest := g.dependableMap[t]; dest != nil { + g.Connect(dag.BasicEdge(v, dest)) } else { - // Mark this module as being an orphan - module := n.Meta.(*GraphNodeModule) - module.Flags |= FlagOrphan - module.State = m - nounsList = append(nounsList, n) + missing = append(missing, t) } } - g.Nouns = append(g.Nouns, nounsList...) + return missing +} + +// Dependable finds the vertices in the graph that have the given dependable +// names and returns them. +func (g *Graph) Dependable(n string) dag.Vertex { + // TODO: do we need this? return nil } -// graphAddOrphanDeps adds the dependencies to the orphans based on their -// explicit Dependencies state. -func graphAddOrphanDeps(g *depgraph.Graph, mod *ModuleState) { - for _, n := range g.Nouns { - rn, ok := n.Meta.(*GraphNodeResource) - if !ok { - continue - } - if rn.Resource.Flags&FlagOrphan == 0 { - continue - } +// Walk walks the graph with the given walker for callbacks. The graph +// will be walked with full parallelism, so the walker should expect +// to be called in concurrently. +func (g *Graph) Walk(walker GraphWalker) error { + return g.walk(walker) +} - // If we have no dependencies, then just continue - rs := mod.Resources[n.Name] - if len(rs.Dependencies) == 0 { - continue - } - - for _, n2 := range g.Nouns { - // Don't ever depend on ourselves - if n2.Meta == n.Meta { - continue - } - - var compareName string - switch rn2 := n2.Meta.(type) { - case *GraphNodeModule: - compareName = n2.Name - case *GraphNodeResource: - compareName = rn2.Resource.Id - } - if compareName == "" { - continue - } - - for _, depName := range rs.Dependencies { - if !strings.HasPrefix(depName, compareName) { - continue - } - dep := &depgraph.Dependency{ - Name: depName, - Source: n, - Target: n2, - } - n.Deps = append(n.Deps, dep) - break - } - } +func (g *Graph) init() { + if g.dependableMap == nil { + g.dependableMap = make(map[string]dag.Vertex) } } -// graphAddOrphanModuleDeps adds the dependencies to the orphan -// modules based on their explicit Dependencies state. -func graphAddOrphanModuleDeps(g *depgraph.Graph, mod *ModuleState) { - for _, n := range g.Nouns { - module, ok := n.Meta.(*GraphNodeModule) - if !ok { - continue - } - if module.Flags&FlagOrphan == 0 { - continue - } +func (g *Graph) walk(walker GraphWalker) error { + // The callbacks for enter/exiting a graph + ctx := walker.EnterGraph(g) + defer walker.ExitGraph(g) - // If we have no dependencies, then just continue - if len(module.State.Dependencies) == 0 { - continue - } + // Get the path for logs + path := strings.Join(ctx.Path(), ".") - for _, n2 := range g.Nouns { - // Don't ever depend on ourselves - if n2.Meta == n.Meta { - continue + // Walk the graph. + var walkFn dag.WalkFunc + walkFn = func(v dag.Vertex) (rerr error) { + log.Printf("[DEBUG] vertex %s.%s: walking", path, dag.VertexName(v)) + + walker.EnterVertex(v) + defer func() { walker.ExitVertex(v, rerr) }() + + // If the node is eval-able, then evaluate it. + if ev, ok := v.(GraphNodeEvalable); ok { + tree := ev.EvalTree() + if tree == nil { + panic(fmt.Sprintf( + "%s.%s (%T): nil eval tree", path, dag.VertexName(v), v)) } - var compareName string - switch rn2 := n2.Meta.(type) { - case *GraphNodeModule: - compareName = n2.Name - case *GraphNodeResource: - compareName = rn2.Resource.Id - } - if compareName == "" { - continue - } - - for _, depName := range module.State.Dependencies { - if !strings.HasPrefix(depName, compareName) { - continue - } - dep := &depgraph.Dependency{ - Name: depName, - Source: n, - Target: n2, - } - n.Deps = append(n.Deps, dep) - break - } - } - } -} - -// graphAddOrphans adds the orphans to the graph. -func graphAddOrphans(g *depgraph.Graph, c *config.Config, mod *ModuleState) { - meta := g.Meta.(*GraphMeta) - - var nlist []*depgraph.Noun - for _, k := range mod.Orphans(c) { - rs := mod.Resources[k] - noun := &depgraph.Noun{ - Name: k, - Meta: &GraphNodeResource{ - Index: -1, - Resource: &Resource{ - Id: k, - Info: &InstanceInfo{ - Id: k, - ModulePath: meta.ModulePath, - Type: rs.Type, - }, - State: rs.Primary, - Config: NewResourceConfig(nil), - Flags: FlagOrphan, - }, - }, - } - - // Append it to the list so we handle it later - nlist = append(nlist, noun) - } - - // Add the nouns to the graph - g.Nouns = append(g.Nouns, nlist...) -} - -// graphAddParentProviderConfigs goes through and adds/merges provider -// configurations from the parent. -func graphAddParentProviderConfigs(g, parent *depgraph.Graph) { - var nounsList []*depgraph.Noun - for _, n := range parent.Nouns { - pn, ok := n.Meta.(*GraphNodeResourceProvider) - if !ok { - continue - } - - // If we have a provider configuration with the exact same - // name, then set specify the parent pointer to their shared - // config. - ourProviderRaw := g.Noun(n.Name) - - // If we don't have a matching configuration, then create one. - if ourProviderRaw == nil { - noun := &depgraph.Noun{ - Name: n.Name, - Meta: &GraphNodeResourceProvider{ - ID: pn.ID, - Provider: &graphSharedProvider{ - Parent: pn.Provider, - parentNoun: n, - }, - }, - } - - nounsList = append(nounsList, noun) - continue - } - - // If we have a matching configuration, then set the parent pointer - ourProvider := ourProviderRaw.Meta.(*GraphNodeResourceProvider) - ourProvider.Provider.Parent = pn.Provider - ourProvider.Provider.parentNoun = n - } - - g.Nouns = append(g.Nouns, nounsList...) -} - -// graphAddConfigProviderConfigs adds a GraphNodeResourceProvider for every -// `provider` configuration block. Note that a provider may exist that -// isn't used for any resources. These will be pruned later. -func graphAddConfigProviderConfigs(g *depgraph.Graph, c *config.Config) { - nounsList := make([]*depgraph.Noun, 0, len(c.ProviderConfigs)) - for _, pc := range c.ProviderConfigs { - noun := &depgraph.Noun{ - Name: fmt.Sprintf("provider.%s", pc.Name), - Meta: &GraphNodeResourceProvider{ - ID: pc.Name, - Provider: &graphSharedProvider{ - Config: pc, - }, - }, - } - - nounsList = append(nounsList, noun) - } - - // Add all the provider config nouns to the graph - g.Nouns = append(g.Nouns, nounsList...) -} - -// graphAddRoot adds a root element to the graph so that there is a single -// root to point to all the dependencies. -func graphAddRoot(g *depgraph.Graph) { - root := &depgraph.Noun{Name: GraphRootNode} - for _, n := range g.Nouns { - switch n.Meta.(type) { - case *GraphNodeResourceProvider: - // ResourceProviders don't need to be in the root deps because - // they're always pointed to by some resource. - continue - } - - root.Deps = append(root.Deps, &depgraph.Dependency{ - Name: n.Name, - Source: root, - Target: n, - }) - } - g.Nouns = append(g.Nouns, root) - g.Root = root -} - -// graphAddVariableDeps inspects all the nouns and adds any dependencies -// based on variable values. -func graphAddVariableDeps(g *depgraph.Graph) { - for _, n := range g.Nouns { - switch m := n.Meta.(type) { - case *GraphNodeModule: - if m.Config != nil { - vars := m.Config.RawConfig.Variables - nounAddVariableDeps(g, n, vars, false) - } - - case *GraphNodeResource: - if m.Config != nil { - // Handle the count variables - vars := m.Config.RawCount.Variables - nounAddVariableDeps(g, n, vars, false) - - // Handle the resource variables - vars = m.Config.RawConfig.Variables - nounAddVariableDeps(g, n, vars, false) - } - - // Handle the variables of the resource provisioners - for _, p := range m.Resource.Provisioners { - vars := p.RawConfig.Variables - nounAddVariableDeps(g, n, vars, true) - - vars = p.ConnInfo.Variables - nounAddVariableDeps(g, n, vars, true) - } - - case *GraphNodeResourceProvider: - if m.Provider != nil && m.Provider.Config != nil { - vars := m.Provider.Config.RawConfig.Variables - nounAddVariableDeps(g, n, vars, false) - } - - default: - // Other node types don't have dependencies or we don't support it - continue - } - } -} - -// graphAddTainted adds the tainted instances to the graph. -func graphAddTainted(g *depgraph.Graph, mod *ModuleState) { - meta := g.Meta.(*GraphMeta) - - var nlist []*depgraph.Noun - for k, rs := range mod.Resources { - // If we have no tainted resources, continue on - if len(rs.Tainted) == 0 { - continue - } - - // Find the untainted resource of this in the noun list. If our - // name is 3 parts, then we mus be a count instance, so our - // untainted node is just the noun without the count. - var untainted *depgraph.Noun - untaintedK := k - if ps := strings.Split(k, "."); len(ps) == 3 { - untaintedK = strings.Join(ps[0:2], ".") - } - for _, n := range g.Nouns { - if n.Name == untaintedK { - untainted = n - break + // Allow the walker to change our tree if needed. Eval, + // then callback with the output. + log.Printf("[DEBUG] vertex %s.%s: evaluating", path, dag.VertexName(v)) + tree = walker.EnterEvalTree(v, tree) + output, err := Eval(tree, ctx) + if rerr = walker.ExitEvalTree(v, output, err); rerr != nil { + return } } - for i, is := range rs.Tainted { - name := fmt.Sprintf("%s (tainted #%d)", k, i+1) - - // Add each of the tainted resources to the graph, and encode - // a dependency from the non-tainted resource to this so that - // tainted resources are always destroyed first. - noun := &depgraph.Noun{ - Name: name, - Meta: &GraphNodeResource{ - Index: -1, - Resource: &Resource{ - Id: k, - Info: &InstanceInfo{ - Id: k, - ModulePath: meta.ModulePath, - Type: rs.Type, - }, - State: is, - Config: NewResourceConfig(nil), - Diff: &InstanceDiff{Destroy: true}, - Flags: FlagTainted, - TaintedIndex: i, - }, - }, - } - - // Append it to the list so we handle it later - nlist = append(nlist, noun) - - // If we have an untainted version, then make sure to add - // the dependency. - if untainted != nil { - dep := &depgraph.Dependency{ - Name: name, - Source: untainted, - Target: noun, - } - - untainted.Deps = append(untainted.Deps, dep) - } - } - } - - // Add the nouns to the graph - g.Nouns = append(g.Nouns, nlist...) -} - -// graphModuleNoun creates a noun for a module. -func graphModuleNoun( - n string, m *config.Module, - g *depgraph.Graph, opts *GraphOpts) (*depgraph.Noun, error) { - name := fmt.Sprintf("module.%s", n) - path := make([]string, len(opts.ModulePath)+1) - copy(path, opts.ModulePath) - path[len(opts.ModulePath)] = n - - // Build the opts we'll use to make the next graph - subOpts := *opts - subOpts.ModulePath = path - subOpts.parent = g - subGraph, err := Graph(&subOpts) - if err != nil { - return nil, fmt.Errorf( - "Error building module graph '%s': %s", - n, err) - } - - return &depgraph.Noun{ - Name: name, - Meta: &GraphNodeModule{ - Config: m, - Path: path, - Graph: subGraph, - }, - }, nil -} - -// nounAddVariableDeps updates the dependencies of a noun given -// a set of associated variable values -func nounAddVariableDeps( - g *depgraph.Graph, - n *depgraph.Noun, - vars map[string]config.InterpolatedVariable, - removeSelf bool) { - for _, rawV := range vars { - var name string - var target *depgraph.Noun - - switch v := rawV.(type) { - case *config.ModuleVariable: - name = fmt.Sprintf("module.%s", v.Name) - target = g.Noun(name) - case *config.ResourceVariable: - // For resource variables, if we ourselves are a resource, then - // we have to check whether to expand or not to create the proper - // resource dependency. - rn, ok := n.Meta.(*GraphNodeResource) - if !ok || rn.ExpandMode > ResourceExpandNone { - name = v.ResourceId() - target = g.Noun(v.ResourceId()) - break - } - - // We're an expanded resource, so add the specific index - // as the dependency. - name = fmt.Sprintf("%s.%d", v.ResourceId(), v.Index) - target = g.Noun(name) - default: - } - - if target == nil { - continue - } - - // If we're ignoring self-references, then don't add that - // dependency. - if removeSelf && n == target { - continue - } - - // Build the dependency - dep := &depgraph.Dependency{ - Name: name, - Source: n, - Target: target, - } - - n.Deps = append(n.Deps, dep) - } -} - -// graphInitResourceProviders maps the resource providers onto the graph -// given a mapping of prefixes to resource providers. -// -// Unlike the graphAdd* functions, this one can return an error if resource -// providers can't be found or can't be instantiated. -func graphInitResourceProviders( - g *depgraph.Graph, - ps map[string]ResourceProviderFactory) error { - var errs []error - - // Keep track of providers we know we couldn't instantiate so - // that we don't get a ton of errors about the same provider. - failures := make(map[string]struct{}) - - for _, n := range g.Nouns { - // We only care about the resource providers first. There is guaranteed - // to be only one node per tuple (providerId, providerConfig), which - // means we don't need to verify we have instantiated it before. - rn, ok := n.Meta.(*GraphNodeResourceProvider) - if !ok { - continue - } - - prefixes := matchingPrefixes(rn.ID, ps) - if len(prefixes) > 0 { - if _, ok := failures[prefixes[0]]; ok { - // We already failed this provider, meaning this - // resource will never succeed, so just continue. - continue - } - } - - sharedProvider := rn.Provider - - // Go through each prefix and instantiate if necessary, then - // verify if this provider is of use to us or not. - sharedProvider.Providers = make(map[string]ResourceProvider) - sharedProvider.ProviderKeys = prefixes - for _, prefix := range prefixes { - p, err := ps[prefix]() + // If the node is dynamically expanded, then expand it + if ev, ok := v.(GraphNodeDynamicExpandable); ok { + log.Printf( + "[DEBUG] vertex %s.%s: expanding/walking dynamic subgraph", + path, + dag.VertexName(v)) + g, err := ev.DynamicExpand(ctx) if err != nil { - errs = append(errs, fmt.Errorf( - "Error instantiating resource provider for "+ - "prefix %s: %s", prefix, err)) - - // Record the error so that we don't check it again - failures[prefix] = struct{}{} - - // Jump to the next prefix - continue + rerr = err + return } - sharedProvider.Providers[prefix] = p + // Walk the subgraph + if rerr = g.walk(walker); rerr != nil { + return + } } - // If we never found a provider, then error and continue - if len(sharedProvider.Providers) == 0 { - errs = append(errs, fmt.Errorf( - "Provider for configuration '%s' not found.", - rn.ID)) - continue + // If the node has a subgraph, then walk the subgraph + if sn, ok := v.(GraphNodeSubgraph); ok { + log.Printf( + "[DEBUG] vertex %s.%s: walking subgraph", + path, + dag.VertexName(v)) + + if rerr = sn.Subgraph().walk(walker); rerr != nil { + return + } } + + return nil } - if len(errs) > 0 { - return &multierror.Error{Errors: errs} - } - - return nil + return g.AcyclicGraph.Walk(walkFn) } -// graphAddResourceProviderDeps goes through all the nodes in the graph -// and adds any dependencies to resource providers as needed. -func graphAddResourceProviderDeps(g *depgraph.Graph) { - for _, rawN := range g.Nouns { - switch n := rawN.Meta.(type) { - case *GraphNodeModule: - // Check if the module depends on any of our providers - // by seeing if there is a parent node back. - for _, moduleRaw := range n.Graph.Nouns { - pn, ok := moduleRaw.Meta.(*GraphNodeResourceProvider) - if !ok { - continue - } - if pn.Provider.parentNoun == nil { - continue - } - - // Create the dependency to the provider - dep := &depgraph.Dependency{ - Name: pn.Provider.parentNoun.Name, - Source: rawN, - Target: pn.Provider.parentNoun, - } - rawN.Deps = append(rawN.Deps, dep) - } - case *GraphNodeResource: - // Not sure how this would happen, but we might as well - // check for it. - if n.ResourceProviderNode == "" { - continue - } - - // Get the noun this depends on. - target := g.Noun(n.ResourceProviderNode) - - // Create the dependency to the provider - dep := &depgraph.Dependency{ - Name: target.Name, - Source: rawN, - Target: target, - } - rawN.Deps = append(rawN.Deps, dep) - } - } -} - -// graphPruneResourceProviders will remove the GraphNodeResourceProvider -// nodes that aren't used in any way. -func graphPruneResourceProviders(g *depgraph.Graph) { - // First, build a mapping of the providers we have. - ps := make(map[string]struct{}) - for _, n := range g.Nouns { - _, ok := n.Meta.(*GraphNodeResourceProvider) - if !ok { - continue - } - - ps[n.Name] = struct{}{} - } - - // Now go through all the dependencies throughout and find - // if any of these aren't reachable. - for _, n := range g.Nouns { - for _, dep := range n.Deps { - delete(ps, dep.Target.Name) - } - } - - if len(ps) == 0 { - // We used all of them! - return - } - - // Now go through and remove these nodes that aren't used - for i := 0; i < len(g.Nouns); i++ { - if _, ok := ps[g.Nouns[i].Name]; !ok { - continue - } - - // Delete this node - copy(g.Nouns[i:], g.Nouns[i+1:]) - g.Nouns[len(g.Nouns)-1] = nil - g.Nouns = g.Nouns[:len(g.Nouns)-1] - i-- - } -} - -// graphMapResourceProviderId goes through the graph and maps the -// ID of a resource provider node to each resource. This lets us know which -// configuration is for which resource. +// GraphNodeDependable is an interface which says that a node can be +// depended on (an edge can be placed between this node and another) according +// to the well-known name returned by DependableName. // -// This is safe to call multiple times. -func graphMapResourceProviderId(g *depgraph.Graph) { - // Build the list of provider configs we have - ps := make(map[string]string) - for _, n := range g.Nouns { - pn, ok := n.Meta.(*GraphNodeResourceProvider) - if !ok { - continue - } - - ps[n.Name] = pn.ID - } - - // Go through every resource and find the shortest matching provider - for _, n := range g.Nouns { - rn, ok := n.Meta.(*GraphNodeResource) - if !ok { - continue - } - - var match, matchNode string - for n, p := range ps { - if !strings.HasPrefix(rn.Resource.Info.Type, p) { - continue - } - if len(p) > len(match) { - match = p - matchNode = n - } - } - if matchNode == "" { - continue - } - - rn.ResourceProviderNode = matchNode - } +// DependableName can return multiple names it is known by. +type GraphNodeDependable interface { + DependableName() []string } -// graphMapResourceProviders takes a graph that already has initialized -// the resource providers (using graphInitResourceProviders) and maps the -// resource providers to the resources themselves. -func graphMapResourceProviders(g *depgraph.Graph) error { - var errs []error - - // First build a mapping of resource provider ID to the node that - // contains those resources. - mapping := make(map[string]*GraphNodeResourceProvider) - for _, n := range g.Nouns { - rn, ok := n.Meta.(*GraphNodeResourceProvider) - if !ok { - continue - } - mapping[rn.ID] = rn - } - - // Now go through each of the resources and find a matching provider. - for _, n := range g.Nouns { - rn, ok := n.Meta.(*GraphNodeResource) - if !ok { - continue - } - - rpnRaw := g.Noun(rn.ResourceProviderNode) - if rpnRaw == nil { - // This should never happen since when building the graph - // we ensure that everything matches up. - panic(fmt.Sprintf( - "Resource provider not found: %s (type: %s)", - rn.ResourceProviderNode, - rn.Resource.Info.Type)) - } - rpn := rpnRaw.Meta.(*GraphNodeResourceProvider) - - var provider ResourceProvider - for _, k := range rpn.Provider.ProviderKeys { - // Only try this provider if it has the right prefix - if !strings.HasPrefix(rn.Resource.Info.Type, k) { - continue - } - - rp := rpn.Provider.Providers[k] - if ProviderSatisfies(rp, rn.Resource.Info.Type) { - provider = rp - break - } - } - - if provider == nil { - errs = append(errs, fmt.Errorf( - "Resource provider not found for resource type '%s'", - rn.Resource.Info.Type)) - continue - } - - rn.Resource.Provider = provider - } - - if len(errs) > 0 { - return &multierror.Error{Errors: errs} - } - - return nil -} - -// graphMapResourceProvisioners takes a graph that already has -// the resources and maps the resource provisioners to the resources themselves. -func graphMapResourceProvisioners(g *depgraph.Graph, - provisioners map[string]ResourceProvisionerFactory) error { - var errs []error - - // Create a cache of resource provisioners, avoids duplicate - // initialization of the instances - cache := make(map[string]ResourceProvisioner) - - // Go through each of the resources and find a matching provisioners - for _, n := range g.Nouns { - rn, ok := n.Meta.(*GraphNodeResource) - if !ok { - continue - } - - // Ignore orphan nodes with no provisioners - if rn.Config == nil { - continue - } - - // Check each provisioner - for _, p := range rn.Config.Provisioners { - // Check for a cached provisioner - provisioner, ok := cache[p.Type] - if !ok { - // Lookup the factory method - factory, ok := provisioners[p.Type] - if !ok { - errs = append(errs, fmt.Errorf( - "Resource provisioner not found for provisioner type '%s'", - p.Type)) - continue - } - - // Initialize the provisioner - prov, err := factory() - if err != nil { - errs = append(errs, fmt.Errorf( - "Failed to instantiate provisioner type '%s': %v", - p.Type, err)) - continue - } - provisioner = prov - - // Cache this type of provisioner - cache[p.Type] = prov - } - - // Save the provisioner - rn.Resource.Provisioners = append(rn.Resource.Provisioners, &ResourceProvisionerConfig{ - Type: p.Type, - Provisioner: provisioner, - Config: NewResourceConfig(p.RawConfig), - RawConfig: p.RawConfig, - ConnInfo: p.ConnInfo, - }) - } - } - - if len(errs) > 0 { - return &multierror.Error{Errors: errs} - } - return nil -} - -// grpahRemoveInvalidDeps goes through the graph and removes dependencies -// that no longer exist. -func graphRemoveInvalidDeps(g *depgraph.Graph) { - check := make(map[*depgraph.Noun]struct{}) - for _, n := range g.Nouns { - check[n] = struct{}{} - } - for _, n := range g.Nouns { - deps := n.Deps - num := len(deps) - for i := 0; i < num; i++ { - if _, ok := check[deps[i].Target]; !ok { - deps[i], deps[num-1] = deps[num-1], nil - i-- - num-- - } - } - n.Deps = deps[:num] - } -} - -// MergeConfig merges all the configurations in the proper order -// to result in the final configuration to use to configure this -// provider. -func (p *graphSharedProvider) MergeConfig( - raw bool, override map[string]interface{}) *ResourceConfig { - var rawMap map[string]interface{} - if p.Config != nil { - rawMap = p.Config.RawConfig.Config() - } - if rawMap == nil { - rawMap = make(map[string]interface{}) - } - for k, v := range override { - rawMap[k] = v - } - - // Merge in all the parent configurations - if p.Parent != nil { - parent := p.Parent - for parent != nil { - if parent.Config != nil { - var merge map[string]interface{} - if raw { - merge = parent.Config.RawConfig.Raw - } else { - merge = parent.Config.RawConfig.Config() - } - - for k, v := range merge { - rawMap[k] = v - } - } - - parent = parent.Parent - } - } - - rc, err := config.NewRawConfig(rawMap) - if err != nil { - panic("error building config: " + err.Error()) - } - - return NewResourceConfig(rc) -} - -// Expand will expand this node into a subgraph if Expand is set. -func (n *GraphNodeResource) Expand() (*depgraph.Graph, error) { - // Expand the count out, which should be interpolated at this point. - count, err := n.Config.Count() - if err != nil { - return nil, err - } - log.Printf("[DEBUG] %s: expanding to count = %d", n.Resource.Id, count) - - // TODO: can we DRY this up? - g := new(depgraph.Graph) - g.Meta = &GraphMeta{ - ModulePath: n.Resource.Info.ModulePath, - } - - // Do the initial expansion of the nodes, attaching diffs if - // applicable - n.expand(g, count, n.Diff) - - // Add all the variable dependencies - graphAddVariableDeps(g) - - // Filter the nodes depending on the expansion type - switch n.ExpandMode { - case ResourceExpandApply: - n.filterResources(g, false) - case ResourceExpandDestroy: - n.filterResources(g, true) - default: - panic(fmt.Sprintf("Unhandled expansion mode %d", n.ExpandMode)) - } - - // Return the finalized graph - return g, n.finalizeGraph(g) -} - -// expand expands this resource and adds the resources to the graph. It -// adds both create and destroy resources. -func (n *GraphNodeResource) expand(g *depgraph.Graph, count int, diff *ModuleDiff) { - // Create the list of nouns - result := make([]*depgraph.Noun, 0, count) - - // Build the key set so we know what is removed - var keys map[string]struct{} - if n.State != nil { - keys = make(map[string]struct{}) - for k, _ := range n.State.Resources { - keys[k] = struct{}{} - } - } - - // First thing, expand the counts that we have defined for our - // current config into the full set of resources that are being - // created. - r := n.Config - for i := 0; i < count; i++ { - name := r.Id() - index := -1 - - // If we have a count that is more than one, then make sure - // we suffix with the number of the resource that this is. - if count > 1 { - name = fmt.Sprintf("%s.%d", name, i) - index = i - } - - var state *ResourceState - if n.State != nil { - // Lookup the resource state - if s, ok := n.State.Resources[name]; ok { - state = s - delete(keys, name) - } - - if count == 1 { - // If the count is one, check the state for ".0" - // appended, which might exist if we go from - // count > 1 to count == 1. - k := r.Id() + ".0" - if state == nil { - state = n.State.Resources[k] - } - delete(keys, k) - } else if i == 0 { - // If count is greater than one, check for state - // with just the ID, which might exist if we go - // from count == 1 to count > 1 - if state == nil { - state = n.State.Resources[r.Id()] - } - delete(keys, r.Id()) - } - } - - // Add in the diff if we have it - var inDiff *InstanceDiff - if diff != nil { - // Looup the instance diff - if d, ok := diff.Resources[name]; ok { - inDiff = d - } - - if inDiff == nil { - if count == 1 { - // If the count is one, check the state for ".0" - // appended, which might exist if we go from - // count > 1 to count == 1. - k := r.Id() + ".0" - inDiff = diff.Resources[k] - } else if i == 0 { - // If count is greater than one, check for state - // with just the ID, which might exist if we go - // from count == 1 to count > 1 - inDiff = diff.Resources[r.Id()] - } - } - } - - // Initialize a default state if not available - if state == nil { - state = &ResourceState{ - Type: r.Type, - } - } - - // Prepare the diff if it exists - if inDiff != nil { - switch n.ExpandMode { - case ResourceExpandApply: - // Disable Destroy if we aren't doing a destroy expansion. - // There is a seperate expansion for the destruction action. - d := new(InstanceDiff) - *d = *inDiff - inDiff = d - inDiff.Destroy = false - - // If we require a new resource, there is a seperate delete - // phase, so the create phase must not have access to the ID. - if inDiff.RequiresNew() { - s := new(ResourceState) - *s = *state - state = s - state.Primary = nil - } - - case ResourceExpandDestroy: - // If we are doing a destroy, make sure it is exclusively - // a destroy, since there is a seperate expansion for the apply - inDiff = new(InstanceDiff) - inDiff.Destroy = true - - default: - panic(fmt.Sprintf("Unhandled expansion mode %d", n.ExpandMode)) - } - } - - // Inherit the existing flags! - flags := n.Resource.Flags - if len(state.Tainted) > 0 { - flags |= FlagHasTainted - } - - // Copy the base resource so we can fill it in - resource := n.copyResource(name) - resource.CountIndex = i - resource.State = state.Primary - resource.Flags = flags - resource.Diff = inDiff - - // Add the result - result = append(result, &depgraph.Noun{ - Name: name, - Meta: &GraphNodeResource{ - Index: index, - Config: r, - Resource: resource, - }, - }) - } - - // Go over the leftover keys which are orphans (decreasing counts) - for k, _ := range keys { - rs := n.State.Resources[k] - - resource := n.copyResource(k) - resource.Config = NewResourceConfig(nil) - resource.State = rs.Primary - resource.Flags = FlagOrphan - resource.Diff = &InstanceDiff{Destroy: true} - - noun := &depgraph.Noun{ - Name: k, - Meta: &GraphNodeResource{ - Index: -1, - Resource: resource, - }, - } - - result = append(result, noun) - } - - g.Nouns = append(g.Nouns, result...) -} - -// copyResource copies the Resource structure to assign to a subgraph. -func (n *GraphNodeResource) copyResource(id string) *Resource { - info := *n.Resource.Info - info.Id = id - resource := *n.Resource - resource.Id = id - resource.Info = &info - resource.Config = NewResourceConfig(n.Config.RawConfig) - resource.Diff = nil - return &resource -} - -// filterResources is used to remove resources from the sub-graph based -// on the ExpandMode. This is because there is a Destroy sub-graph, and -// Apply sub-graph, and we cannot includes the same instances in both -// sub-graphs. -func (n *GraphNodeResource) filterResources(g *depgraph.Graph, destroy bool) { - result := make([]*depgraph.Noun, 0, len(g.Nouns)) - for _, n := range g.Nouns { - rn, ok := n.Meta.(*GraphNodeResource) - if !ok { - continue - } - - if destroy { - if rn.Resource.Diff != nil && rn.Resource.Diff.Destroy { - result = append(result, n) - } - continue - } - - if rn.Resource.Flags&FlagOrphan != 0 || - rn.Resource.Diff == nil || !rn.Resource.Diff.Destroy { - result = append(result, n) - } - } - g.Nouns = result -} - -// finalizeGraph is used to ensure the generated graph is valid -func (n *GraphNodeResource) finalizeGraph(g *depgraph.Graph) error { - // Remove the dependencies that don't exist - graphRemoveInvalidDeps(g) - - // Build the root so that we have a single valid root - graphAddRoot(g) - - // Validate - if err := g.Validate(); err != nil { - return err - } - return nil -} - -// matchingPrefixes takes a resource type and a set of resource -// providers we know about by prefix and returns a list of prefixes -// that might be valid for that resource. -// -// The list returned is in the order that they should be attempted. -func matchingPrefixes( - t string, - ps map[string]ResourceProviderFactory) []string { - result := make([]string, 0, 1) - for prefix, _ := range ps { - if strings.HasPrefix(t, prefix) { - result = append(result, prefix) - } - } - - // Sort by longest first - sort.Sort(stringLenSort(result)) - - return result -} - -// stringLenSort implements sort.Interface and sorts strings in increasing -// length order. i.e. "a", "aa", "aaa" -type stringLenSort []string - -func (s stringLenSort) Len() int { - return len(s) -} - -func (s stringLenSort) Less(i, j int) bool { - return len(s[i]) < len(s[j]) -} - -func (s stringLenSort) Swap(i, j int) { - s[i], s[j] = s[j], s[i] +// GraphNodeDependent is an interface which says that a node depends +// on another GraphNodeDependable by some name. By implementing this +// interface, Graph.ConnectDependents() can be called multiple times +// safely and efficiently. +type GraphNodeDependent interface { + DependentOn() []string } diff --git a/terraform/graph_builder.go b/terraform/graph_builder.go new file mode 100644 index 000000000..e70d58752 --- /dev/null +++ b/terraform/graph_builder.go @@ -0,0 +1,113 @@ +package terraform + +import ( + "log" + + "github.com/hashicorp/terraform/config/module" +) + +// GraphBuilder is an interface that can be implemented and used with +// Terraform to build the graph that Terraform walks. +type GraphBuilder interface { + // Build builds the graph for the given module path. It is up to + // the interface implementation whether this build should expand + // the graph or not. + Build(path []string) (*Graph, error) +} + +// BasicGraphBuilder is a GraphBuilder that builds a graph out of a +// series of transforms and validates the graph is a valid structure. +type BasicGraphBuilder struct { + Steps []GraphTransformer +} + +func (b *BasicGraphBuilder) Build(path []string) (*Graph, error) { + g := &Graph{Path: path} + for _, step := range b.Steps { + if err := step.Transform(g); err != nil { + return g, err + } + + log.Printf("[TRACE] Graph after step %T:\n\n%s", step, g.String()) + } + + // Validate the graph structure + if err := g.Validate(); err != nil { + log.Printf("[ERROR] Graph validation failed. Graph:\n\n%s", g.String()) + return nil, err + } + + return g, nil +} + +// BuiltinGraphBuilder is responsible for building the complete graph that +// Terraform uses for execution. It is an opinionated builder that defines +// the step order required to build a complete graph as is used and expected +// by Terraform. +// +// If you require a custom graph, you'll have to build it up manually +// on your own by building a new GraphBuilder implementation. +type BuiltinGraphBuilder struct { + // Root is the root module of the graph to build. + Root *module.Tree + + // Diff is the diff. The proper module diffs will be looked up. + Diff *Diff + + // State is the global state. The proper module states will be looked + // up by graph path. + State *State + + // Providers is the list of providers supported. + Providers []string + + // Provisioners is the list of provisioners supported. + Provisioners []string +} + +// Build builds the graph according to the steps returned by Steps. +func (b *BuiltinGraphBuilder) Build(path []string) (*Graph, error) { + basic := &BasicGraphBuilder{ + Steps: b.Steps(), + } + + return basic.Build(path) +} + +// Steps returns the ordered list of GraphTransformers that must be executed +// to build a complete graph. +func (b *BuiltinGraphBuilder) Steps() []GraphTransformer { + return []GraphTransformer{ + // Create all our resources from the configuration and state + &ConfigTransformer{Module: b.Root}, + &OrphanTransformer{State: b.State, Module: b.Root}, + + // Provider-related transformations + &MissingProviderTransformer{Providers: b.Providers}, + &ProviderTransformer{}, + &PruneProviderTransformer{}, + + // Provisioner-related transformations + &MissingProvisionerTransformer{Provisioners: b.Provisioners}, + &ProvisionerTransformer{}, + &PruneProvisionerTransformer{}, + + // Run our vertex-level transforms + &VertexTransformer{ + Transforms: []GraphVertexTransformer{ + // Expand any statically expanded nodes, such as module graphs + &ExpandTransform{ + Builder: b, + }, + }, + }, + + // Create the destruction nodes + &DestroyTransformer{}, + &CreateBeforeDestroyTransformer{}, + &PruneDestroyTransformer{Diff: b.Diff, State: b.State}, + + // Make sure we create one root + &RootTransformer{}, + } +} diff --git a/terraform/graph_builder_test.go b/terraform/graph_builder_test.go new file mode 100644 index 000000000..ee1dbc1f5 --- /dev/null +++ b/terraform/graph_builder_test.go @@ -0,0 +1,156 @@ +package terraform + +import ( + "reflect" + "strings" + "testing" + + "github.com/hashicorp/terraform/dag" +) + +func TestBasicGraphBuilder_impl(t *testing.T) { + var _ GraphBuilder = new(BasicGraphBuilder) +} + +func TestBasicGraphBuilder(t *testing.T) { + b := &BasicGraphBuilder{ + Steps: []GraphTransformer{ + &testBasicGraphBuilderTransform{1}, + }, + } + + g, err := b.Build(RootModulePath) + if err != nil { + t.Fatalf("err: %s", err) + } + + if !reflect.DeepEqual(g.Path, RootModulePath) { + t.Fatalf("bad: %#v", g.Path) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testBasicGraphBuilderStr) + if actual != expected { + t.Fatalf("bad: %s", actual) + } +} + +func TestBasicGraphBuilder_validate(t *testing.T) { + b := &BasicGraphBuilder{ + Steps: []GraphTransformer{ + &testBasicGraphBuilderTransform{1}, + &testBasicGraphBuilderTransform{2}, + }, + } + + _, err := b.Build(RootModulePath) + if err == nil { + t.Fatal("should error") + } +} + +func TestBuiltinGraphBuilder_impl(t *testing.T) { + var _ GraphBuilder = new(BuiltinGraphBuilder) +} + +// This test is not meant to test all the transforms but rather just +// to verify we get some basic sane graph out. Special tests to ensure +// specific ordering of steps should be added in other tests. +func TestBuiltinGraphBuilder(t *testing.T) { + b := &BuiltinGraphBuilder{ + Root: testModule(t, "graph-builder-basic"), + } + + g, err := b.Build(RootModulePath) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testBuiltinGraphBuilderBasicStr) + if actual != expected { + t.Fatalf("bad: %s", actual) + } +} + +// This tests a cycle we got when a CBD resource depends on a non-CBD +// resource. This cycle shouldn't happen in the general case anymore. +func TestBuiltinGraphBuilder_cbdDepNonCbd(t *testing.T) { + b := &BuiltinGraphBuilder{ + Root: testModule(t, "graph-builder-cbd-non-cbd"), + } + + _, err := b.Build(RootModulePath) + if err != nil { + t.Fatalf("err: %s", err) + } +} + +/* +TODO: This exposes a really bad bug we need to fix after we merge +the f-ast-branch. This bug still exists in master. + +// This test tests that the graph builder properly expands modules. +func TestBuiltinGraphBuilder_modules(t *testing.T) { + b := &BuiltinGraphBuilder{ + Root: testModule(t, "graph-builder-modules"), + } + + g, err := b.Build(RootModulePath) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testBuiltinGraphBuilderModuleStr) + if actual != expected { + t.Fatalf("bad: %s", actual) + } +} +*/ + +type testBasicGraphBuilderTransform struct { + V dag.Vertex +} + +func (t *testBasicGraphBuilderTransform) Transform(g *Graph) error { + g.Add(t.V) + return nil +} + +const testBasicGraphBuilderStr = ` +1 +` + +const testBuiltinGraphBuilderBasicStr = ` +aws_instance.db + aws_instance.db (destroy tainted) + provider.aws +aws_instance.db (destroy tainted) + aws_instance.web (destroy tainted) + provider.aws +aws_instance.web + aws_instance.db + aws_instance.web (destroy tainted) + provider.aws +aws_instance.web (destroy tainted) + provider.aws +provider.aws +` + +const testBuiltinGraphBuilderModuleStr = ` +aws_instance.web + aws_instance.web (destroy) +aws_instance.web (destroy) + aws_security_group.firewall + module.consul (expanded) + provider.aws +aws_security_group.firewall + aws_security_group.firewall (destroy) +aws_security_group.firewall (destroy) + provider.aws +module.consul (expanded) + aws_security_group.firewall + provider.aws +provider.aws +` diff --git a/terraform/graph_config_node.go b/terraform/graph_config_node.go new file mode 100644 index 000000000..30a614146 --- /dev/null +++ b/terraform/graph_config_node.go @@ -0,0 +1,546 @@ +package terraform + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/dag" +) + +// graphNodeConfig is an interface that all graph nodes for the +// configuration graph need to implement in order to build the variable +// dependencies properly. +type graphNodeConfig interface { + dag.NamedVertex + + // All graph nodes should be dependent on other things, and able to + // be depended on. + GraphNodeDependable + GraphNodeDependent +} + +// GraphNodeConfigModule represents a module within the configuration graph. +type GraphNodeConfigModule struct { + Path []string + Module *config.Module + Tree *module.Tree +} + +func (n *GraphNodeConfigModule) DependableName() []string { + return []string{n.Name()} +} + +func (n *GraphNodeConfigModule) DependentOn() []string { + vars := n.Module.RawConfig.Variables + result := make([]string, 0, len(vars)) + for _, v := range vars { + if vn := varNameForVar(v); vn != "" { + result = append(result, vn) + } + } + + return result +} + +func (n *GraphNodeConfigModule) Name() string { + return fmt.Sprintf("module.%s", n.Module.Name) +} + +// GraphNodeExpandable +func (n *GraphNodeConfigModule) Expand(b GraphBuilder) (GraphNodeSubgraph, error) { + // Build the graph first + graph, err := b.Build(n.Path) + if err != nil { + return nil, err + } + + // Add the parameters node to the module + t := &ModuleInputTransformer{Variables: make(map[string]string)} + if err := t.Transform(graph); err != nil { + return nil, err + } + + // Build the actual subgraph node + return &graphNodeModuleExpanded{ + Original: n, + Graph: graph, + InputConfig: n.Module.RawConfig, + Variables: t.Variables, + }, nil +} + +// GraphNodeExpandable +func (n *GraphNodeConfigModule) ProvidedBy() []string { + // Build up the list of providers by simply going over our configuration + // to find the providers that are configured there as well as the + // providers that the resources use. + config := n.Tree.Config() + providers := make(map[string]struct{}) + for _, p := range config.ProviderConfigs { + providers[p.Name] = struct{}{} + } + for _, r := range config.Resources { + providers[resourceProvider(r.Type)] = struct{}{} + } + + // Turn the map into a string. This makes sure that the list is + // de-dupped since we could be going over potentially many resources. + result := make([]string, 0, len(providers)) + for p, _ := range providers { + result = append(result, p) + } + + return result +} + +// GraphNodeConfigOutput represents an output configured within the +// configuration. +type GraphNodeConfigOutput struct { + Output *config.Output +} + +func (n *GraphNodeConfigOutput) Name() string { + return fmt.Sprintf("output.%s", n.Output.Name) +} + +func (n *GraphNodeConfigOutput) DependableName() []string { + return []string{n.Name()} +} + +func (n *GraphNodeConfigOutput) DependentOn() []string { + vars := n.Output.RawConfig.Variables + result := make([]string, 0, len(vars)) + for _, v := range vars { + if vn := varNameForVar(v); vn != "" { + result = append(result, vn) + } + } + + return result +} + +// GraphNodeEvalable impl. +func (n *GraphNodeConfigOutput) EvalTree() EvalNode { + return &EvalOpFilter{ + Ops: []walkOperation{walkRefresh, walkPlan, walkApply}, + Node: &EvalSequence{ + Nodes: []EvalNode{ + &EvalWriteOutput{ + Name: n.Output.Name, + Value: n.Output.RawConfig, + }, + }, + }, + } +} + +// GraphNodeConfigProvider represents a configured provider within the +// configuration graph. These are only immediately in the graph when an +// explicit `provider` configuration block is in the configuration. +type GraphNodeConfigProvider struct { + Provider *config.ProviderConfig +} + +func (n *GraphNodeConfigProvider) Name() string { + return fmt.Sprintf("provider.%s", n.Provider.Name) +} + +func (n *GraphNodeConfigProvider) DependableName() []string { + return []string{n.Name()} +} + +func (n *GraphNodeConfigProvider) DependentOn() []string { + vars := n.Provider.RawConfig.Variables + result := make([]string, 0, len(vars)) + for _, v := range vars { + if vn := varNameForVar(v); vn != "" { + result = append(result, vn) + } + } + + return result +} + +// GraphNodeEvalable impl. +func (n *GraphNodeConfigProvider) EvalTree() EvalNode { + return ProviderEvalTree(n.Provider.Name, n.Provider.RawConfig) +} + +// GraphNodeProvider implementation +func (n *GraphNodeConfigProvider) ProviderName() string { + return n.Provider.Name +} + +// GraphNodeDotter impl. +func (n *GraphNodeConfigProvider) Dot(name string) string { + return fmt.Sprintf( + "\"%s\" [\n"+ + "\tlabel=\"%s\"\n"+ + "\tshape=diamond\n"+ + "];", + name, + n.Name()) +} + +// GraphNodeConfigResource represents a resource within the config graph. +type GraphNodeConfigResource struct { + Resource *config.Resource + + // If this is set to anything other than destroyModeNone, then this + // resource represents a resource that will be destroyed in some way. + DestroyMode GraphNodeDestroyMode +} + +func (n *GraphNodeConfigResource) DependableName() []string { + return []string{n.Resource.Id()} +} + +// GraphNodeDependent impl. +func (n *GraphNodeConfigResource) DependentOn() []string { + result := make([]string, len(n.Resource.DependsOn), + (len(n.Resource.RawCount.Variables)+ + len(n.Resource.RawConfig.Variables)+ + len(n.Resource.DependsOn))*2) + copy(result, n.Resource.DependsOn) + + for _, v := range n.Resource.RawCount.Variables { + if vn := varNameForVar(v); vn != "" { + result = append(result, vn) + } + } + for _, v := range n.Resource.RawConfig.Variables { + if vn := varNameForVar(v); vn != "" { + result = append(result, vn) + } + } + for _, p := range n.Resource.Provisioners { + for _, v := range p.ConnInfo.Variables { + if vn := varNameForVar(v); vn != "" && vn != n.Resource.Id() { + result = append(result, vn) + } + } + for _, v := range p.RawConfig.Variables { + if vn := varNameForVar(v); vn != "" && vn != n.Resource.Id() { + result = append(result, vn) + } + } + } + + return result +} + +func (n *GraphNodeConfigResource) Name() string { + result := n.Resource.Id() + switch n.DestroyMode { + case DestroyNone: + case DestroyPrimary: + result += " (destroy)" + case DestroyTainted: + result += " (destroy tainted)" + default: + result += " (unknown destroy type)" + } + + return result +} + +// GraphNodeDotter impl. +func (n *GraphNodeConfigResource) Dot(name string) string { + if n.DestroyMode != DestroyNone { + return "" + } + + return fmt.Sprintf( + "\"%s\" [\n"+ + "\tlabel=\"%s\"\n"+ + "\tshape=box\n"+ + "];", + name, + n.Name()) +} + +// GraphNodeDynamicExpandable impl. +func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error) { + state, lock := ctx.State() + lock.RLock() + defer lock.RUnlock() + + // Start creating the steps + steps := make([]GraphTransformer, 0, 5) + + // Primary and non-destroy modes are responsible for creating/destroying + // all the nodes, expanding counts. + switch n.DestroyMode { + case DestroyNone: + fallthrough + case DestroyPrimary: + steps = append(steps, &ResourceCountTransformer{ + Resource: n.Resource, + Destroy: n.DestroyMode != DestroyNone, + }) + } + + // Additional destroy modifications. + switch n.DestroyMode { + case DestroyPrimary: + // If we're destroying the primary instance, then we want to + // expand orphans, which have all the same semantics in a destroy + // as a primary. + steps = append(steps, &OrphanTransformer{ + State: state, + View: n.Resource.Id(), + }) + + // If we're only destroying tainted resources, then we only + // want to find tainted resources and destroy them here. + steps = append(steps, &TaintedTransformer{ + State: state, + View: n.Resource.Id(), + Deposed: n.Resource.Lifecycle.CreateBeforeDestroy, + DeposedInclude: true, + }) + case DestroyTainted: + // If we're only destroying tainted resources, then we only + // want to find tainted resources and destroy them here. + steps = append(steps, &TaintedTransformer{ + State: state, + View: n.Resource.Id(), + Deposed: n.Resource.Lifecycle.CreateBeforeDestroy, + DeposedInclude: false, + }) + } + + // Always end with the root being added + steps = append(steps, &RootTransformer{}) + + // Build the graph + b := &BasicGraphBuilder{Steps: steps} + return b.Build(ctx.Path()) +} + +// GraphNodeEvalable impl. +func (n *GraphNodeConfigResource) EvalTree() EvalNode { + return &EvalSequence{ + Nodes: []EvalNode{ + &EvalInterpolate{Config: n.Resource.RawCount}, + &EvalOpFilter{ + Ops: []walkOperation{walkValidate}, + Node: &EvalValidateCount{Resource: n.Resource}, + }, + &EvalCountFixZeroOneBoundary{Resource: n.Resource}, + }, + } +} + +// GraphNodeProviderConsumer +func (n *GraphNodeConfigResource) ProvidedBy() []string { + return []string{resourceProvider(n.Resource.Type)} +} + +// GraphNodeProvisionerConsumer +func (n *GraphNodeConfigResource) ProvisionedBy() []string { + result := make([]string, len(n.Resource.Provisioners)) + for i, p := range n.Resource.Provisioners { + result[i] = p.Type + } + + return result +} + +// GraphNodeDestroyable +func (n *GraphNodeConfigResource) DestroyNode(mode GraphNodeDestroyMode) GraphNodeDestroy { + // If we're already a destroy node, then don't do anything + if n.DestroyMode != DestroyNone { + return nil + } + + result := &graphNodeResourceDestroy{ + GraphNodeConfigResource: *n, + Original: n, + } + result.DestroyMode = mode + return result +} + +// graphNodeResourceDestroy represents the logical destruction of a +// resource. This node doesn't mean it will be destroyed for sure, but +// instead that if a destroy were to happen, it must happen at this point. +type graphNodeResourceDestroy struct { + GraphNodeConfigResource + Original *GraphNodeConfigResource +} + +func (n *graphNodeResourceDestroy) CreateBeforeDestroy() bool { + // CBD is enabled if the resource enables it in addition to us + // being responsible for destroying the primary state. The primary + // state destroy node is the only destroy node that needs to be + // "shuffled" according to the CBD rules, since tainted resources + // don't have the same inverse dependencies. + return n.Original.Resource.Lifecycle.CreateBeforeDestroy && + n.DestroyMode == DestroyPrimary +} + +func (n *graphNodeResourceDestroy) CreateNode() dag.Vertex { + return n.Original +} + +func (n *graphNodeResourceDestroy) DestroyInclude(d *ModuleDiff, s *ModuleState) bool { + // Always include anything other than the primary destroy + if n.DestroyMode != DestroyPrimary { + return true + } + + // Get the count, and specifically the raw value of the count + // (with interpolations and all). If the count is NOT a static "1", + // then we keep the destroy node no matter what. + // + // The reasoning for this is complicated and not intuitively obvious, + // but I attempt to explain it below. + // + // The destroy transform works by generating the worst case graph, + // with worst case being the case that every resource already exists + // and needs to be destroy/created (force-new). There is a single important + // edge case where this actually results in a real-life cycle: if a + // create-before-destroy (CBD) resource depends on a non-CBD resource. + // Imagine a EC2 instance "foo" with CBD depending on a security + // group "bar" without CBD, and conceptualize the worst case destroy + // order: + // + // 1.) SG must be destroyed (non-CBD) + // 2.) SG must be created/updated + // 3.) EC2 instance must be created (CBD, requires the SG be made) + // 4.) EC2 instance must be destroyed (requires SG be destroyed) + // + // Except, #1 depends on #4, since the SG can't be destroyed while + // an EC2 instance is using it (AWS API requirements). As you can see, + // this is a real life cycle that can't be automatically reconciled + // except under two conditions: + // + // 1.) SG is also CBD. This doesn't work 100% of the time though + // since the non-CBD resource might not support CBD. To make matters + // worse, the entire transitive closure of dependencies must be + // CBD (if the SG depends on a VPC, you have the same problem). + // 2.) EC2 must not CBD. This can't happen automatically because CBD + // is used as a way to ensure zero (or minimal) downtime Terraform + // applies, and it isn't acceptable for TF to ignore this request, + // since it can result in unexpected downtime. + // + // Therefore, we compromise with this edge case here: if there is + // a static count of "1", we prune the diff to remove cycles during a + // graph optimization path if we don't see the resource in the diff. + // If the count is set to ANYTHING other than a static "1" (variable, + // computed attribute, static number greater than 1), then we keep the + // destroy, since it is required for dynamic graph expansion to find + // orphan/tainted count objects. + // + // This isn't ideal logic, but its strictly better without introducing + // new impossibilities. It breaks the cycle in practical cases, and the + // cycle comes back in no cases we've found to be practical, but just + // as the cycle would already exist without this anyways. + count := n.Original.Resource.RawCount + if raw := count.Raw[count.Key]; raw != "1" { + return true + } + + // Okay, we're dealing with a static count. There are a few ways + // to include this resource. + prefix := n.Original.Resource.Id() + + // If we're present in the diff proper, then keep it. + if d != nil { + for k, _ := range d.Resources { + if strings.HasPrefix(k, prefix) { + return true + } + } + } + + // If we're in the state as a primary in any form, then keep it. + // This does a prefix check so it will also catch orphans on count + // decreases to "1". + if s != nil { + for k, v := range s.Resources { + if !strings.HasPrefix(k, prefix) { + continue + } + + // Ignore exact matches and the 0'th index. We only care + // about if there is a decrease in count. + if k == prefix { + continue + } + if k == prefix+".0" { + continue + } + + if v.Primary != nil { + return true + } + } + } + + return false +} + +// graphNodeModuleExpanded represents a module where the graph has +// been expanded. It stores the graph of the module as well as a reference +// to the map of variables. +type graphNodeModuleExpanded struct { + Original dag.Vertex + Graph *Graph + InputConfig *config.RawConfig + + // Variables is a map of the input variables. This reference should + // be shared with ModuleInputTransformer in order to create a connection + // where the variables are set properly. + Variables map[string]string +} + +func (n *graphNodeModuleExpanded) Name() string { + return fmt.Sprintf("%s (expanded)", dag.VertexName(n.Original)) +} + +// GraphNodeDotter impl. +func (n *graphNodeModuleExpanded) Dot(name string) string { + return fmt.Sprintf( + "\"%s\" [\n"+ + "\tlabel=\"%s\"\n"+ + "\tshape=component\n"+ + "];", + name, + dag.VertexName(n.Original)) +} + +// GraphNodeEvalable impl. +func (n *graphNodeModuleExpanded) EvalTree() EvalNode { + var resourceConfig *ResourceConfig + return &EvalSequence{ + Nodes: []EvalNode{ + &EvalInterpolate{ + Config: n.InputConfig, + Output: &resourceConfig, + }, + + &EvalVariableBlock{ + Config: &resourceConfig, + Variables: n.Variables, + }, + + &EvalOpFilter{ + Ops: []walkOperation{walkPlanDestroy}, + Node: &EvalSequence{ + Nodes: []EvalNode{ + &EvalDiffDestroyModule{Path: n.Graph.Path}, + }, + }, + }, + }, + } +} + +// GraphNodeSubgraph impl. +func (n *graphNodeModuleExpanded) Subgraph() *Graph { + return n.Graph +} diff --git a/terraform/graph_config_node_test.go b/terraform/graph_config_node_test.go new file mode 100644 index 000000000..4ebf9bf12 --- /dev/null +++ b/terraform/graph_config_node_test.go @@ -0,0 +1,103 @@ +package terraform + +import ( + "reflect" + "strings" + "testing" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/dag" +) + +func TestGraphNodeConfigModule_impl(t *testing.T) { + var _ dag.Vertex = new(GraphNodeConfigModule) + var _ dag.NamedVertex = new(GraphNodeConfigModule) + var _ graphNodeConfig = new(GraphNodeConfigModule) + var _ GraphNodeExpandable = new(GraphNodeConfigModule) +} + +func TestGraphNodeConfigModuleExpand(t *testing.T) { + mod := testModule(t, "graph-node-module-expand") + + node := &GraphNodeConfigModule{ + Path: []string{RootModuleName, "child"}, + Module: &config.Module{}, + Tree: nil, + } + + g, err := node.Expand(&BasicGraphBuilder{ + Steps: []GraphTransformer{ + &ConfigTransformer{Module: mod}, + }, + }) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.Subgraph().String()) + expected := strings.TrimSpace(testGraphNodeModuleExpandStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestGraphNodeConfigProvider_impl(t *testing.T) { + var _ dag.Vertex = new(GraphNodeConfigProvider) + var _ dag.NamedVertex = new(GraphNodeConfigProvider) + var _ graphNodeConfig = new(GraphNodeConfigProvider) + var _ GraphNodeProvider = new(GraphNodeConfigProvider) +} + +func TestGraphNodeConfigProvider_ProviderName(t *testing.T) { + n := &GraphNodeConfigProvider{ + Provider: &config.ProviderConfig{Name: "foo"}, + } + + if v := n.ProviderName(); v != "foo" { + t.Fatalf("bad: %#v", v) + } +} + +func TestGraphNodeConfigResource_impl(t *testing.T) { + var _ dag.Vertex = new(GraphNodeConfigResource) + var _ dag.NamedVertex = new(GraphNodeConfigResource) + var _ graphNodeConfig = new(GraphNodeConfigResource) + var _ GraphNodeProviderConsumer = new(GraphNodeConfigResource) + var _ GraphNodeProvisionerConsumer = new(GraphNodeConfigResource) +} + +func TestGraphNodeConfigResource_ProvidedBy(t *testing.T) { + n := &GraphNodeConfigResource{ + Resource: &config.Resource{Type: "aws_instance"}, + } + + if v := n.ProvidedBy(); v[0] != "aws" { + t.Fatalf("bad: %#v", v) + } +} + +func TestGraphNodeConfigResource_ProvisionedBy(t *testing.T) { + n := &GraphNodeConfigResource{ + Resource: &config.Resource{ + Type: "aws_instance", + Provisioners: []*config.Provisioner{ + &config.Provisioner{Type: "foo"}, + &config.Provisioner{Type: "bar"}, + }, + }, + } + + expected := []string{"foo", "bar"} + actual := n.ProvisionedBy() + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) + } +} + +const testGraphNodeModuleExpandStr = ` +aws_instance.bar + aws_instance.foo +aws_instance.foo + module inputs +module inputs +` diff --git a/terraform/graph_dot.go b/terraform/graph_dot.go index 5b66d2753..2e420c637 100644 --- a/terraform/graph_dot.go +++ b/terraform/graph_dot.go @@ -6,371 +6,66 @@ import ( "fmt" "strings" - "github.com/hashicorp/terraform/depgraph" + "github.com/hashicorp/terraform/dag" ) -// GraphDotOpts are options for turning a graph into dot format. -type GraphDotOpts struct { - // ModuleDepth is the depth of modules to expand. Zero is no expansion, - // one expands the first set of modules, etc. If this is set to -1, then - // all modules are expanded. - ModuleDepth int - - // Depth is an internal track of what depth we're at within - // the graph, used to control indentation and other such things. - depth int +// GraphNodeDotter can be implemented by a node to cause it to be included +// in the dot graph. The Dot method will be called which is expected to +// return a representation of this node. +type GraphNodeDotter interface { + // Dot is called to return the dot formatting for the node. + // The parameter must be the title of the node. + Dot(string) string } +// GraphDotOpts are the options for generating a dot formatted Graph. +type GraphDotOpts struct{} + // GraphDot returns the dot formatting of a visual representation of // the given Terraform graph. -func GraphDot(g *depgraph.Graph, opts *GraphDotOpts) string { +func GraphDot(g *Graph, opts *GraphDotOpts) string { buf := new(bytes.Buffer) - if opts.depth == 0 { - buf.WriteString("digraph {\n") - buf.WriteString("\tcompound = true;\n") + // Start the graph + buf.WriteString("digraph {\n") + buf.WriteString("\tcompound = true;\n") + + // Go through all the vertices and draw it + vertices := g.Vertices() + dotVertices := make(map[dag.Vertex]struct{}, len(vertices)) + for _, v := range vertices { + if dn, ok := v.(GraphNodeDotter); !ok { + continue + } else if dn.Dot("fake") == "" { + continue + } + + dotVertices[v] = struct{}{} } - // Determine and add the title - // graphDotTitle(buf, g) + for v, _ := range dotVertices { + dn := v.(GraphNodeDotter) + scanner := bufio.NewScanner(strings.NewReader( + dn.Dot(dag.VertexName(v)))) + for scanner.Scan() { + buf.WriteString("\t" + scanner.Text() + "\n") + } - // Add all the resource. - graphDotAddResources(buf, g, opts) + // Draw all the edges + for _, t := range g.DownEdges(v).List() { + target := t.(dag.Vertex) + if _, ok := dotVertices[target]; !ok { + continue + } - // Add all the resource providers - graphDotAddResourceProviders(buf, g, opts) - - // Add all the modules - graphDotAddModules(buf, g, opts) - - if opts.depth == 0 { - buf.WriteString("}\n") + buf.WriteString(fmt.Sprintf( + "\t\"%s\" -> \"%s\";\n", + dag.VertexName(v), + dag.VertexName(target))) + } } + // End the graph + buf.WriteString("}\n") return buf.String() } - -func graphDotAddRoot(buf *bytes.Buffer, n *depgraph.Noun) { - buf.WriteString(fmt.Sprintf("\t\"%s\" [shape=circle];\n", "root")) - - for _, e := range n.Edges() { - target := e.Tail() - buf.WriteString(fmt.Sprintf( - "\t\"%s\" -> \"%s\";\n", - "root", - target)) - } -} - -func graphDotAddModules(buf *bytes.Buffer, g *depgraph.Graph, opts *GraphDotOpts) { - for _, n := range g.Nouns { - _, ok := n.Meta.(*GraphNodeModule) - if !ok { - continue - } - - if graphExpand(opts) { - // We're expanding - graphDotAddModuleExpand(buf, n, opts) - } else { - // We're not expanding, so just add the module on its own - graphDotAddModuleSingle(buf, n, opts) - } - - graphWriteEdges(buf, n, opts) - } -} - -func graphDotAddModuleExpand( - buf *bytes.Buffer, n *depgraph.Noun, opts *GraphDotOpts) { - m := n.Meta.(*GraphNodeModule) - tab := strings.Repeat("\t", opts.depth+1) - uniqueName := graphUniqueName(n, opts) - - // Wrap ourselves in a subgraph - buf.WriteString(fmt.Sprintf("%ssubgraph \"cluster_%s\" {\n", tab, uniqueName)) - defer buf.WriteString(fmt.Sprintf("%s}\n", tab)) - - // Add our label so that we have the proper name. - buf.WriteString(fmt.Sprintf("%s\tlabel = \"%s\";\n", tab, n)) - - // Add a hidden name for edges to point from/to - buf.WriteString(fmt.Sprintf("%s\t\"%s_hidden\" [fixedsize=true,width=0,height=0,label=\"\",style=invisible];\n", tab, uniqueName)) - - // Graph the subgraph just as we would any other graph - subOpts := *opts - subOpts.depth++ - subStr := GraphDot(m.Graph, &subOpts) - - // Tab all the lines of the subgraph - s := bufio.NewScanner(strings.NewReader(subStr)) - for s.Scan() { - buf.WriteString(fmt.Sprintf("%s%s\n", tab, s.Text())) - } -} - -func graphDotAddModuleSingle( - buf *bytes.Buffer, n *depgraph.Noun, opts *GraphDotOpts) { - tab := strings.Repeat("\t", opts.depth+1) - uniqueName := graphUniqueName(n, opts) - - // Create this node. - buf.WriteString(fmt.Sprintf("%s\"%s\" [\n", tab, uniqueName)) - buf.WriteString(fmt.Sprintf("%s\tlabel=\"%s\"\n", tab, n)) - buf.WriteString(fmt.Sprintf("%s\tshape=component\n", tab)) - buf.WriteString(fmt.Sprintf("%s];\n", tab)) -} - -func graphDotAddResources( - buf *bytes.Buffer, g *depgraph.Graph, opts *GraphDotOpts) { - // Determine if we have diffs. If we do, then we're graphing a - // plan, which alters our graph a bit. - hasDiff := false - for _, n := range g.Nouns { - rn, ok := n.Meta.(*GraphNodeResource) - if !ok { - continue - } - if rn.Resource.Diff != nil && !rn.Resource.Diff.Empty() { - hasDiff = true - break - } - } - - var edgeBuf bytes.Buffer - // Do all the non-destroy resources - buf.WriteString("\tsubgraph {\n") - for _, n := range g.Nouns { - rn, ok := n.Meta.(*GraphNodeResource) - if !ok { - continue - } - if rn.Resource.Diff != nil && rn.Resource.Diff.Destroy { - continue - } - - // If we have diffs then we're graphing a plan. If we don't have - // have a diff on this resource, don't graph anything, since the - // plan wouldn't do anything to this resource. - if hasDiff { - if rn.Resource.Diff == nil || rn.Resource.Diff.Empty() { - continue - } - } - - // Determine the colors. White = no change, yellow = change, - // green = create. Destroy is in the next section. - var color, fillColor string - if rn.Resource.Diff != nil && !rn.Resource.Diff.Empty() { - if rn.Resource.State != nil && rn.Resource.State.ID != "" { - color = "#FFFF00" - fillColor = "#FFFF94" - } else { - color = "#00FF00" - fillColor = "#9EFF9E" - } - } - - uniqueName := fmt.Sprintf("%d_%s", opts.depth, n) - - // Create this node. - buf.WriteString(fmt.Sprintf("\t\t\"%s\" [\n", uniqueName)) - buf.WriteString(fmt.Sprintf("\t\t\tlabel=\"%s\"\n", n)) - buf.WriteString("\t\t\tshape=box\n") - if color != "" { - buf.WriteString("\t\t\tstyle=filled\n") - buf.WriteString(fmt.Sprintf("\t\t\tcolor=\"%s\"\n", color)) - buf.WriteString(fmt.Sprintf("\t\t\tfillcolor=\"%s\"\n", fillColor)) - } - buf.WriteString("\t\t];\n") - - // Build up all the edges in a separate buffer so they're not in the - // subgraph. - graphWriteEdges(&edgeBuf, n, opts) - } - buf.WriteString("\t}\n\n") - if edgeBuf.Len() > 0 { - buf.WriteString(edgeBuf.String()) - buf.WriteString("\n") - } - - // Do all the destroy resources - edgeBuf.Reset() - buf.WriteString("\tsubgraph {\n") - for _, n := range g.Nouns { - rn, ok := n.Meta.(*GraphNodeResource) - if !ok { - continue - } - if rn.Resource.Diff == nil || !rn.Resource.Diff.Destroy { - continue - } - - uniqueName := fmt.Sprintf("%d_%s", opts.depth, n) - - buf.WriteString(fmt.Sprintf( - "\t\t\"%s\" [label=\"%s\",shape=box,style=filled,color=\"#FF0000\",fillcolor=\"#FF9494\"];\n", uniqueName, n)) - - graphWriteEdges(&edgeBuf, n, opts) - } - buf.WriteString("\t}\n\n") - if edgeBuf.Len() > 0 { - buf.WriteString(edgeBuf.String()) - buf.WriteString("\n") - } - - // Handle the meta resources - /* - edgeBuf.Reset() - for _, n := range g.Nouns { - _, ok := n.Meta.(*GraphNodeResourceMeta) - if !ok { - continue - } - - // Determine which edges to add - var edges []digraph.Edge - if hasDiff { - for _, e := range n.Edges() { - rn, ok := e.Tail().(*depgraph.Noun).Meta.(*GraphNodeResource) - if !ok { - continue - } - if rn.Resource.Diff == nil || rn.Resource.Diff.Empty() { - continue - } - edges = append(edges, e) - } - } else { - edges = n.Edges() - } - - // Do not draw if we have no edges - if len(edges) == 0 { - continue - } - - uniqueName := fmt.Sprintf("%d_%s", opts.depth, n) - for _, e := range edges { - target := e.Tail() - uniqueTarget := fmt.Sprintf("%d_%s", opts.depth, target) - edgeBuf.WriteString(fmt.Sprintf( - "\t\"%s\" -> \"%s\";\n", - uniqueName, - uniqueTarget)) - } - } - if edgeBuf.Len() > 0 { - buf.WriteString(edgeBuf.String()) - buf.WriteString("\n") - } - */ -} - -func graphDotAddResourceProviders( - buf *bytes.Buffer, g *depgraph.Graph, opts *GraphDotOpts) { - var edgeBuf bytes.Buffer - buf.WriteString("\tsubgraph {\n") - for _, n := range g.Nouns { - _, ok := n.Meta.(*GraphNodeResourceProvider) - if !ok { - continue - } - - uniqueName := fmt.Sprintf("%d_%s", opts.depth, n) - - // Create this node. - buf.WriteString(fmt.Sprintf("\t\t\"%s\" [\n", uniqueName)) - buf.WriteString(fmt.Sprintf("\t\t\tlabel=\"%s\"\n", n)) - buf.WriteString("\t\t\tshape=diamond\n") - buf.WriteString("\t\t];\n") - - // Build up all the edges in a separate buffer so they're not in the - // subgraph. - graphWriteEdges(&edgeBuf, n, opts) - } - buf.WriteString("\t}\n\n") - if edgeBuf.Len() > 0 { - buf.WriteString(edgeBuf.String()) - buf.WriteString("\n") - } -} - -func graphDotTitle(buf *bytes.Buffer, g *depgraph.Graph) { - // Determine if we have diffs. If we do, then we're graphing a - // plan, which alters our graph a bit. - hasDiff := false - for _, n := range g.Nouns { - rn, ok := n.Meta.(*GraphNodeResource) - if !ok { - continue - } - if rn.Resource.Diff != nil && !rn.Resource.Diff.Empty() { - hasDiff = true - break - } - } - - graphType := "Configuration" - if hasDiff { - graphType = "Plan" - } - title := fmt.Sprintf("Terraform %s Resource Graph", graphType) - - buf.WriteString(fmt.Sprintf("\tlabel=\"%s\\n\\n\\n\";\n", title)) - buf.WriteString("\tlabelloc=\"t\";\n\n") -} - -func graphExpand(opts *GraphDotOpts) bool { - return opts.ModuleDepth > opts.depth || opts.ModuleDepth == -1 -} - -func graphUniqueName(n *depgraph.Noun, opts *GraphDotOpts) string { - return fmt.Sprintf("%d_%s", opts.depth, n) -} - -func graphWriteEdges( - buf *bytes.Buffer, n *depgraph.Noun, opts *GraphDotOpts) { - tab := strings.Repeat("\t", opts.depth+1) - - uniqueName := graphUniqueName(n, opts) - var ltail string - if _, ok := n.Meta.(*GraphNodeModule); ok && graphExpand(opts) { - ltail = "cluster_" + uniqueName - uniqueName = uniqueName + "_hidden" - } - - for _, e := range n.Edges() { - target := e.Tail() - targetN := target.(*depgraph.Noun) - uniqueTarget := graphUniqueName(targetN, opts) - - var lhead string - if _, ok := targetN.Meta.(*GraphNodeModule); ok && graphExpand(opts) { - lhead = "cluster_" + uniqueTarget - uniqueTarget = uniqueTarget + "_hidden" - } - - var attrs string - if lhead != "" || ltail != "" { - var attrList []string - if lhead != "" { - attrList = append(attrList, fmt.Sprintf( - "lhead=\"%s\"", lhead)) - } - if ltail != "" { - attrList = append(attrList, fmt.Sprintf( - "ltail=\"%s\"", ltail)) - } - - attrs = fmt.Sprintf(" [%s]", strings.Join(attrList, ",")) - } - - buf.WriteString(fmt.Sprintf( - "%s\"%s\" -> \"%s\"%s;\n", - tab, - uniqueName, - uniqueTarget, - attrs)) - } -} diff --git a/terraform/graph_test.go b/terraform/graph_test.go index c966f5f31..c56f5bfad 100644 --- a/terraform/graph_test.go +++ b/terraform/graph_test.go @@ -1,1646 +1,67 @@ package terraform import ( - "reflect" - "sort" "strings" "testing" ) -func TestGraph_basic(t *testing.T) { - m := testModule(t, "graph-basic") - - g, err := Graph(&GraphOpts{Module: m}) - if err != nil { - t.Fatalf("err: %s", err) - } +func TestGraphAdd(t *testing.T) { + // Test Add since we override it and want to make sure we don't break it. + var g Graph + g.Add(42) + g.Add(84) actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTerraformGraphStr) + expected := strings.TrimSpace(testGraphAddStr) if actual != expected { - t.Fatalf("bad:\n\n%s", actual) + t.Fatalf("bad: %s", actual) } } -func TestGraph_configRequired(t *testing.T) { - if _, err := Graph(new(GraphOpts)); err == nil { - t.Fatal("should error") - } -} - -func TestGraph_count(t *testing.T) { - m := testModule(t, "graph-count") - - g, err := Graph(&GraphOpts{Module: m}) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTerraformGraphCountStr) - if actual != expected { - t.Fatalf("bad:\n\n%s", actual) - } -} - -func TestGraph_countTainted(t *testing.T) { - m := testModule(t, "graph-count") - state := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: []string{"root"}, - Resources: map[string]*ResourceState{ - "aws_instance.web.0": &ResourceState{ - Type: "aws_instance", - Tainted: []*InstanceState{ - &InstanceState{ - ID: "foo", - }, - }, - }, - }, - }, - }, - } - - g, err := Graph(&GraphOpts{Module: m, State: state}) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTerraformGraphCountTaintedStr) - if actual != expected { - t.Fatalf("bad:\n\n%s", actual) - } -} - -func TestGraph_varResource(t *testing.T) { - m := testModule(t, "graph-count-var-resource") - - g, err := Graph(&GraphOpts{Module: m}) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTerraformGraphCountVarResourceStr) - if actual != expected { - t.Fatalf("bad:\n\n%s", actual) - } -} - -func TestGraph_cycle(t *testing.T) { - m := testModule(t, "graph-cycle") - - _, err := Graph(&GraphOpts{Module: m}) - if err == nil { - t.Fatal("should error") - } -} - -func TestGraph_dependsOn(t *testing.T) { - m := testModule(t, "graph-depends-on") - - g, err := Graph(&GraphOpts{Module: m}) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTerraformGraphDependsStr) - if actual != expected { - t.Fatalf("bad:\n\n%s", actual) - } -} - -func TestGraph_dependsOnCount(t *testing.T) { - m := testModule(t, "graph-depends-on-count") - - g, err := Graph(&GraphOpts{Module: m}) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTerraformGraphDependsCountStr) - if actual != expected { - t.Fatalf("bad:\n\n%s", actual) - } -} - -func TestGraph_dependsOnWithOrphan(t *testing.T) { - m := testModule(t, "graph-depends-on") - - state := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: []string{"root"}, - Resources: map[string]*ResourceState{ - "aws_instance.old": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - }, - }, - }, - } - - g, err := Graph(&GraphOpts{Module: m, State: state}) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTerraformGraphDependsOrphanStr) - if actual != expected { - t.Fatalf("bad:\n\n%s", actual) - } -} - -func TestGraph_modules(t *testing.T) { - m := testModule(t, "graph-modules") - - g, err := Graph(&GraphOpts{Module: m}) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTerraformGraphModulesStr) - if actual != expected { - t.Fatalf("bad:\n\n%s", actual) - } - - n := g.Noun("module.consul") - if n == nil { - t.Fatal("can't find noun") - } - mn := n.Meta.(*GraphNodeModule) - - if !reflect.DeepEqual(mn.Path, []string{"root", "consul"}) { - t.Fatalf("bad: %#v", mn.Path) - } - - actual = strings.TrimSpace(mn.Graph.String()) - expected = strings.TrimSpace(testTerraformGraphModulesConsulStr) - if actual != expected { - t.Fatalf("bad:\n\n%s", actual) - } -} - -func TestGraph_moduleOrphan(t *testing.T) { - m := testModule(t, "graph-module-orphan") - state := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: []string{"root", "consul"}, - - Resources: map[string]*ResourceState{ - "aws_instance.old": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - }, - }, - }, - } - - g, err := Graph(&GraphOpts{Module: m, State: state}) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTerraformGraphModuleOrphanStr) - if actual != expected { - t.Fatalf("bad:\n\n%s", actual) - } - - n := g.Noun("module.consul") - if n == nil { - t.Fatal("can't find noun") - } - mn := n.Meta.(*GraphNodeModule) - - if !reflect.DeepEqual(mn.Path, []string{"root", "consul"}) { - t.Fatalf("bad: %#v", mn.Path) - } - - actual = strings.TrimSpace(mn.Graph.String()) - expected = strings.TrimSpace(testTerraformGraphModuleOrphanConsulStr) - if actual != expected { - t.Fatalf("bad:\n\n%s", actual) - } -} - -func TestGraph_providerPrune(t *testing.T) { - m := testModule(t, "graph-provider-prune") - - g, err := Graph(&GraphOpts{Module: m}) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTerraformGraphProviderPruneStr) - if actual != expected { - t.Fatalf("bad:\n\n%s", actual) - } -} - -func TestGraph_state(t *testing.T) { - m := testModule(t, "graph-basic") - state := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - - Resources: map[string]*ResourceState{ - "aws_instance.old": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - }, - }, - }, - } - - g, err := Graph(&GraphOpts{Module: m, State: state}) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTerraformGraphStateStr) - if actual != expected { - t.Fatalf("bad:\n\n%s", actual) - } -} - -func TestGraph_tainted(t *testing.T) { - m := testModule(t, "graph-tainted") - state := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - - Resources: map[string]*ResourceState{ - "aws_instance.web": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - Tainted: []*InstanceState{ - &InstanceState{ - ID: "bar", - }, - }, - }, - }, - }, - }, - } - - g, err := Graph(&GraphOpts{Module: m, State: state}) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTerraformGraphTaintedStr) - if actual != expected { - t.Fatalf("bad:\n\n%s", actual) - } -} - -func TestGraph_taintedMulti(t *testing.T) { - m := testModule(t, "graph-tainted") - state := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - - Resources: map[string]*ResourceState{ - "aws_instance.web": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - Tainted: []*InstanceState{ - &InstanceState{ - ID: "bar", - }, - &InstanceState{ - ID: "baz", - }, - }, - }, - }, - }, - }, - } - - g, err := Graph(&GraphOpts{Module: m, State: state}) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTerraformGraphTaintedMultiStr) - if actual != expected { - t.Fatalf("bad:\n\n%s", actual) - } -} - -func TestGraphFull(t *testing.T) { - rpAws := new(MockResourceProvider) - rpOS := new(MockResourceProvider) - - rpAws.ResourcesReturn = []ResourceType{ - ResourceType{Name: "aws_instance"}, - ResourceType{Name: "aws_load_balancer"}, - ResourceType{Name: "aws_security_group"}, - } - rpOS.ResourcesReturn = []ResourceType{ - ResourceType{Name: "openstack_floating_ip"}, - } - - ps := map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(rpAws), - "open": testProviderFuncFixed(rpOS), - } - - m := testModule(t, "graph-basic") - g, err := Graph(&GraphOpts{Module: m, Providers: ps}) - if err != nil { - t.Fatalf("err: %s", err) - } - - // A helper to help get us the provider for a resource. - graphProvider := func(n string) ResourceProvider { - return g.Noun(n).Meta.(*GraphNodeResource).Resource.Provider - } - - // Test a couple - if graphProvider("aws_instance.web") != rpAws { - t.Fatalf("bad: %#v", graphProvider("aws_instance.web")) - } - if graphProvider("openstack_floating_ip.random") != rpOS { - t.Fatalf("bad: %#v", graphProvider("openstack_floating_ip.random")) - } - - // Test that all providers have been set - for _, n := range g.Nouns { - switch m := n.Meta.(type) { - case *GraphNodeResource: - if m.Resource.Provider == nil { - t.Fatalf("bad: %#v", m) - } - case *GraphNodeResourceProvider: - if len(m.Provider.Providers) == 0 { - t.Fatalf("bad: %#v", m) - } - default: - continue - } - } -} - -func TestGraphProvisioners(t *testing.T) { - rpAws := new(MockResourceProvider) - provShell := new(MockResourceProvisioner) - provWinRM := new(MockResourceProvisioner) - - rpAws.ResourcesReturn = []ResourceType{ - ResourceType{Name: "aws_instance"}, - ResourceType{Name: "aws_load_balancer"}, - ResourceType{Name: "aws_security_group"}, - } - - ps := map[string]ResourceProvisionerFactory{ - "shell": testProvisionerFuncFixed(provShell), - "winrm": testProvisionerFuncFixed(provWinRM), - } - - pf := map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(rpAws), - } - - m := testModule(t, "graph-provisioners") - g, err := Graph(&GraphOpts{Module: m, Providers: pf, Provisioners: ps}) - if err != nil { - t.Fatalf("err: %s", err) - } - - // A helper to help get us the provider for a resource. - graphProvisioner := func(n string, idx int) *ResourceProvisionerConfig { - return g.Noun(n).Meta.(*GraphNodeResource).Resource.Provisioners[idx] - } - - // A helper to verify depedencies - depends := func(a, b string) bool { - aNoun := g.Noun(a) - bNoun := g.Noun(b) - for _, dep := range aNoun.Deps { - if dep.Source == aNoun && dep.Target == bNoun { - return true - } - } - return false - } - - // Test a couple - prov := graphProvisioner("aws_instance.web", 0) - if prov.Provisioner != provWinRM { - t.Fatalf("bad: %#v", prov) - } - if prov.RawConfig.Config()["cmd"] != "echo foo" { - t.Fatalf("bad: %#v", prov) - } - - prov = graphProvisioner("aws_instance.web", 1) - if prov.Provisioner != provWinRM { - t.Fatalf("bad: %#v", prov) - } - if prov.RawConfig.Config()["cmd"] != "echo bar" { - t.Fatalf("bad: %#v", prov) - } - - prov = graphProvisioner("aws_load_balancer.weblb", 0) - if prov.Provisioner != provShell { - t.Fatalf("bad: %#v", prov) - } - if prov.RawConfig.Config()["cmd"] != "add ${aws_instance.web.id}" { - t.Fatalf("bad: %#v", prov) - } - if prov.ConnInfo == nil || len(prov.ConnInfo.Raw) != 2 { - t.Fatalf("bad: %#v", prov) - } - - // Check that the variable dependency is handled - if !depends("aws_load_balancer.weblb", "aws_instance.web") { - t.Fatalf("missing dependency from provisioner variable") - } - - // Check that the connection variable dependency is handled - if !depends("aws_load_balancer.weblb", "aws_security_group.firewall") { - t.Fatalf("missing dependency from provisioner connection") - } -} - -func TestGraphAddDiff(t *testing.T) { - m := testModule(t, "graph-diff") - diff := &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{ - Path: rootModulePath, - Resources: map[string]*InstanceDiff{ - "aws_instance.foo": &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - New: "bar", - }, - }, - }, - }, - }, - }, - } - - g, err := Graph(&GraphOpts{Module: m, Diff: diff}) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTerraformGraphDiffStr) - if actual != expected { - t.Fatalf("bad:\n\n%s", actual) - } - - /* - TODO: test this somewhere - // Verify that the state has been added - n := g.Noun("aws_instance.foo") - rn := n.Meta.(*GraphNodeResource) - - expected2 := diff.RootModule().Resources["aws_instance.foo"] - actual2 := rn.Resource.Diff - if !reflect.DeepEqual(actual2, expected2) { - t.Fatalf("bad: %#v", actual2) - } - */ -} - -func TestGraphAddDiff_destroy(t *testing.T) { - m := testModule(t, "graph-diff-destroy") - diff := &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{ - Path: rootModulePath, - Resources: map[string]*InstanceDiff{ - "aws_instance.foo": &InstanceDiff{ - Destroy: true, - }, - "aws_instance.bar": &InstanceDiff{ - Destroy: true, - }, - }, - }, - }, - } - state := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.foo": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - - "aws_instance.bar": &ResourceState{ - Type: "aws_instance", - Dependencies: []string{"foo"}, - Primary: &InstanceState{ - ID: "bar", - }, - }, - }, - }, - }, - } - - diffHash := checksumStruct(t, diff) - - g, err := Graph(&GraphOpts{ - Module: m, - Diff: diff, - State: state, +func TestGraphConnectDependent(t *testing.T) { + var g Graph + g.Add(&testGraphDependable{VertexName: "a", Mock: []string{"a"}}) + b := g.Add(&testGraphDependable{ + VertexName: "b", + DependentOnMock: []string{"a"}, }) - if err != nil { - t.Fatalf("err: %s", err) + + if missing := g.ConnectDependent(b); len(missing) > 0 { + t.Fatalf("bad: %#v", missing) } actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTerraformGraphDiffDestroyStr) + expected := strings.TrimSpace(testGraphConnectDepsStr) if actual != expected { - t.Fatalf("bad:\n\n%s\n\nexpected:\n\n%s", actual, expected) - } - - // Verify that the state has been added - n := g.Noun("aws_instance.foo (destroy)") - rn := n.Meta.(*GraphNodeResource) - - expected2 := &InstanceDiff{Destroy: true} - actual2 := rn.Resource.Diff - if !reflect.DeepEqual(actual2, expected2) { - t.Fatalf("bad: %#v", actual2) - } - - // Verify that our original structure has not been modified - diffHash2 := checksumStruct(t, diff) - if diffHash != diffHash2 { - t.Fatal("diff has been modified") + t.Fatalf("bad: %s", actual) } } -func TestGraphAddDiff_destroy_counts(t *testing.T) { - m := testModule(t, "graph-count") - diff := &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{ - Path: rootModulePath, - Resources: map[string]*InstanceDiff{ - "aws_instance.web.0": &InstanceDiff{ - Destroy: true, - }, - "aws_instance.web.1": &InstanceDiff{ - Destroy: true, - }, - "aws_instance.web.2": &InstanceDiff{ - Destroy: true, - }, - "aws_load_balancer.weblb": &InstanceDiff{ - Destroy: true, - }, - }, - }, - }, - } - state := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.web.0": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - "aws_instance.web.1": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - "aws_instance.web.2": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - "aws_load_balancer.weblb": &ResourceState{ - Type: "aws_load_balancer", - Dependencies: []string{"aws_instance.web.0", "aws_instance.web.1", "aws_instance.web.2"}, - Primary: &InstanceState{ - ID: "bar", - }, - }, - }, - }, - }, - } - - diffHash := checksumStruct(t, diff) - - g, err := Graph(&GraphOpts{ - Module: m, - Diff: diff, - State: state, - }) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTerraformGraphDiffDestroyCountsStr) - if actual != expected { - t.Fatalf("bad:\n\n%s\n\nexpected:\n\n%s", actual, expected) - } - - // Verify that the state has been added - n := g.Noun("aws_instance.web (destroy)") - rn := n.Meta.(*GraphNodeResource) - - if rn.ExpandMode != ResourceExpandDestroy { - t.Fatalf("bad: %#v", rn) - } - - // Verify that our original structure has not been modified - diffHash2 := checksumStruct(t, diff) - if diffHash != diffHash2 { - t.Fatal("diff has been modified") - } +type testGraphDependable struct { + VertexName string + DependentOnMock []string + Mock []string } -func TestGraphAddDiff_module(t *testing.T) { - m := testModule(t, "graph-diff-module") - diff := &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{ - Path: rootModulePath, - Resources: map[string]*InstanceDiff{ - "aws_instance.foo": &InstanceDiff{ - Destroy: true, - }, - }, - }, - }, - } - - g, err := Graph(&GraphOpts{Module: m, Diff: diff}) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTerraformGraphDiffModuleStr) - if actual != expected { - t.Fatalf("bad:\n\n%s", actual) - } +func (v *testGraphDependable) Name() string { + return v.VertexName } -func TestGraphAddDiff_module_depends(t *testing.T) { - m := testModule(t, "graph-diff-module-dep") - diff := &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{ - Path: rootModulePath, - Resources: map[string]*InstanceDiff{ - "aws_instance.foo": &InstanceDiff{ - Destroy: true, - }, - }, - }, - &ModuleDiff{ - Path: []string{"root", "child"}, - Destroy: true, - Resources: map[string]*InstanceDiff{ - "aws_instance.foo": &InstanceDiff{ - Destroy: true, - }, - }, - }, - }, - } - state := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: []string{"root", "orphan"}, - Resources: map[string]*ResourceState{ - "aws_instance.dead": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "dead", - }, - }, - }, - Dependencies: []string{ - "aws_instance.foo", - "module.child", - }, - }, - }, - } - - g, err := Graph(&GraphOpts{Module: m, Diff: diff, State: state}) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTerraformGraphDiffModuleDependsStr) - if actual != expected { - t.Fatalf("bad:\n\n%s", actual) - } +func (v *testGraphDependable) DependableName() []string { + return v.Mock } -func TestGraphAddDiff_moduleDependsModule(t *testing.T) { - m := testModule(t, "graph-diff-module-dep-module") - diff := &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{ - Path: []string{"root"}, - Destroy: true, - }, - &ModuleDiff{ - Path: []string{"root", "foo"}, - Destroy: true, - Resources: map[string]*InstanceDiff{ - "aws_instance.foo": &InstanceDiff{ - Destroy: true, - }, - }, - }, - &ModuleDiff{ - Path: []string{"root", "bar"}, - Destroy: true, - Resources: map[string]*InstanceDiff{ - "aws_instance.foo": &InstanceDiff{ - Destroy: true, - }, - }, - }, - }, - } - state := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: []string{"root", "foo"}, - Resources: map[string]*ResourceState{ - "aws_instance.foo": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - }, - }, - - &ModuleState{ - Path: []string{"root", "bar"}, - Resources: map[string]*ResourceState{ - "aws_instance.foo": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - }, - Dependencies: []string{ - "module.foo", - }, - }, - }, - } - - g, err := Graph(&GraphOpts{Module: m, Diff: diff, State: state}) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTerraformGraphDiffModuleDependsModuleStr) - if actual != expected { - t.Fatalf("bad:\n\n%s", actual) - } +func (v *testGraphDependable) DependentOn() []string { + return v.DependentOnMock } -func TestGraphAddDiff_createBeforeDestroy(t *testing.T) { - m := testModule(t, "graph-diff-create-before") - diff := &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{ - Path: rootModulePath, - Resources: map[string]*InstanceDiff{ - "aws_instance.bar": &InstanceDiff{ - Destroy: true, - Attributes: map[string]*ResourceAttrDiff{ - "ami": &ResourceAttrDiff{ - Old: "abc", - New: "xyz", - RequiresNew: true, - }, - }, - }, - }, - }, - }, - } - state := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.bar": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "ami": "abc", - }, - }, - }, - }, - }, - }, - } - - diffHash := checksumStruct(t, diff) - - g, err := Graph(&GraphOpts{ - Module: m, - Diff: diff, - State: state, - }) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTerraformGraphDiffCreateBeforeDestroyStr) - if actual != expected { - t.Fatalf("bad:\n\n%s\n\nexpected:\n\n%s", actual, expected) - } - - // Verify the flags are set - r := g.Noun("aws_instance.bar") - if r.Meta.(*GraphNodeResource).Resource.Flags&FlagReplacePrimary == 0 { - t.Fatalf("missing FlagReplacePrimary") - } - - r = g.Noun("aws_instance.bar (destroy)") - if r.Meta.(*GraphNodeResource).Resource.Flags&FlagDeposed == 0 { - t.Fatalf("missing FlagDeposed") - } - - // Verify that our original structure has not been modified - diffHash2 := checksumStruct(t, diff) - if diffHash != diffHash2 { - t.Fatal("diff has been modified") - } -} - -func TestGraphAddDiff_moduleDestroy(t *testing.T) { - m := testModule(t, "graph-diff-module") - diff := &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{ - Path: rootModulePath, - Resources: map[string]*InstanceDiff{ - "aws_instance.foo": &InstanceDiff{ - Destroy: true, - }, - }, - }, - &ModuleDiff{ - Path: []string{"root", "child"}, - Resources: map[string]*InstanceDiff{ - "aws_instance.foo": &InstanceDiff{ - Destroy: true, - }, - }, - }, - }, - } - - g, err := Graph(&GraphOpts{Module: m, Diff: diff}) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTerraformGraphDiffModuleStr) - if actual != expected { - t.Fatalf("bad:\n\n%s", actual) - } -} - -func TestGraphEncodeDependencies(t *testing.T) { - m := testModule(t, "graph-basic") - - g, err := Graph(&GraphOpts{Module: m}) - if err != nil { - t.Fatalf("err: %s", err) - } - - // This should encode the dependency information into the state - graphEncodeDependencies(g) - - web := g.Noun("aws_instance.web").Meta.(*GraphNodeResource).Resource - if len(web.Dependencies) != 1 || web.Dependencies[0] != "aws_security_group.firewall" { - t.Fatalf("bad: %#v", web) - } - - weblb := g.Noun("aws_load_balancer.weblb").Meta.(*GraphNodeResource).Resource - if len(weblb.Dependencies) != 1 || weblb.Dependencies[0] != "aws_instance.web" { - t.Fatalf("bad: %#v", weblb) - } -} - -func TestGraphEncodeDependencies_count(t *testing.T) { - m := testModule(t, "graph-count") - state := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.web.0": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - "aws_load_balancer.weblb": &ResourceState{ - Type: "aws_load_balancer", - Primary: &InstanceState{ - ID: "foo", - }, - }, - }, - }, - }, - } - - g, err := Graph(&GraphOpts{Module: m, State: state}) - if err != nil { - t.Fatalf("err: %s", err) - } - - // This should encode the dependency information into the state - graphEncodeDependencies(g) - - web := g.Noun("aws_instance.web").Meta.(*GraphNodeResource).Resource - if len(web.Dependencies) != 0 { - t.Fatalf("bad: %#v", web) - } - - weblb := g.Noun("aws_load_balancer.weblb").Meta.(*GraphNodeResource).Resource - if len(weblb.Dependencies) != 1 { - t.Fatalf("bad: %#v", weblb) - } -} - -func TestGraphEncodeDependencies_module(t *testing.T) { - m := testModule(t, "graph-modules") - - g, err := Graph(&GraphOpts{Module: m, State: &State{}}) - if err != nil { - t.Fatalf("err: %s", err) - } - - // This should encode the dependency information into the state - graphEncodeDependencies(g) - - web := g.Noun("aws_instance.web").Meta.(*GraphNodeResource).Resource - sort.Strings(web.Dependencies) - if len(web.Dependencies) != 2 { - t.Fatalf("bad: %#v", web) - } - if web.Dependencies[0] != "aws_security_group.firewall" { - t.Fatalf("bad: %#v", web) - } - if web.Dependencies[1] != "module.consul" { - t.Fatalf("bad: %#v", web) - } - - mod := g.Noun("module.consul").Meta.(*GraphNodeModule) - deps := mod.State.Dependencies - if len(deps) != 1 { - t.Fatalf("Bad: %#v", deps) - } - if deps[0] != "aws_security_group.firewall" { - t.Fatalf("Bad: %#v", deps) - } -} - -func TestGraph_orphan_dependencies(t *testing.T) { - m := testModule(t, "graph-count") - state := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - - Resources: map[string]*ResourceState{ - "aws_instance.web.0": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - "aws_instance.web.1": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - "aws_load_balancer.old": &ResourceState{ - Type: "aws_load_balancer", - Primary: &InstanceState{ - ID: "foo", - }, - Dependencies: []string{ - "aws_instance.web.0", - "aws_instance.web.1", - "aws_instance.web.2", - }, - }, - }, - }, - }, - } - - g, err := Graph(&GraphOpts{Module: m, State: state}) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTerraformGraphCountOrphanStr) - if actual != expected { - t.Fatalf("bad:\n\nactual:\n%s\n\nexpected:\n%s", actual, expected) - } -} - -func TestGraph_orphanDependenciesModules(t *testing.T) { - m := testModule(t, "graph-modules") - state := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - - Resources: map[string]*ResourceState{ - "aws_instance.foo": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - Dependencies: []string{ - "module.consul", - }, - }, - }, - }, - }, - } - - g, err := Graph(&GraphOpts{Module: m, State: state}) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTerraformGraphOrphanModuleDepsStr) - if actual != expected { - t.Fatalf("bad:\n\nactual:\n%s\n\nexpected:\n%s", actual, expected) - } -} - -func TestGraph_orphanModules_Dependencies(t *testing.T) { - m := testModule(t, "graph-modules") - state := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - - Resources: map[string]*ResourceState{ - "aws_instance.foo": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - Dependencies: []string{ - "module.consul", - }, - }, - }, - }, - - // Add an orphan module - &ModuleState{ - Path: []string{"root", "orphan"}, - Resources: map[string]*ResourceState{ - "aws_instance.bar": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "bar", - }, - }, - }, - Dependencies: []string{ - "aws_instance.foo", - "aws_instance.web", - }, - }, - }, - } - - g, err := Graph(&GraphOpts{Module: m, State: state}) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTerraformGraphOrphanedModuleDepsStr) - if actual != expected { - t.Fatalf("bad:\n\nactual:\n%s\n\nexpected:\n%s", actual, expected) - } -} - -func TestGraphNodeResourceExpand(t *testing.T) { - m := testModule(t, "graph-resource-expand") - - g, err := Graph(&GraphOpts{Module: m}) - if err != nil { - t.Fatalf("err: %s", err) - } - - // Get the resource we care about expanding - n := g.Noun("aws_instance.web") - if n == nil { - t.Fatal("could not find") - } - rn := n.Meta.(*GraphNodeResource) - - g, err = rn.Expand() - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTerraformGraphResourceExpandStr) - if actual != expected { - t.Fatalf("bad:\n\nactual:\n%s\n\nexpected:\n%s", actual, expected) - } -} - -func TestGraphNodeResourceExpand_provDeps(t *testing.T) { - m := testModule(t, "graph-resource-expand-prov-deps") - provs := map[string]ResourceProvisionerFactory{ - "remote-exec": func() (ResourceProvisioner, error) { - return new(MockResourceProvisioner), nil - }, - } - - g, err := Graph(&GraphOpts{Module: m, Provisioners: provs}) - if err != nil { - t.Fatalf("err: %s", err) - } - - // Get the resource we care about expanding - n := g.Noun("aws_instance.web") - if n == nil { - t.Fatal("could not find") - } - rn := n.Meta.(*GraphNodeResource) - - g, err = rn.Expand() - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTerraformGraphResourceExpandProvDepsStr) - if actual != expected { - t.Fatalf("bad:\n\nactual:\n%s\n\nexpected:\n%s", actual, expected) - } -} - -const testTerraformGraphStr = ` -root: root -aws_instance.web - aws_instance.web -> aws_security_group.firewall - aws_instance.web -> provider.aws -aws_load_balancer.weblb - aws_load_balancer.weblb -> aws_instance.web - aws_load_balancer.weblb -> provider.aws -aws_security_group.firewall - aws_security_group.firewall -> provider.aws -openstack_floating_ip.random -provider.aws - provider.aws -> openstack_floating_ip.random -root - root -> aws_instance.web - root -> aws_load_balancer.weblb - root -> aws_security_group.firewall - root -> openstack_floating_ip.random +const testGraphAddStr = ` +42 +84 ` -const testTerraformGraphCountStr = ` -root: root -aws_instance.web -aws_load_balancer.weblb - aws_load_balancer.weblb -> aws_instance.web -root - root -> aws_instance.web - root -> aws_load_balancer.weblb -` - -const testTerraformGraphCountTaintedStr = ` -root: root -aws_instance.web - aws_instance.web -> aws_instance.web.0 (tainted #1) -aws_instance.web.0 (tainted #1) -aws_load_balancer.weblb - aws_load_balancer.weblb -> aws_instance.web -root - root -> aws_instance.web - root -> aws_instance.web.0 (tainted #1) - root -> aws_load_balancer.weblb -` - -const testTerraformGraphCountVarResourceStr = ` -root: root -aws_instance.foo -aws_instance.web - aws_instance.web -> aws_instance.foo -aws_load_balancer.weblb - aws_load_balancer.weblb -> aws_instance.web -root - root -> aws_instance.foo - root -> aws_instance.web - root -> aws_load_balancer.weblb -` - -const testTerraformGraphDependsStr = ` -root: root -aws_instance.db - aws_instance.db -> aws_instance.web -aws_instance.web -root - root -> aws_instance.db - root -> aws_instance.web -` - -const testTerraformGraphDependsCountStr = ` -root: root -aws_instance.db - aws_instance.db -> aws_instance.web -aws_instance.web -root - root -> aws_instance.db - root -> aws_instance.web -` - -const testTerraformGraphDependsOrphanStr = ` -root: root -aws_instance.db - aws_instance.db -> aws_instance.web -aws_instance.old -aws_instance.web -root - root -> aws_instance.db - root -> aws_instance.old - root -> aws_instance.web -` - -const testTerraformGraphDiffStr = ` -root: root -aws_instance.foo -root - root -> aws_instance.foo -` - -const testTerraformGraphDiffDestroyStr = ` -root: root -aws_instance.bar - aws_instance.bar -> aws_instance.bar (destroy) - aws_instance.bar -> aws_instance.foo - aws_instance.bar -> provider.aws -aws_instance.bar (destroy) - aws_instance.bar (destroy) -> provider.aws -aws_instance.foo - aws_instance.foo -> aws_instance.foo (destroy) - aws_instance.foo -> provider.aws -aws_instance.foo (destroy) - aws_instance.foo (destroy) -> aws_instance.bar (destroy) - aws_instance.foo (destroy) -> provider.aws -provider.aws -root - root -> aws_instance.bar - root -> aws_instance.foo -` - -const testTerraformGraphDiffDestroyCountsStr = ` -root: root -aws_instance.web - aws_instance.web -> aws_instance.web (destroy) -aws_instance.web (destroy) - aws_instance.web (destroy) -> aws_load_balancer.weblb (destroy) -aws_load_balancer.weblb - aws_load_balancer.weblb -> aws_instance.web - aws_load_balancer.weblb -> aws_load_balancer.weblb (destroy) -aws_load_balancer.weblb (destroy) -root - root -> aws_instance.web - root -> aws_load_balancer.weblb -` - -const testTerraformGraphDiffModuleStr = ` -root: root -aws_instance.foo - aws_instance.foo -> aws_instance.foo (destroy) - aws_instance.foo -> module.child -aws_instance.foo (destroy) -module.child - module.child -> aws_instance.foo (destroy) -root - root -> aws_instance.foo - root -> module.child -` - -const testTerraformGraphDiffModuleDependsStr = ` -root: root -aws_instance.foo - aws_instance.foo -> aws_instance.foo (destroy) -aws_instance.foo (destroy) - aws_instance.foo (destroy) -> module.child - aws_instance.foo (destroy) -> module.orphan -module.child - module.child -> module.orphan -module.orphan -root - root -> aws_instance.foo - root -> module.child - root -> module.orphan -` - -const testTerraformGraphDiffModuleDependsModuleStr = ` -root: root -module.bar -module.foo - module.foo -> module.bar -root - root -> module.bar - root -> module.foo -` - -const testTerraformGraphModulesStr = ` -root: root -aws_instance.web - aws_instance.web -> aws_security_group.firewall - aws_instance.web -> module.consul - aws_instance.web -> provider.aws -aws_security_group.firewall - aws_security_group.firewall -> provider.aws -module.consul - module.consul -> aws_security_group.firewall - module.consul -> provider.aws -provider.aws -root - root -> aws_instance.web - root -> aws_security_group.firewall - root -> module.consul -` - -const testTerraformGraphModulesConsulStr = ` -root: root -aws_instance.server - aws_instance.server -> provider.aws -provider.aws -root - root -> aws_instance.server -` - -const testTerraformGraphModuleOrphanStr = ` -root: root -aws_instance.web - aws_instance.web -> aws_security_group.firewall - aws_instance.web -> provider.aws -aws_security_group.firewall - aws_security_group.firewall -> provider.aws -module.consul - module.consul -> provider.aws -provider.aws -root - root -> aws_instance.web - root -> aws_security_group.firewall - root -> module.consul -` - -const testTerraformGraphModuleOrphanConsulStr = ` -root: root -aws_instance.old - aws_instance.old -> provider.aws -provider.aws -root - root -> aws_instance.old -` - -const testTerraformGraphProviderPruneStr = ` -root: root -aws_load_balancer.weblb - aws_load_balancer.weblb -> provider.aws -provider.aws -root - root -> aws_load_balancer.weblb` - -const testTerraformGraphDiffCreateBeforeDestroyStr = ` -root: root -aws_instance.bar - aws_instance.bar -> provider.aws -aws_instance.bar (destroy) - aws_instance.bar (destroy) -> aws_instance.bar - aws_instance.bar (destroy) -> provider.aws -provider.aws -root - root -> aws_instance.bar - root -> aws_instance.bar (destroy)` - -const testTerraformGraphStateStr = ` -root: root -aws_instance.old - aws_instance.old -> provider.aws -aws_instance.web - aws_instance.web -> aws_security_group.firewall - aws_instance.web -> provider.aws -aws_load_balancer.weblb - aws_load_balancer.weblb -> aws_instance.web - aws_load_balancer.weblb -> provider.aws -aws_security_group.firewall - aws_security_group.firewall -> provider.aws -openstack_floating_ip.random -provider.aws - provider.aws -> openstack_floating_ip.random -root - root -> aws_instance.old - root -> aws_instance.web - root -> aws_load_balancer.weblb - root -> aws_security_group.firewall - root -> openstack_floating_ip.random -` - -const testTerraformGraphTaintedStr = ` -root: root -aws_instance.web - aws_instance.web -> aws_instance.web (tainted #1) - aws_instance.web -> aws_security_group.firewall - aws_instance.web -> provider.aws -aws_instance.web (tainted #1) - aws_instance.web (tainted #1) -> provider.aws -aws_security_group.firewall - aws_security_group.firewall -> provider.aws -provider.aws -root - root -> aws_instance.web - root -> aws_instance.web (tainted #1) - root -> aws_security_group.firewall -` - -const testTerraformGraphTaintedMultiStr = ` -root: root -aws_instance.web - aws_instance.web -> aws_instance.web (tainted #1) - aws_instance.web -> aws_instance.web (tainted #2) - aws_instance.web -> aws_security_group.firewall - aws_instance.web -> provider.aws -aws_instance.web (tainted #1) - aws_instance.web (tainted #1) -> provider.aws -aws_instance.web (tainted #2) - aws_instance.web (tainted #2) -> provider.aws -aws_security_group.firewall - aws_security_group.firewall -> provider.aws -provider.aws -root - root -> aws_instance.web - root -> aws_instance.web (tainted #1) - root -> aws_instance.web (tainted #2) - root -> aws_security_group.firewall -` - -const testTerraformGraphCountOrphanStr = ` -root: root -aws_instance.web -aws_load_balancer.old - aws_load_balancer.old -> aws_instance.web -aws_load_balancer.weblb - aws_load_balancer.weblb -> aws_instance.web -root - root -> aws_instance.web - root -> aws_load_balancer.old - root -> aws_load_balancer.weblb -` - -const testTerraformGraphOrphanModuleDepsStr = ` -root: root -aws_instance.foo - aws_instance.foo -> module.consul - aws_instance.foo -> provider.aws -aws_instance.web - aws_instance.web -> aws_security_group.firewall - aws_instance.web -> module.consul - aws_instance.web -> provider.aws -aws_security_group.firewall - aws_security_group.firewall -> provider.aws -module.consul - module.consul -> aws_security_group.firewall - module.consul -> provider.aws -provider.aws -root - root -> aws_instance.foo - root -> aws_instance.web - root -> aws_security_group.firewall - root -> module.consul -` - -const testTerraformGraphOrphanedModuleDepsStr = ` -root: root -aws_instance.foo - aws_instance.foo -> module.consul - aws_instance.foo -> provider.aws -aws_instance.web - aws_instance.web -> aws_security_group.firewall - aws_instance.web -> module.consul - aws_instance.web -> provider.aws -aws_security_group.firewall - aws_security_group.firewall -> provider.aws -module.consul - module.consul -> aws_security_group.firewall - module.consul -> provider.aws -module.orphan - module.orphan -> aws_instance.foo - module.orphan -> aws_instance.web - module.orphan -> provider.aws -provider.aws -root - root -> aws_instance.foo - root -> aws_instance.web - root -> aws_security_group.firewall - root -> module.consul - root -> module.orphan -` - -const testTerraformGraphResourceExpandStr = ` -root: root -aws_instance.web.0 -aws_instance.web.1 -aws_instance.web.2 -root - root -> aws_instance.web.0 - root -> aws_instance.web.1 - root -> aws_instance.web.2 -` - -const testTerraformGraphResourceExpandProvDepsStr = ` -root: root -aws_instance.web.0 -aws_instance.web.1 - aws_instance.web.1 -> aws_instance.web.0 -aws_instance.web.2 - aws_instance.web.2 -> aws_instance.web.0 -root - root -> aws_instance.web.0 - root -> aws_instance.web.1 - root -> aws_instance.web.2 +const testGraphConnectDepsStr = ` +a +b + a ` diff --git a/terraform/graph_walk.go b/terraform/graph_walk.go new file mode 100644 index 000000000..f5da6c093 --- /dev/null +++ b/terraform/graph_walk.go @@ -0,0 +1,30 @@ +package terraform + +import ( + "github.com/hashicorp/terraform/dag" +) + +// GraphWalker is an interface that can be implemented that when used +// with Graph.Walk will invoke the given callbacks under certain events. +type GraphWalker interface { + EnterGraph(*Graph) EvalContext + ExitGraph(*Graph) + EnterVertex(dag.Vertex) + ExitVertex(dag.Vertex, error) + EnterEvalTree(dag.Vertex, EvalNode) EvalNode + ExitEvalTree(dag.Vertex, interface{}, error) error +} + +// NullGraphWalker is a GraphWalker implementation that does nothing. +// This can be embedded within other GraphWalker implementations for easily +// implementing all the required functions. +type NullGraphWalker struct{} + +func (NullGraphWalker) EnterGraph(*Graph) EvalContext { return nil } +func (NullGraphWalker) ExitGraph(*Graph) {} +func (NullGraphWalker) EnterVertex(dag.Vertex) {} +func (NullGraphWalker) ExitVertex(dag.Vertex, error) {} +func (NullGraphWalker) EnterEvalTree(v dag.Vertex, n EvalNode) EvalNode { return n } +func (NullGraphWalker) ExitEvalTree(dag.Vertex, interface{}, error) error { + return nil +} diff --git a/terraform/graph_walk_context.go b/terraform/graph_walk_context.go new file mode 100644 index 000000000..64a9f3d02 --- /dev/null +++ b/terraform/graph_walk_context.go @@ -0,0 +1,134 @@ +package terraform + +import ( + "fmt" + "sync" + + "github.com/hashicorp/errwrap" + "github.com/hashicorp/terraform/dag" +) + +// ContextGraphWalker is the GraphWalker implementation used with the +// Context struct to walk and evaluate the graph. +type ContextGraphWalker struct { + NullGraphWalker + + // Configurable values + Context *Context + Operation walkOperation + + // Outputs, do not set these. Do not read these while the graph + // is being walked. + ValidationWarnings []string + ValidationErrors []error + + errorLock sync.Mutex + once sync.Once + contexts map[string]*BuiltinEvalContext + contextLock sync.Mutex + providerCache map[string]ResourceProvider + providerConfigCache map[string]*ResourceConfig + providerLock sync.Mutex + provisionerCache map[string]ResourceProvisioner + provisionerLock sync.Mutex +} + +func (w *ContextGraphWalker) EnterGraph(g *Graph) EvalContext { + w.once.Do(w.init) + + w.contextLock.Lock() + defer w.contextLock.Unlock() + + // If we already have a context for this path cached, use that + key := PathCacheKey(g.Path) + if ctx, ok := w.contexts[key]; ok { + return ctx + } + + // Variables should be our context variables, but these only apply + // to the root module. As we enter subgraphs, we don't want to set + // variables, which is set by the SetVariables EvalContext function. + variables := w.Context.variables + if len(g.Path) > 1 { + // We're in a submodule, the variables should be empty + variables = make(map[string]string) + } + + ctx := &BuiltinEvalContext{ + PathValue: g.Path, + Hooks: w.Context.hooks, + InputValue: w.Context.uiInput, + Providers: w.Context.providers, + ProviderCache: w.providerCache, + ProviderConfigCache: w.providerConfigCache, + ProviderInputConfig: w.Context.providerInputConfig, + ProviderLock: &w.providerLock, + Provisioners: w.Context.provisioners, + ProvisionerCache: w.provisionerCache, + ProvisionerLock: &w.provisionerLock, + DiffValue: w.Context.diff, + DiffLock: &w.Context.diffLock, + StateValue: w.Context.state, + StateLock: &w.Context.stateLock, + Interpolater: &Interpolater{ + Operation: w.Operation, + Module: w.Context.module, + State: w.Context.state, + StateLock: &w.Context.stateLock, + Variables: variables, + }, + } + + w.contexts[key] = ctx + return ctx +} + +func (w *ContextGraphWalker) EnterEvalTree(v dag.Vertex, n EvalNode) EvalNode { + // Acquire a lock on the semaphore + w.Context.parallelSem.Acquire() + + // We want to filter the evaluation tree to only include operations + // that belong in this operation. + return EvalFilter(n, EvalNodeFilterOp(w.Operation)) +} + +func (w *ContextGraphWalker) ExitEvalTree( + v dag.Vertex, output interface{}, err error) error { + // Release the semaphore + w.Context.parallelSem.Release() + + if err == nil { + return nil + } + + // Acquire the lock because anything is going to require a lock. + w.errorLock.Lock() + defer w.errorLock.Unlock() + + // Try to get a validation error out of it. If its not a validation + // error, then just record the normal error. + verr, ok := err.(*EvalValidateError) + if !ok { + return err + } + + for _, msg := range verr.Warnings { + w.ValidationWarnings = append( + w.ValidationWarnings, + fmt.Sprintf("%s: %s", dag.VertexName(v), msg)) + } + for _, e := range verr.Errors { + w.ValidationErrors = append( + w.ValidationErrors, + errwrap.Wrapf(fmt.Sprintf("%s: {{err}}", dag.VertexName(v)), e)) + } + + return nil +} + +func (w *ContextGraphWalker) init() { + w.contexts = make(map[string]*BuiltinEvalContext, 5) + w.providerCache = make(map[string]ResourceProvider, 5) + w.providerConfigCache = make(map[string]*ResourceConfig, 5) + w.provisionerCache = make(map[string]ResourceProvisioner, 5) +} diff --git a/terraform/graph_walk_operation.go b/terraform/graph_walk_operation.go new file mode 100644 index 000000000..c2143fbd8 --- /dev/null +++ b/terraform/graph_walk_operation.go @@ -0,0 +1,16 @@ +package terraform + +//go:generate stringer -type=walkOperation graph_walk_operation.go + +// walkOperation is an enum which tells the walkContext what to do. +type walkOperation byte + +const ( + walkInvalid walkOperation = iota + walkInput + walkApply + walkPlan + walkPlanDestroy + walkRefresh + walkValidate +) diff --git a/terraform/graph_walk_test.go b/terraform/graph_walk_test.go new file mode 100644 index 000000000..88b52a748 --- /dev/null +++ b/terraform/graph_walk_test.go @@ -0,0 +1,9 @@ +package terraform + +import ( + "testing" +) + +func TestNullGraphWalker_impl(t *testing.T) { + var _ GraphWalker = NullGraphWalker{} +} diff --git a/terraform/interpolate.go b/terraform/interpolate.go new file mode 100644 index 000000000..2d5798aa0 --- /dev/null +++ b/terraform/interpolate.go @@ -0,0 +1,440 @@ +package terraform + +import ( + "fmt" + "os" + "strings" + "sync" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/config/lang/ast" + "github.com/hashicorp/terraform/config/module" +) + +// Interpolater is the structure responsible for determining the values +// for interpolations such as `aws_instance.foo.bar`. +type Interpolater struct { + Operation walkOperation + Module *module.Tree + State *State + StateLock *sync.RWMutex + Variables map[string]string +} + +// InterpolationScope is the current scope of execution. This is required +// since some variables which are interpolated are dependent on what we're +// operating on and where we are. +type InterpolationScope struct { + Path []string + Resource *Resource +} + +// Values returns the values for all the variables in the given map. +func (i *Interpolater) Values( + scope *InterpolationScope, + vars map[string]config.InterpolatedVariable) (map[string]ast.Variable, error) { + result := make(map[string]ast.Variable, len(vars)) + + // Copy the default variables + if i.Module != nil && scope != nil { + mod := i.Module + if len(scope.Path) > 1 { + mod = i.Module.Child(scope.Path[1:]) + } + for _, v := range mod.Config().Variables { + for k, val := range v.DefaultsMap() { + result[k] = ast.Variable{ + Value: val, + Type: ast.TypeString, + } + } + } + } + + for n, rawV := range vars { + var err error + switch v := rawV.(type) { + case *config.CountVariable: + err = i.valueCountVar(scope, n, v, result) + case *config.ModuleVariable: + err = i.valueModuleVar(scope, n, v, result) + case *config.PathVariable: + err = i.valuePathVar(scope, n, v, result) + case *config.ResourceVariable: + err = i.valueResourceVar(scope, n, v, result) + case *config.UserVariable: + err = i.valueUserVar(scope, n, v, result) + default: + err = fmt.Errorf("%s: unknown variable type: %T", n, rawV) + } + + if err != nil { + return nil, err + } + } + + return result, nil +} + +func (i *Interpolater) valueCountVar( + scope *InterpolationScope, + n string, + v *config.CountVariable, + result map[string]ast.Variable) error { + switch v.Type { + case config.CountValueIndex: + result[n] = ast.Variable{ + Value: scope.Resource.CountIndex, + Type: ast.TypeInt, + } + return nil + default: + return fmt.Errorf("%s: unknown count type: %#v", n, v.Type) + } +} + +func (i *Interpolater) valueModuleVar( + scope *InterpolationScope, + n string, + v *config.ModuleVariable, + result map[string]ast.Variable) error { + // If we're computing all dynamic fields, then module vars count + // and we mark it as computed. + if i.Operation == walkValidate { + result[n] = ast.Variable{ + Value: config.UnknownVariableValue, + Type: ast.TypeString, + } + return nil + } + + // Build the path to the child module we want + path := make([]string, len(scope.Path), len(scope.Path)+1) + copy(path, scope.Path) + path = append(path, v.Name) + + // Grab the lock so that if other interpolations are running or + // state is being modified, we'll be safe. + i.StateLock.RLock() + defer i.StateLock.RUnlock() + + // Get the module where we're looking for the value + var value string + mod := i.State.ModuleByPath(path) + if mod == nil { + // If the module doesn't exist, then we can return an empty string. + // This happens usually only in Refresh() when we haven't populated + // a state. During validation, we semantically verify that all + // modules reference other modules, and graph ordering should + // ensure that the module is in the state, so if we reach this + // point otherwise it really is a panic. + value = config.UnknownVariableValue + } else { + // Get the value from the outputs + var ok bool + value, ok = mod.Outputs[v.Field] + if !ok { + // Same reasons as the comment above. + value = config.UnknownVariableValue + } + } + + result[n] = ast.Variable{ + Value: value, + Type: ast.TypeString, + } + return nil +} + +func (i *Interpolater) valuePathVar( + scope *InterpolationScope, + n string, + v *config.PathVariable, + result map[string]ast.Variable) error { + switch v.Type { + case config.PathValueCwd: + wd, err := os.Getwd() + if err != nil { + return fmt.Errorf( + "Couldn't get cwd for var %s: %s", + v.FullKey(), err) + } + + result[n] = ast.Variable{ + Value: wd, + Type: ast.TypeString, + } + case config.PathValueModule: + if t := i.Module.Child(scope.Path[1:]); t != nil { + result[n] = ast.Variable{ + Value: t.Config().Dir, + Type: ast.TypeString, + } + } + case config.PathValueRoot: + result[n] = ast.Variable{ + Value: i.Module.Config().Dir, + Type: ast.TypeString, + } + default: + return fmt.Errorf("%s: unknown path type: %#v", n, v.Type) + } + + return nil + +} + +func (i *Interpolater) valueResourceVar( + scope *InterpolationScope, + n string, + v *config.ResourceVariable, + result map[string]ast.Variable) error { + // If we're computing all dynamic fields, then module vars count + // and we mark it as computed. + if i.Operation == walkValidate || i.Operation == walkRefresh { + result[n] = ast.Variable{ + Value: config.UnknownVariableValue, + Type: ast.TypeString, + } + return nil + } + + var attr string + var err error + if v.Multi && v.Index == -1 { + attr, err = i.computeResourceMultiVariable(scope, v) + } else { + attr, err = i.computeResourceVariable(scope, v) + } + if err != nil { + return err + } + + result[n] = ast.Variable{ + Value: attr, + Type: ast.TypeString, + } + return nil +} + +func (i *Interpolater) valueUserVar( + scope *InterpolationScope, + n string, + v *config.UserVariable, + result map[string]ast.Variable) error { + val, ok := i.Variables[v.Name] + if ok { + result[n] = ast.Variable{ + Value: val, + Type: ast.TypeString, + } + return nil + } + + if _, ok := result[n]; !ok && i.Operation == walkValidate { + result[n] = ast.Variable{ + Value: config.UnknownVariableValue, + Type: ast.TypeString, + } + return nil + } + + // Look up if we have any variables with this prefix because + // those are map overrides. Include those. + for k, val := range i.Variables { + if strings.HasPrefix(k, v.Name+".") { + result["var."+k] = ast.Variable{ + Value: val, + Type: ast.TypeString, + } + } + } + + return nil +} + +func (i *Interpolater) computeResourceVariable( + scope *InterpolationScope, + v *config.ResourceVariable) (string, error) { + id := v.ResourceId() + if v.Multi { + id = fmt.Sprintf("%s.%d", id, v.Index) + } + + i.StateLock.RLock() + defer i.StateLock.RUnlock() + + // Get the information about this resource variable, and verify + // that it exists and such. + module, _, err := i.resourceVariableInfo(scope, v) + if err != nil { + return "", err + } + + // If we have no module in the state yet or count, return empty + if module == nil || len(module.Resources) == 0 { + return "", nil + } + + // Get the resource out from the state. We know the state exists + // at this point and if there is a state, we expect there to be a + // resource with the given name. + r, ok := module.Resources[id] + if !ok && v.Multi && v.Index == 0 { + r, ok = module.Resources[v.ResourceId()] + } + if !ok { + r = nil + } + if r == nil { + return "", fmt.Errorf( + "Resource '%s' not found for variable '%s'", + id, + v.FullKey()) + } + + if r.Primary == nil { + goto MISSING + } + + if attr, ok := r.Primary.Attributes[v.Field]; ok { + return attr, nil + } + + // At apply time, we can't do the "maybe has it" check below + // that we need for plans since parent elements might be computed. + // Therefore, it is an error and we're missing the key. + // + // TODO: test by creating a state and configuration that is referencing + // a non-existent variable "foo.bar" where the state only has "foo" + // and verify plan works, but apply doesn't. + if i.Operation == walkApply { + goto MISSING + } + + // We didn't find the exact field, so lets separate the dots + // and see if anything along the way is a computed set. i.e. if + // we have "foo.0.bar" as the field, check to see if "foo" is + // a computed list. If so, then the whole thing is computed. + if parts := strings.Split(v.Field, "."); len(parts) > 1 { + for i := 1; i < len(parts); i++ { + // Lists and sets make this + key := fmt.Sprintf("%s.#", strings.Join(parts[:i], ".")) + if attr, ok := r.Primary.Attributes[key]; ok { + return attr, nil + } + + // Maps make this + key = fmt.Sprintf("%s", strings.Join(parts[:i], ".")) + if attr, ok := r.Primary.Attributes[key]; ok { + return attr, nil + } + } + } + +MISSING: + return "", fmt.Errorf( + "Resource '%s' does not have attribute '%s' "+ + "for variable '%s'", + id, + v.Field, + v.FullKey()) +} + +func (i *Interpolater) computeResourceMultiVariable( + scope *InterpolationScope, + v *config.ResourceVariable) (string, error) { + i.StateLock.RLock() + defer i.StateLock.RUnlock() + + // Get the information about this resource variable, and verify + // that it exists and such. + module, cr, err := i.resourceVariableInfo(scope, v) + if err != nil { + return "", err + } + + // Get the count so we know how many to iterate over + count, err := cr.Count() + if err != nil { + return "", fmt.Errorf( + "Error reading %s count: %s", + v.ResourceId(), + err) + } + + // If we have no module in the state yet or count, return empty + if module == nil || len(module.Resources) == 0 || count == 0 { + return "", nil + } + + var values []string + for i := 0; i < count; i++ { + id := fmt.Sprintf("%s.%d", v.ResourceId(), i) + + // If we're dealing with only a single resource, then the + // ID doesn't have a trailing index. + if count == 1 { + id = v.ResourceId() + } + + r, ok := module.Resources[id] + if !ok { + continue + } + + if r.Primary == nil { + continue + } + + attr, ok := r.Primary.Attributes[v.Field] + if !ok { + continue + } + + values = append(values, attr) + } + + if len(values) == 0 { + return "", fmt.Errorf( + "Resource '%s' does not have attribute '%s' "+ + "for variable '%s'", + v.ResourceId(), + v.Field, + v.FullKey()) + } + + return strings.Join(values, config.InterpSplitDelim), nil +} + +func (i *Interpolater) resourceVariableInfo( + scope *InterpolationScope, + v *config.ResourceVariable) (*ModuleState, *config.Resource, error) { + // Get the module tree that contains our current path. This is + // either the current module (path is empty) or a child. + modTree := i.Module + if len(scope.Path) > 1 { + modTree = i.Module.Child(scope.Path[1:]) + } + + // Get the resource from the configuration so we can verify + // that the resource is in the configuration and so we can access + // the configuration if we need to. + var cr *config.Resource + for _, r := range modTree.Config().Resources { + if r.Id() == v.ResourceId() { + cr = r + break + } + } + if cr == nil { + return nil, nil, fmt.Errorf( + "Resource '%s' not found for variable '%s'", + v.ResourceId(), + v.FullKey()) + } + + // Get the relevant module + module := i.State.ModuleByPath(scope.Path) + return module, cr, nil +} diff --git a/terraform/interpolate_test.go b/terraform/interpolate_test.go new file mode 100644 index 000000000..13d56fffb --- /dev/null +++ b/terraform/interpolate_test.go @@ -0,0 +1,135 @@ +package terraform + +import ( + "os" + "reflect" + "sync" + "testing" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/config/lang/ast" +) + +func TestInterpolater_countIndex(t *testing.T) { + i := &Interpolater{} + + scope := &InterpolationScope{ + Path: rootModulePath, + Resource: &Resource{CountIndex: 42}, + } + + testInterpolate(t, i, scope, "count.index", ast.Variable{ + Value: 42, + Type: ast.TypeInt, + }) +} + +func TestInterpolater_moduleVariable(t *testing.T) { + lock := new(sync.RWMutex) + state := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.web": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "bar", + }, + }, + }, + }, + &ModuleState{ + Path: []string{RootModuleName, "child"}, + Outputs: map[string]string{ + "foo": "bar", + }, + }, + }, + } + + i := &Interpolater{ + State: state, + StateLock: lock, + } + + scope := &InterpolationScope{ + Path: rootModulePath, + } + + testInterpolate(t, i, scope, "module.child.foo", ast.Variable{ + Value: "bar", + Type: ast.TypeString, + }) +} + +func TestInterpolater_pathCwd(t *testing.T) { + i := &Interpolater{} + scope := &InterpolationScope{} + + expected, err := os.Getwd() + if err != nil { + t.Fatalf("err: %s", err) + } + + testInterpolate(t, i, scope, "path.cwd", ast.Variable{ + Value: expected, + Type: ast.TypeString, + }) +} + +func TestInterpolater_pathModule(t *testing.T) { + mod := testModule(t, "interpolate-path-module") + i := &Interpolater{ + Module: mod, + } + scope := &InterpolationScope{ + Path: []string{RootModuleName, "child"}, + } + + path := mod.Child([]string{"child"}).Config().Dir + testInterpolate(t, i, scope, "path.module", ast.Variable{ + Value: path, + Type: ast.TypeString, + }) +} + +func TestInterpolater_pathRoot(t *testing.T) { + mod := testModule(t, "interpolate-path-module") + i := &Interpolater{ + Module: mod, + } + scope := &InterpolationScope{ + Path: []string{RootModuleName, "child"}, + } + + path := mod.Config().Dir + testInterpolate(t, i, scope, "path.root", ast.Variable{ + Value: path, + Type: ast.TypeString, + }) +} + +func testInterpolate( + t *testing.T, i *Interpolater, + scope *InterpolationScope, + n string, expectedVar ast.Variable) { + v, err := config.NewInterpolatedVariable(n) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual, err := i.Values(scope, map[string]config.InterpolatedVariable{ + "foo": v, + }) + if err != nil { + t.Fatalf("err: %s", err) + } + + expected := map[string]ast.Variable{ + "foo": expectedVar, + } + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) + } +} diff --git a/terraform/path.go b/terraform/path.go new file mode 100644 index 000000000..ca99685ad --- /dev/null +++ b/terraform/path.go @@ -0,0 +1,24 @@ +package terraform + +import ( + "crypto/md5" + "encoding/hex" +) + +// PathCacheKey returns a cache key for a module path. +// +// TODO: test +func PathCacheKey(path []string) string { + // There is probably a better way to do this, but this is working for now. + // We just create an MD5 hash of all the MD5 hashes of all the path + // elements. This gets us the property that it is unique per ordering. + hash := md5.New() + for _, p := range path { + single := md5.Sum([]byte(p)) + if _, err := hash.Write(single[:]); err != nil { + panic(err) + } + } + + return hex.EncodeToString(hash.Sum(nil)) +} diff --git a/terraform/resource.go b/terraform/resource.go index cbcd7814d..7775ab789 100644 --- a/terraform/resource.go +++ b/terraform/resource.go @@ -93,7 +93,7 @@ type ResourceConfig struct { // NewResourceConfig creates a new ResourceConfig from a config.RawConfig. func NewResourceConfig(c *config.RawConfig) *ResourceConfig { result := &ResourceConfig{raw: c} - result.interpolate(nil, nil) + result.interpolateForce() return result } @@ -201,28 +201,19 @@ func (c *ResourceConfig) get( return current, true } -func (c *ResourceConfig) interpolate( - ctx *walkContext, r *Resource) error { - if c == nil { - return nil - } - - if ctx != nil { - if err := ctx.computeVars(c.raw, r); err != nil { - return err - } - } - +// interpolateForce is a temporary thing. We want to get rid of interpolate +// above and likewise this, but it can only be done after the f-ast-graph +// refactor is complete. +func (c *ResourceConfig) interpolateForce() { if c.raw == nil { var err error c.raw, err = config.NewRawConfig(make(map[string]interface{})) if err != nil { - return err + panic(err) } } c.ComputedKeys = c.raw.UnknownKeys() c.Raw = c.raw.Raw c.Config = c.raw.Config() - return nil } diff --git a/terraform/resource_test.go b/terraform/resource_test.go index cc260ec3f..4b23be0d1 100644 --- a/terraform/resource_test.go +++ b/terraform/resource_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/config/lang/ast" ) func TestInstanceInfo(t *testing.T) { @@ -99,20 +100,33 @@ func TestResourceConfigGet(t *testing.T) { } } - rc := NewResourceConfig(rawC) if tc.Vars != nil { - ctx := NewContext(&ContextOpts{Variables: tc.Vars}) - err := rc.interpolate( - ctx.walkContext(walkInvalid, rootModulePath), - nil) - if err != nil { + vs := make(map[string]ast.Variable) + for k, v := range tc.Vars { + vs["var."+k] = ast.Variable{Value: v, Type: ast.TypeString} + } + + if err := rawC.Interpolate(vs); err != nil { t.Fatalf("err: %s", err) } } + rc := NewResourceConfig(rawC) + rc.interpolateForce() + v, _ := rc.Get(tc.Key) if !reflect.DeepEqual(v, tc.Value) { t.Fatalf("%d bad: %#v", i, v) } } } + +func testResourceConfig( + t *testing.T, c map[string]interface{}) *ResourceConfig { + raw, err := config.NewRawConfig(c) + if err != nil { + t.Fatalf("err: %s", err) + } + + return NewResourceConfig(raw) +} diff --git a/terraform/semantics.go b/terraform/semantics.go index 51c17b68a..566a3c279 100644 --- a/terraform/semantics.go +++ b/terraform/semantics.go @@ -3,9 +3,70 @@ package terraform import ( "fmt" + "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/dag" ) +// GraphSemanticChecker is the interface that semantic checks across +// the entire Terraform graph implement. +// +// The graph should NOT be modified by the semantic checker. +type GraphSemanticChecker interface { + Check(*dag.Graph) error +} + +// UnorderedSemanticCheckRunner is an implementation of GraphSemanticChecker +// that runs a list of SemanticCheckers against the vertices of the graph +// in no specified order. +type UnorderedSemanticCheckRunner struct { + Checks []SemanticChecker +} + +func (sc *UnorderedSemanticCheckRunner) Check(g *dag.Graph) error { + var err error + for _, v := range g.Vertices() { + for _, check := range sc.Checks { + if e := check.Check(g, v); e != nil { + err = multierror.Append(err, e) + } + } + } + + return err +} + +// SemanticChecker is the interface that semantic checks across the +// Terraform graph implement. Errors are accumulated. Even after an error +// is returned, child vertices in the graph will still be visited. +// +// The graph should NOT be modified by the semantic checker. +// +// The order in which vertices are visited is left unspecified, so the +// semantic checks should not rely on that. +type SemanticChecker interface { + Check(*dag.Graph, dag.Vertex) error +} + +// SemanticCheckModulesExist is an implementation of SemanticChecker that +// verifies that all the modules that are referenced in the graph exist. +type SemanticCheckModulesExist struct{} + +// TODO: test +func (*SemanticCheckModulesExist) Check(g *dag.Graph, v dag.Vertex) error { + mn, ok := v.(*GraphNodeConfigModule) + if !ok { + return nil + } + + if mn.Tree == nil { + return fmt.Errorf( + "module '%s' not found", mn.Module.Name) + } + + return nil +} + // smcUserVariables does all the semantic checks to verify that the // variables given satisfy the configuration itself. func smcUserVariables(c *config.Config, vs map[string]string) []error { diff --git a/terraform/state.go b/terraform/state.go index dea8fe961..22a0935b7 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -100,6 +100,31 @@ func (s *State) ModuleByPath(path []string) *ModuleState { return nil } +// ModuleOrphans returns all the module orphans in this state by +// returning their full paths. These paths can be used with ModuleByPath +// to return the actual state. +func (s *State) ModuleOrphans(path []string, c *config.Config) [][]string { + childrenKeys := make(map[string]struct{}) + if c != nil { + for _, m := range c.Modules { + childrenKeys[m.Name] = struct{}{} + } + } + + // Go over the direct children and find any that aren't in our + // keys. + var orphans [][]string + for _, m := range s.Children(path) { + if _, ok := childrenKeys[m.Path[len(m.Path)-1]]; ok { + continue + } + + orphans = append(orphans, m.Path) + } + + return orphans +} + // RootModule returns the ModuleState for the root module func (s *State) RootModule() *ModuleState { root := s.ModuleByPath(rootModulePath) @@ -278,12 +303,14 @@ func (m *ModuleState) Orphans(c *config.Config) []string { keys[k] = struct{}{} } - for _, r := range c.Resources { - delete(keys, r.Id()) + if c != nil { + for _, r := range c.Resources { + delete(keys, r.Id()) - for k, _ := range keys { - if strings.HasPrefix(k, r.Id()+".") { - delete(keys, k) + for k, _ := range keys { + if strings.HasPrefix(k, r.Id()+".") { + delete(keys, k) + } } } } @@ -346,10 +373,17 @@ func (m *ModuleState) deepcopy() *ModuleState { func (m *ModuleState) prune() { for k, v := range m.Resources { v.prune() + if (v.Primary == nil || v.Primary.ID == "") && len(v.Tainted) == 0 { delete(m.Resources, k) } } + + for k, v := range m.Outputs { + if v == config.UnknownVariableValue { + delete(m.Outputs, k) + } + } } func (m *ModuleState) sort() { @@ -517,12 +551,14 @@ func (r *ResourceState) prune() { n := len(r.Tainted) for i := 0; i < n; i++ { inst := r.Tainted[i] - if inst.ID == "" { + if inst == nil || inst.ID == "" { copy(r.Tainted[i:], r.Tainted[i+1:]) r.Tainted[n-1] = nil n-- + i-- } } + r.Tainted = r.Tainted[:n] } diff --git a/terraform/state_test.go b/terraform/state_test.go index 7a5554651..8211a35b5 100644 --- a/terraform/state_test.go +++ b/terraform/state_test.go @@ -59,6 +59,58 @@ func TestStateAddModule(t *testing.T) { } } +func TestStateModuleOrphans(t *testing.T) { + state := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: RootModulePath, + }, + &ModuleState{ + Path: []string{RootModuleName, "foo"}, + }, + &ModuleState{ + Path: []string{RootModuleName, "bar"}, + }, + }, + } + + config := testModule(t, "state-module-orphans").Config() + actual := state.ModuleOrphans(RootModulePath, config) + expected := [][]string{ + []string{RootModuleName, "foo"}, + } + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) + } +} + +func TestStateModuleOrphans_nilConfig(t *testing.T) { + state := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: RootModulePath, + }, + &ModuleState{ + Path: []string{RootModuleName, "foo"}, + }, + &ModuleState{ + Path: []string{RootModuleName, "bar"}, + }, + }, + } + + actual := state.ModuleOrphans(RootModulePath, nil) + expected := [][]string{ + []string{RootModuleName, "foo"}, + []string{RootModuleName, "bar"}, + } + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) + } +} + func TestInstanceState_MergeDiff(t *testing.T) { is := InstanceState{ ID: "foo", diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index cede986f8..d4b56967f 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -200,6 +200,13 @@ aws_instance.bar: type = aws_instance ` +const testTerraformApplyCreateBeforeUpdateStr = ` +aws_instance.bar: + ID = foo + foo = baz + type = aws_instance +` + const testTerraformApplyCancelStr = ` aws_instance.foo: ID = foo @@ -233,7 +240,7 @@ aws_instance.foo.1: ` const testTerraformApplyCountDecToOneStr = ` -aws_instance.foo.0: +aws_instance.foo: ID = bar foo = foo type = aws_instance @@ -278,6 +285,17 @@ module.child: type = aws_instance ` +const testTerraformApplyMultiProviderStr = ` +aws_instance.bar: + ID = foo + foo = bar + type = aws_instance +do_instance.foo: + ID = foo + num = 2 + type = do_instance +` + const testTerraformApplyProvisionerStr = ` aws_instance.bar: ID = foo diff --git a/terraform/test-fixtures/apply-good-create-before-update/main.tf b/terraform/test-fixtures/apply-good-create-before-update/main.tf new file mode 100644 index 000000000..d0a2fc937 --- /dev/null +++ b/terraform/test-fixtures/apply-good-create-before-update/main.tf @@ -0,0 +1,7 @@ +resource "aws_instance" "bar" { + foo = "baz" + + lifecycle { + create_before_destroy = true + } +} diff --git a/terraform/test-fixtures/apply-multi-provider/main.tf b/terraform/test-fixtures/apply-multi-provider/main.tf new file mode 100644 index 000000000..4ee94a3bf --- /dev/null +++ b/terraform/test-fixtures/apply-multi-provider/main.tf @@ -0,0 +1,7 @@ +resource "do_instance" "foo" { + num = "2" +} + +resource "aws_instance" "bar" { + foo = "bar" +} diff --git a/terraform/test-fixtures/graph-builder-basic/main.tf b/terraform/test-fixtures/graph-builder-basic/main.tf new file mode 100644 index 000000000..add0dd43f --- /dev/null +++ b/terraform/test-fixtures/graph-builder-basic/main.tf @@ -0,0 +1,5 @@ +provider "aws" {} +resource "aws_instance" "db" {} +resource "aws_instance" "web" { + foo = "${aws_instance.db.id}" +} diff --git a/terraform/test-fixtures/graph-builder-cbd-non-cbd/main.tf b/terraform/test-fixtures/graph-builder-cbd-non-cbd/main.tf new file mode 100644 index 000000000..f478d4f33 --- /dev/null +++ b/terraform/test-fixtures/graph-builder-cbd-non-cbd/main.tf @@ -0,0 +1,9 @@ +provider "aws" {} + +resource "aws_lc" "foo" {} + +resource "aws_asg" "foo" { + lc = "${aws_lc.foo.id}" + + lifecycle { create_before_destroy = true } +} diff --git a/terraform/test-fixtures/graph-builder-modules/consul/main.tf b/terraform/test-fixtures/graph-builder-modules/consul/main.tf new file mode 100644 index 000000000..8b0469b8c --- /dev/null +++ b/terraform/test-fixtures/graph-builder-modules/consul/main.tf @@ -0,0 +1,2 @@ +provider "aws" {} +resource "aws_instance" "server" {} diff --git a/terraform/test-fixtures/graph-builder-modules/main.tf b/terraform/test-fixtures/graph-builder-modules/main.tf new file mode 100644 index 000000000..8e8b532dd --- /dev/null +++ b/terraform/test-fixtures/graph-builder-modules/main.tf @@ -0,0 +1,16 @@ +module "consul" { + foo = "${aws_security_group.firewall.foo}" + source = "./consul" +} + +provider "aws" {} + +resource "aws_security_group" "firewall" {} + +resource "aws_instance" "web" { + security_groups = [ + "foo", + "${aws_security_group.firewall.foo}", + "${module.consul.security_group}" + ] +} diff --git a/terraform/test-fixtures/graph-missing-deps/main.tf b/terraform/test-fixtures/graph-missing-deps/main.tf new file mode 100644 index 000000000..44297f318 --- /dev/null +++ b/terraform/test-fixtures/graph-missing-deps/main.tf @@ -0,0 +1,5 @@ +resource "aws_instance" "db" {} + +resource "aws_instance" "web" { + foo = "${aws_instance.lb.id}" +} diff --git a/terraform/test-fixtures/graph-node-module-expand/child/main.tf b/terraform/test-fixtures/graph-node-module-expand/child/main.tf new file mode 100644 index 000000000..f14f189b0 --- /dev/null +++ b/terraform/test-fixtures/graph-node-module-expand/child/main.tf @@ -0,0 +1,4 @@ +resource "aws_instance" "foo" {} +resource "aws_instance" "bar" { + var = "${aws_instance.foo.whatever}" +} diff --git a/terraform/test-fixtures/graph-node-module-expand/main.tf b/terraform/test-fixtures/graph-node-module-expand/main.tf new file mode 100644 index 000000000..0f6991c53 --- /dev/null +++ b/terraform/test-fixtures/graph-node-module-expand/main.tf @@ -0,0 +1,3 @@ +module "child" { + source = "./child" +} diff --git a/terraform/test-fixtures/graph-outputs/main.tf b/terraform/test-fixtures/graph-outputs/main.tf new file mode 100644 index 000000000..92c4bf226 --- /dev/null +++ b/terraform/test-fixtures/graph-outputs/main.tf @@ -0,0 +1,5 @@ +resource "aws_instance" "foo" {} + +output "foo" { + value = "${aws_instance.foo.value}" +} diff --git a/terraform/test-fixtures/interpolate-path-module/child/main.tf b/terraform/test-fixtures/interpolate-path-module/child/main.tf new file mode 100644 index 000000000..e69de29bb diff --git a/terraform/test-fixtures/interpolate-path-module/main.tf b/terraform/test-fixtures/interpolate-path-module/main.tf new file mode 100644 index 000000000..0f6991c53 --- /dev/null +++ b/terraform/test-fixtures/interpolate-path-module/main.tf @@ -0,0 +1,3 @@ +module "child" { + source = "./child" +} diff --git a/terraform/test-fixtures/state-module-orphans/bar/main.tf b/terraform/test-fixtures/state-module-orphans/bar/main.tf new file mode 100644 index 000000000..c01ade299 --- /dev/null +++ b/terraform/test-fixtures/state-module-orphans/bar/main.tf @@ -0,0 +1 @@ +# Nothing diff --git a/terraform/test-fixtures/state-module-orphans/main.tf b/terraform/test-fixtures/state-module-orphans/main.tf new file mode 100644 index 000000000..f009f1924 --- /dev/null +++ b/terraform/test-fixtures/state-module-orphans/main.tf @@ -0,0 +1,3 @@ +module "bar" { + source = "./bar" +} diff --git a/terraform/test-fixtures/transform-create-before-destroy-basic/main.tf b/terraform/test-fixtures/transform-create-before-destroy-basic/main.tf new file mode 100644 index 000000000..478c911c0 --- /dev/null +++ b/terraform/test-fixtures/transform-create-before-destroy-basic/main.tf @@ -0,0 +1,9 @@ +resource "aws_instance" "web" { + lifecycle { + create_before_destroy = true + } +} + +resource "aws_load_balancer" "lb" { + member = "${aws_instance.web.id}" +} diff --git a/terraform/test-fixtures/transform-create-before-destroy-twice/main.tf b/terraform/test-fixtures/transform-create-before-destroy-twice/main.tf new file mode 100644 index 000000000..c84a7a678 --- /dev/null +++ b/terraform/test-fixtures/transform-create-before-destroy-twice/main.tf @@ -0,0 +1,9 @@ +resource "aws_lc" "foo" { + lifecycle { create_before_destroy = true } +} + +resource "aws_autoscale" "bar" { + lc = "${aws_lc.foo.id}" + + lifecycle { create_before_destroy = true } +} diff --git a/terraform/test-fixtures/transform-destroy-basic/main.tf b/terraform/test-fixtures/transform-destroy-basic/main.tf new file mode 100644 index 000000000..14bca3e82 --- /dev/null +++ b/terraform/test-fixtures/transform-destroy-basic/main.tf @@ -0,0 +1,5 @@ +resource "aws_instance" "foo" {} + +resource "aws_instance" "bar" { + value = "${aws_instance.foo.value}" +} diff --git a/terraform/test-fixtures/transform-destroy-deps/main.tf b/terraform/test-fixtures/transform-destroy-deps/main.tf new file mode 100644 index 000000000..1419d893c --- /dev/null +++ b/terraform/test-fixtures/transform-destroy-deps/main.tf @@ -0,0 +1,5 @@ +resource "aws_lc" "foo" {} + +resource "aws_asg" "bar" { + lc = "${aws_lc.foo.id}" +} diff --git a/terraform/test-fixtures/transform-destroy-prune-count/main.tf b/terraform/test-fixtures/transform-destroy-prune-count/main.tf new file mode 100644 index 000000000..756ae10d5 --- /dev/null +++ b/terraform/test-fixtures/transform-destroy-prune-count/main.tf @@ -0,0 +1,6 @@ +resource "aws_instance" "foo" {} + +resource "aws_instance" "bar" { + value = "${aws_instance.foo.value}" + count = "5" +} diff --git a/terraform/test-fixtures/transform-orphan-basic/main.tf b/terraform/test-fixtures/transform-orphan-basic/main.tf new file mode 100644 index 000000000..64cbf6236 --- /dev/null +++ b/terraform/test-fixtures/transform-orphan-basic/main.tf @@ -0,0 +1 @@ +resource "aws_instance" "web" {} diff --git a/terraform/test-fixtures/transform-orphan-modules/main.tf b/terraform/test-fixtures/transform-orphan-modules/main.tf new file mode 100644 index 000000000..919f140bb --- /dev/null +++ b/terraform/test-fixtures/transform-orphan-modules/main.tf @@ -0,0 +1 @@ +resource "aws_instance" "foo" {} diff --git a/terraform/test-fixtures/transform-provider-basic/main.tf b/terraform/test-fixtures/transform-provider-basic/main.tf new file mode 100644 index 000000000..8a44e1dcb --- /dev/null +++ b/terraform/test-fixtures/transform-provider-basic/main.tf @@ -0,0 +1,2 @@ +provider "aws" {} +resource "aws_instance" "web" {} diff --git a/terraform/test-fixtures/transform-provider-prune/main.tf b/terraform/test-fixtures/transform-provider-prune/main.tf new file mode 100644 index 000000000..986f8840b --- /dev/null +++ b/terraform/test-fixtures/transform-provider-prune/main.tf @@ -0,0 +1,2 @@ +provider "aws" {} +resource "foo_instance" "web" {} diff --git a/terraform/test-fixtures/transform-provisioner-basic/main.tf b/terraform/test-fixtures/transform-provisioner-basic/main.tf new file mode 100644 index 000000000..3898ac4db --- /dev/null +++ b/terraform/test-fixtures/transform-provisioner-basic/main.tf @@ -0,0 +1,3 @@ +resource "aws_instance" "web" { + provisioner "shell" {} +} diff --git a/terraform/test-fixtures/transform-provisioner-prune/main.tf b/terraform/test-fixtures/transform-provisioner-prune/main.tf new file mode 100644 index 000000000..c78a6ecac --- /dev/null +++ b/terraform/test-fixtures/transform-provisioner-prune/main.tf @@ -0,0 +1,3 @@ +resource "aws_instance" "web" { + provisioner "foo" {} +} diff --git a/terraform/test-fixtures/transform-resource-count-basic/main.tf b/terraform/test-fixtures/transform-resource-count-basic/main.tf new file mode 100644 index 000000000..83bdd56e6 --- /dev/null +++ b/terraform/test-fixtures/transform-resource-count-basic/main.tf @@ -0,0 +1,4 @@ +resource "aws_instance" "foo" { + count = 3 + value = "${aws_instance.foo.0.value}" +} diff --git a/terraform/test-fixtures/transform-resource-count-negative/main.tf b/terraform/test-fixtures/transform-resource-count-negative/main.tf new file mode 100644 index 000000000..267e20086 --- /dev/null +++ b/terraform/test-fixtures/transform-resource-count-negative/main.tf @@ -0,0 +1,4 @@ +resource "aws_instance" "foo" { + count = -5 + value = "${aws_instance.foo.0.value}" +} diff --git a/terraform/test-fixtures/transform-root-basic/main.tf b/terraform/test-fixtures/transform-root-basic/main.tf new file mode 100644 index 000000000..e4ff4b3e9 --- /dev/null +++ b/terraform/test-fixtures/transform-root-basic/main.tf @@ -0,0 +1,5 @@ +provider "aws" {} +resource "aws_instance" "foo" {} + +provider "do" {} +resource "do_droplet" "bar" {} diff --git a/terraform/test-fixtures/transform-tainted-basic/main.tf b/terraform/test-fixtures/transform-tainted-basic/main.tf new file mode 100644 index 000000000..64cbf6236 --- /dev/null +++ b/terraform/test-fixtures/transform-tainted-basic/main.tf @@ -0,0 +1 @@ +resource "aws_instance" "web" {} diff --git a/terraform/test-fixtures/validate-bad-prov-conf/main.tf b/terraform/test-fixtures/validate-bad-prov-conf/main.tf index abca992de..347cfc02a 100644 --- a/terraform/test-fixtures/validate-bad-prov-conf/main.tf +++ b/terraform/test-fixtures/validate-bad-prov-conf/main.tf @@ -3,5 +3,7 @@ provider "aws" { } resource "aws_instance" "test" { - provisioner "shell" {} + provisioner "shell" { + command = "foo" + } } diff --git a/terraform/transform.go b/terraform/transform.go new file mode 100644 index 000000000..ca5736940 --- /dev/null +++ b/terraform/transform.go @@ -0,0 +1,21 @@ +package terraform + +import ( + "github.com/hashicorp/terraform/dag" +) + +// GraphTransformer is the interface that transformers implement. This +// interface is only for transforms that need entire graph visibility. +type GraphTransformer interface { + Transform(*Graph) error +} + +// GraphVertexTransformer is an interface that transforms a single +// Vertex within with graph. This is a specialization of GraphTransformer +// that makes it easy to do vertex replacement. +// +// The GraphTransformer that runs through the GraphVertexTransformers is +// VertexTransformer. +type GraphVertexTransformer interface { + Transform(dag.Vertex) (dag.Vertex, error) +} diff --git a/terraform/transform_config.go b/terraform/transform_config.go new file mode 100644 index 000000000..3b115688b --- /dev/null +++ b/terraform/transform_config.go @@ -0,0 +1,105 @@ +package terraform + +import ( + "errors" + "fmt" + + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/config/module" +) + +// ConfigTransformer is a GraphTransformer that adds the configuration +// to the graph. The module used to configure this transformer must be +// the root module. We'll look up the child module by the Path in the +// Graph. +type ConfigTransformer struct { + Module *module.Tree +} + +func (t *ConfigTransformer) Transform(g *Graph) error { + // A module is required and also must be completely loaded. + if t.Module == nil { + return errors.New("module must not be nil") + } + if !t.Module.Loaded() { + return errors.New("module must be loaded") + } + + // Get the module we care about + module := t.Module.Child(g.Path[1:]) + if module == nil { + return nil + } + + // Get the configuration for this module + config := module.Config() + + // Create the node list we'll use for the graph + nodes := make([]graphNodeConfig, 0, + (len(config.ProviderConfigs)+len(config.Modules)+len(config.Resources))*2) + + // Write all the provider configs out + for _, pc := range config.ProviderConfigs { + nodes = append(nodes, &GraphNodeConfigProvider{Provider: pc}) + } + + // Write all the resources out + for _, r := range config.Resources { + nodes = append(nodes, &GraphNodeConfigResource{Resource: r}) + } + + // Write all the modules out + children := module.Children() + for _, m := range config.Modules { + path := make([]string, len(g.Path), len(g.Path)+1) + copy(path, g.Path) + path = append(path, m.Name) + + nodes = append(nodes, &GraphNodeConfigModule{ + Path: path, + Module: m, + Tree: children[m.Name], + }) + } + + // Write all the outputs out + for _, o := range config.Outputs { + nodes = append(nodes, &GraphNodeConfigOutput{Output: o}) + } + + // Err is where the final error value will go if there is one + var err error + + // Build the graph vertices + for _, n := range nodes { + g.Add(n) + } + + // Build up the dependencies. We have to do this outside of the above + // loop since the nodes need to be in place for us to build the deps. + for _, n := range nodes { + if missing := g.ConnectDependent(n); len(missing) > 0 { + for _, m := range missing { + err = multierror.Append(err, fmt.Errorf( + "%s: missing dependency: %s", n.Name(), m)) + } + } + } + + return err +} + +// varNameForVar returns the VarName value for an interpolated variable. +// This value is compared to the VarName() value for the nodes within the +// graph to build the graph edges. +func varNameForVar(raw config.InterpolatedVariable) string { + switch v := raw.(type) { + case *config.ModuleVariable: + return fmt.Sprintf("module.%s", v.Name) + case *config.ResourceVariable: + return v.ResourceId() + default: + return "" + } +} diff --git a/terraform/transform_config_test.go b/terraform/transform_config_test.go new file mode 100644 index 000000000..371f01448 --- /dev/null +++ b/terraform/transform_config_test.go @@ -0,0 +1,128 @@ +package terraform + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/hashicorp/terraform/config/module" +) + +func TestConfigTransformer_nilModule(t *testing.T) { + g := Graph{Path: RootModulePath} + tf := &ConfigTransformer{} + if err := tf.Transform(&g); err == nil { + t.Fatal("should error") + } +} + +func TestConfigTransformer_unloadedModule(t *testing.T) { + mod, err := module.NewTreeModule( + "", filepath.Join(fixtureDir, "graph-basic")) + if err != nil { + t.Fatalf("err: %s", err) + } + + g := Graph{Path: RootModulePath} + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err == nil { + t.Fatal("should error") + } +} + +func TestConfigTransformer(t *testing.T) { + g := Graph{Path: RootModulePath} + tf := &ConfigTransformer{Module: testModule(t, "graph-basic")} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testGraphBasicStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestConfigTransformer_dependsOn(t *testing.T) { + g := Graph{Path: RootModulePath} + tf := &ConfigTransformer{Module: testModule(t, "graph-depends-on")} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testGraphDependsOnStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestConfigTransformer_modules(t *testing.T) { + g := Graph{Path: RootModulePath} + tf := &ConfigTransformer{Module: testModule(t, "graph-modules")} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testGraphModulesStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestConfigTransformer_outputs(t *testing.T) { + g := Graph{Path: RootModulePath} + tf := &ConfigTransformer{Module: testModule(t, "graph-outputs")} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testGraphOutputsStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestConfigTransformer_errMissingDeps(t *testing.T) { + g := Graph{Path: RootModulePath} + tf := &ConfigTransformer{Module: testModule(t, "graph-missing-deps")} + if err := tf.Transform(&g); err == nil { + t.Fatalf("err: %s", err) + } +} + +const testGraphBasicStr = ` +aws_instance.web + aws_security_group.firewall +aws_load_balancer.weblb + aws_instance.web +aws_security_group.firewall +openstack_floating_ip.random +provider.aws + openstack_floating_ip.random +` + +const testGraphDependsOnStr = ` +aws_instance.db + aws_instance.web +aws_instance.web +` + +const testGraphModulesStr = ` +aws_instance.web + aws_security_group.firewall + module.consul +aws_security_group.firewall +module.consul + aws_security_group.firewall +provider.aws +` + +const testGraphOutputsStr = ` +aws_instance.foo +output.foo + aws_instance.foo +` diff --git a/terraform/transform_destroy.go b/terraform/transform_destroy.go new file mode 100644 index 000000000..7d4293ef3 --- /dev/null +++ b/terraform/transform_destroy.go @@ -0,0 +1,230 @@ +package terraform + +import ( + "github.com/hashicorp/terraform/dag" +) + +type GraphNodeDestroyMode byte + +const ( + DestroyNone GraphNodeDestroyMode = 0 + DestroyPrimary GraphNodeDestroyMode = 1 << iota + DestroyTainted +) + +// GraphNodeDestroyable is the interface that nodes that can be destroyed +// must implement. This is used to automatically handle the creation of +// destroy nodes in the graph and the dependency ordering of those destroys. +type GraphNodeDestroyable interface { + // DestroyNode returns the node used for the destroy with the given + // mode. If this returns nil, then a destroy node for that mode + // will not be added. + DestroyNode(GraphNodeDestroyMode) GraphNodeDestroy +} + +// GraphNodeDestroy is the interface that must implemented by +// nodes that destroy. +type GraphNodeDestroy interface { + dag.Vertex + + // CreateBeforeDestroy is called to check whether this node + // should be created before it is destroyed. The CreateBeforeDestroy + // transformer uses this information to setup the graph. + CreateBeforeDestroy() bool + + // CreateNode returns the node used for the create side of this + // destroy. This must already exist within the graph. + CreateNode() dag.Vertex +} + +// GraphNodeDestroyPrunable is the interface that can be implemented to +// signal that this node can be pruned depending on state. +type GraphNodeDestroyPrunable interface { + // DestroyInclude is called to check if this node should be included + // with the given state. The state and diff must NOT be modified. + DestroyInclude(*ModuleDiff, *ModuleState) bool +} + +// DestroyTransformer is a GraphTransformer that creates the destruction +// nodes for things that _might_ be destroyed. +type DestroyTransformer struct{} + +func (t *DestroyTransformer) Transform(g *Graph) error { + var connect, remove []dag.Edge + + modes := []GraphNodeDestroyMode{DestroyPrimary, DestroyTainted} + for _, m := range modes { + connectMode, removeMode, err := t.transform(g, m) + if err != nil { + return err + } + + connect = append(connect, connectMode...) + remove = append(remove, removeMode...) + } + + // Atomatically add/remove the edges + for _, e := range connect { + g.Connect(e) + } + for _, e := range remove { + g.RemoveEdge(e) + } + + return nil +} + +func (t *DestroyTransformer) transform( + g *Graph, mode GraphNodeDestroyMode) ([]dag.Edge, []dag.Edge, error) { + var connect, remove []dag.Edge + nodeToCn := make(map[dag.Vertex]dag.Vertex, len(g.Vertices())) + nodeToDn := make(map[dag.Vertex]dag.Vertex, len(g.Vertices())) + for _, v := range g.Vertices() { + // If it is not a destroyable, we don't care + cn, ok := v.(GraphNodeDestroyable) + if !ok { + continue + } + + // Grab the destroy side of the node and connect it through + n := cn.DestroyNode(mode) + if n == nil { + continue + } + + // Store it + nodeToCn[n] = cn + nodeToDn[cn] = n + + // Add it to the graph + g.Add(n) + + // Inherit all the edges from the old node + downEdges := g.DownEdges(v).List() + for _, edgeRaw := range downEdges { + g.Connect(dag.BasicEdge(n, edgeRaw.(dag.Vertex))) + } + + // Add a new edge to connect the node to be created to + // the destroy node. + connect = append(connect, dag.BasicEdge(v, n)) + } + + // Go through the nodes we added and determine if they depend + // on any nodes with a destroy node. If so, depend on that instead. + for n, _ := range nodeToCn { + for _, downRaw := range g.DownEdges(n).List() { + target := downRaw.(dag.Vertex) + cn2, ok := target.(GraphNodeDestroyable) + if !ok { + continue + } + + newTarget := nodeToDn[cn2] + if newTarget == nil { + continue + } + + // Make the new edge and transpose + connect = append(connect, dag.BasicEdge(newTarget, n)) + + // Remove the old edge + remove = append(remove, dag.BasicEdge(n, target)) + } + } + + return connect, remove, nil +} + +// CreateBeforeDestroyTransformer is a GraphTransformer that modifies +// the destroys of some nodes so that the creation happens before the +// destroy. +type CreateBeforeDestroyTransformer struct{} + +func (t *CreateBeforeDestroyTransformer) Transform(g *Graph) error { + // We "stage" the edge connections/destroys in these slices so that + // while we're doing the edge transformations (transpositions) in + // the graph, we're not affecting future edge transpositions. These + // slices let us stage ALL the changes that WILL happen so that all + // of the transformations happen atomically. + var connect, destroy []dag.Edge + + for _, v := range g.Vertices() { + // We only care to use the destroy nodes + dn, ok := v.(GraphNodeDestroy) + if !ok { + continue + } + + // If the node doesn't need to create before destroy, then continue + if !dn.CreateBeforeDestroy() { + continue + } + + // Get the creation side of this node + cn := dn.CreateNode() + + // Take all the things which depend on the creation node and + // make them dependencies on the destruction. Clarifying this + // with an example: if you have a web server and a load balancer + // and the load balancer depends on the web server, then when we + // do a create before destroy, we want to make sure the steps are: + // + // 1.) Create new web server + // 2.) Update load balancer + // 3.) Delete old web server + // + // This ensures that. + for _, sourceRaw := range g.UpEdges(cn).List() { + source := sourceRaw.(dag.Vertex) + connect = append(connect, dag.BasicEdge(dn, source)) + } + + // Swap the edge so that the destroy depends on the creation + // happening... + connect = append(connect, dag.BasicEdge(dn, cn)) + destroy = append(destroy, dag.BasicEdge(cn, dn)) + } + + for _, edge := range connect { + g.Connect(edge) + } + for _, edge := range destroy { + g.RemoveEdge(edge) + } + + return nil +} + +// PruneDestroyTransformer is a GraphTransformer that removes the destroy +// nodes that aren't in the diff. +type PruneDestroyTransformer struct { + Diff *Diff + State *State +} + +func (t *PruneDestroyTransformer) Transform(g *Graph) error { + var modDiff *ModuleDiff + var modState *ModuleState + if t.Diff != nil { + modDiff = t.Diff.ModuleByPath(g.Path) + } + if t.State != nil { + modState = t.State.ModuleByPath(g.Path) + } + + for _, v := range g.Vertices() { + // If it is not a destroyer, we don't care + dn, ok := v.(GraphNodeDestroyPrunable) + if !ok { + continue + } + + // Remove it if we should + if !dn.DestroyInclude(modDiff, modState) { + g.Remove(v) + } + } + + return nil +} diff --git a/terraform/transform_destroy_test.go b/terraform/transform_destroy_test.go new file mode 100644 index 000000000..784ff0669 --- /dev/null +++ b/terraform/transform_destroy_test.go @@ -0,0 +1,411 @@ +package terraform + +import ( + "strings" + "testing" +) + +func TestDestroyTransformer(t *testing.T) { + mod := testModule(t, "transform-destroy-basic") + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + tf := &DestroyTransformer{} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformDestroyBasicStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestCreateBeforeDestroyTransformer(t *testing.T) { + mod := testModule(t, "transform-create-before-destroy-basic") + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + tf := &DestroyTransformer{} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + tf := &CreateBeforeDestroyTransformer{} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformCreateBeforeDestroyBasicStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestCreateBeforeDestroyTransformer_twice(t *testing.T) { + mod := testModule(t, "transform-create-before-destroy-twice") + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + tf := &DestroyTransformer{} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + tf := &CreateBeforeDestroyTransformer{} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformCreateBeforeDestroyTwiceStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestPruneDestroyTransformer(t *testing.T) { + var diff *Diff + mod := testModule(t, "transform-destroy-basic") + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + tf := &DestroyTransformer{} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + tf := &PruneDestroyTransformer{Diff: diff} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformPruneDestroyBasicStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestPruneDestroyTransformer_diff(t *testing.T) { + mod := testModule(t, "transform-destroy-basic") + + diff := &Diff{ + Modules: []*ModuleDiff{ + &ModuleDiff{ + Path: RootModulePath, + Resources: map[string]*InstanceDiff{ + "aws_instance.bar": &InstanceDiff{}, + }, + }, + }, + } + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + tf := &DestroyTransformer{} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + tf := &PruneDestroyTransformer{Diff: diff} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformPruneDestroyBasicDiffStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestPruneDestroyTransformer_count(t *testing.T) { + mod := testModule(t, "transform-destroy-prune-count") + + diff := &Diff{} + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + tf := &DestroyTransformer{} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + tf := &PruneDestroyTransformer{Diff: diff} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformPruneDestroyCountStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestPruneDestroyTransformer_countDec(t *testing.T) { + mod := testModule(t, "transform-destroy-basic") + + diff := &Diff{} + state := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: RootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.bar.1": &ResourceState{ + Primary: &InstanceState{}, + }, + "aws_instance.bar.2": &ResourceState{ + Primary: &InstanceState{}, + }, + }, + }, + }, + } + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + tf := &DestroyTransformer{} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + tf := &PruneDestroyTransformer{Diff: diff, State: state} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformPruneDestroyCountDecStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestPruneDestroyTransformer_countState(t *testing.T) { + mod := testModule(t, "transform-destroy-basic") + + diff := &Diff{} + state := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: RootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.bar": &ResourceState{ + Primary: &InstanceState{}, + }, + }, + }, + }, + } + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + tf := &DestroyTransformer{} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + tf := &PruneDestroyTransformer{Diff: diff, State: state} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformPruneDestroyCountStateStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +const testTransformDestroyBasicStr = ` +aws_instance.bar + aws_instance.bar (destroy tainted) + aws_instance.bar (destroy) + aws_instance.foo +aws_instance.bar (destroy tainted) +aws_instance.bar (destroy) +aws_instance.foo + aws_instance.foo (destroy tainted) + aws_instance.foo (destroy) +aws_instance.foo (destroy tainted) + aws_instance.bar (destroy tainted) +aws_instance.foo (destroy) + aws_instance.bar (destroy) +` + +const testTransformPruneDestroyBasicStr = ` +aws_instance.bar + aws_instance.bar (destroy tainted) + aws_instance.foo +aws_instance.bar (destroy tainted) +aws_instance.foo + aws_instance.foo (destroy tainted) +aws_instance.foo (destroy tainted) + aws_instance.bar (destroy tainted) +` + +const testTransformPruneDestroyBasicDiffStr = ` +aws_instance.bar + aws_instance.bar (destroy tainted) + aws_instance.bar (destroy) + aws_instance.foo +aws_instance.bar (destroy tainted) +aws_instance.bar (destroy) +aws_instance.foo + aws_instance.foo (destroy tainted) +aws_instance.foo (destroy tainted) + aws_instance.bar (destroy tainted) +` + +const testTransformPruneDestroyCountStr = ` +aws_instance.bar + aws_instance.bar (destroy tainted) + aws_instance.bar (destroy) + aws_instance.foo +aws_instance.bar (destroy tainted) +aws_instance.bar (destroy) +aws_instance.foo + aws_instance.foo (destroy tainted) +aws_instance.foo (destroy tainted) + aws_instance.bar (destroy tainted) +` + +const testTransformPruneDestroyCountDecStr = ` +aws_instance.bar + aws_instance.bar (destroy tainted) + aws_instance.bar (destroy) + aws_instance.foo +aws_instance.bar (destroy tainted) +aws_instance.bar (destroy) +aws_instance.foo + aws_instance.foo (destroy tainted) +aws_instance.foo (destroy tainted) + aws_instance.bar (destroy tainted) +` + +const testTransformPruneDestroyCountStateStr = ` +aws_instance.bar + aws_instance.bar (destroy tainted) + aws_instance.foo +aws_instance.bar (destroy tainted) +aws_instance.foo + aws_instance.foo (destroy tainted) +aws_instance.foo (destroy tainted) + aws_instance.bar (destroy tainted) +` + +const testTransformCreateBeforeDestroyBasicStr = ` +aws_instance.web + aws_instance.web (destroy tainted) +aws_instance.web (destroy tainted) + aws_load_balancer.lb (destroy tainted) +aws_instance.web (destroy) + aws_instance.web + aws_load_balancer.lb + aws_load_balancer.lb (destroy) +aws_load_balancer.lb + aws_instance.web + aws_load_balancer.lb (destroy tainted) + aws_load_balancer.lb (destroy) +aws_load_balancer.lb (destroy tainted) +aws_load_balancer.lb (destroy) +` + +const testTransformCreateBeforeDestroyTwiceStr = ` +aws_autoscale.bar + aws_autoscale.bar (destroy tainted) + aws_lc.foo +aws_autoscale.bar (destroy tainted) +aws_autoscale.bar (destroy) + aws_autoscale.bar +aws_lc.foo + aws_lc.foo (destroy tainted) +aws_lc.foo (destroy tainted) + aws_autoscale.bar (destroy tainted) +aws_lc.foo (destroy) + aws_autoscale.bar + aws_autoscale.bar (destroy) + aws_lc.foo +` diff --git a/terraform/transform_expand.go b/terraform/transform_expand.go new file mode 100644 index 000000000..85d660206 --- /dev/null +++ b/terraform/transform_expand.go @@ -0,0 +1,61 @@ +package terraform + +import ( + "log" + + "github.com/hashicorp/terraform/dag" +) + +// GraphNodeExapndable is an interface that nodes can implement to +// signal that they can be expanded. Expanded nodes turn into +// GraphNodeSubgraph nodes within the graph. +type GraphNodeExpandable interface { + Expand(GraphBuilder) (GraphNodeSubgraph, error) +} + +// GraphNodeDynamicExpandable is an interface that nodes can implement +// to signal that they can be expanded at eval-time (hence dynamic). +// These nodes are given the eval context and are expected to return +// a new subgraph. +type GraphNodeDynamicExpandable interface { + DynamicExpand(EvalContext) (*Graph, error) +} + +// GraphNodeSubgraph is an interface a node can implement if it has +// a larger subgraph that should be walked. +type GraphNodeSubgraph interface { + Subgraph() *Graph +} + +// ExpandTransform is a transformer that does a subgraph expansion +// at graph transform time (vs. at eval time). The benefit of earlier +// subgraph expansion is that errors with the graph build can be detected +// at an earlier stage. +type ExpandTransform struct { + Builder GraphBuilder +} + +func (t *ExpandTransform) Transform(v dag.Vertex) (dag.Vertex, error) { + ev, ok := v.(GraphNodeExpandable) + if !ok { + // This isn't an expandable vertex, so just ignore it. + return v, nil + } + + // Expand the subgraph! + log.Printf("[DEBUG] vertex %s: static expanding", dag.VertexName(ev)) + return ev.Expand(t.Builder) +} + +type GraphNodeBasicSubgraph struct { + NameValue string + Graph *Graph +} + +func (n *GraphNodeBasicSubgraph) Name() string { + return n.NameValue +} + +func (n *GraphNodeBasicSubgraph) Subgraph() *Graph { + return n.Graph +} diff --git a/terraform/transform_expand_test.go b/terraform/transform_expand_test.go new file mode 100644 index 000000000..e0ee9cca2 --- /dev/null +++ b/terraform/transform_expand_test.go @@ -0,0 +1,77 @@ +package terraform + +import ( + "strings" + "testing" + + "github.com/hashicorp/terraform/dag" +) + +func TestExpandTransform_impl(t *testing.T) { + var _ GraphVertexTransformer = new(ExpandTransform) +} + +func TestExpandTransform(t *testing.T) { + var g Graph + g.Add(1) + g.Add(2) + g.Connect(dag.BasicEdge(1, 2)) + + tf := &ExpandTransform{} + out, err := tf.Transform(&testExpandable{ + Result: &g, + }) + if err != nil { + t.Fatalf("err: %s", err) + } + + sn, ok := out.(GraphNodeSubgraph) + if !ok { + t.Fatalf("not subgraph: %#v", out) + } + + actual := strings.TrimSpace(sn.Subgraph().String()) + expected := strings.TrimSpace(testExpandTransformStr) + if actual != expected { + t.Fatalf("bad: %s", actual) + } +} + +func TestExpandTransform_nonExpandable(t *testing.T) { + tf := &ExpandTransform{} + out, err := tf.Transform(42) + if err != nil { + t.Fatalf("err: %s", err) + } + if out != 42 { + t.Fatalf("bad: %#v", out) + } +} + +type testExpandable struct { + // Inputs + Result *Graph + ResultError error + + // Outputs + Builder GraphBuilder +} + +func (n *testExpandable) Expand(b GraphBuilder) (GraphNodeSubgraph, error) { + n.Builder = b + return &testSubgraph{n.Result}, n.ResultError +} + +type testSubgraph struct { + Graph *Graph +} + +func (n *testSubgraph) Subgraph() *Graph { + return n.Graph +} + +const testExpandTransformStr = ` +1 + 2 +2 +` diff --git a/terraform/transform_module.go b/terraform/transform_module.go new file mode 100644 index 000000000..816ccc455 --- /dev/null +++ b/terraform/transform_module.go @@ -0,0 +1,47 @@ +package terraform + +import ( + "github.com/hashicorp/terraform/dag" +) + +// ModuleInputTransformer is a GraphTransformer that adds a node to the +// graph for setting the module input variables for the remainder of the +// graph. +type ModuleInputTransformer struct { + Variables map[string]string +} + +func (t *ModuleInputTransformer) Transform(g *Graph) error { + // Create the node + n := &graphNodeModuleInput{Variables: t.Variables} + + // Add it to the graph + g.Add(n) + + // Connect the inputs to the bottom of the graph so that it happens + // first. + for _, v := range g.Vertices() { + if v == n { + continue + } + + if g.DownEdges(v).Len() == 0 { + g.Connect(dag.BasicEdge(v, n)) + } + } + + return nil +} + +type graphNodeModuleInput struct { + Variables map[string]string +} + +func (n *graphNodeModuleInput) Name() string { + return "module inputs" +} + +// GraphNodeEvalable impl. +func (n *graphNodeModuleInput) EvalTree() EvalNode { + return &EvalSetVariables{Variables: n.Variables} +} diff --git a/terraform/transform_module_test.go b/terraform/transform_module_test.go new file mode 100644 index 000000000..b857108b2 --- /dev/null +++ b/terraform/transform_module_test.go @@ -0,0 +1,41 @@ +package terraform + +import ( + "strings" + "testing" + + "github.com/hashicorp/terraform/dag" +) + +func TestModuleInputTransformer(t *testing.T) { + var g Graph + g.Add(1) + g.Add(2) + g.Add(3) + g.Connect(dag.BasicEdge(1, 2)) + g.Connect(dag.BasicEdge(1, 3)) + + { + tf := &ModuleInputTransformer{} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testModuleInputTransformStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +const testModuleInputTransformStr = ` +1 + 2 + 3 +2 + module inputs +3 + module inputs +module inputs +` diff --git a/terraform/transform_orphan.go b/terraform/transform_orphan.go new file mode 100644 index 000000000..780d8430b --- /dev/null +++ b/terraform/transform_orphan.go @@ -0,0 +1,274 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/dag" +) + +// GraphNodeStateRepresentative is an interface that can be implemented by +// a node to say that it is representing a resource in the state. +type GraphNodeStateRepresentative interface { + StateId() []string +} + +// OrphanTransformer is a GraphTransformer that adds orphans to the +// graph. This transformer adds both resource and module orphans. +type OrphanTransformer struct { + // State is the global state. We require the global state to + // properly find module orphans at our path. + State *State + + // Module is the root module. We'll look up the proper configuration + // using the graph path. + Module *module.Tree + + // View, if non-nil will set a view on the module state. + View string +} + +func (t *OrphanTransformer) Transform(g *Graph) error { + if t.State == nil { + // If the entire state is nil, there can't be any orphans + return nil + } + + // Build up all our state representatives + resourceRep := make(map[string]struct{}) + for _, v := range g.Vertices() { + if sr, ok := v.(GraphNodeStateRepresentative); ok { + for _, k := range sr.StateId() { + resourceRep[k] = struct{}{} + } + } + } + + var config *config.Config + if t.Module != nil { + if module := t.Module.Child(g.Path[1:]); module != nil { + config = module.Config() + } + } + + var resourceVertexes []dag.Vertex + if state := t.State.ModuleByPath(g.Path); state != nil { + // If we have state, then we can have orphan resources + + // If we have a view, get the view + if t.View != "" { + state = state.View(t.View) + } + + // Go over each resource orphan and add it to the graph. + resourceOrphans := state.Orphans(config) + resourceVertexes = make([]dag.Vertex, len(resourceOrphans)) + for i, k := range resourceOrphans { + // If this orphan is represented by some other node somehow, + // then ignore it. + if _, ok := resourceRep[k]; ok { + continue + } + + rs := state.Resources[k] + + resourceVertexes[i] = g.Add(&graphNodeOrphanResource{ + ResourceName: k, + ResourceType: rs.Type, + dependentOn: rs.Dependencies, + }) + } + } + + // Go over each module orphan and add it to the graph. We store the + // vertexes and states outside so that we can connect dependencies later. + moduleOrphans := t.State.ModuleOrphans(g.Path, config) + moduleVertexes := make([]dag.Vertex, len(moduleOrphans)) + for i, path := range moduleOrphans { + moduleVertexes[i] = g.Add(&graphNodeOrphanModule{ + Path: path, + dependentOn: t.State.ModuleByPath(path).Dependencies, + }) + } + + // Now do the dependencies. We do this _after_ adding all the orphan + // nodes above because there are cases in which the orphans themselves + // depend on other orphans. + + // Resource dependencies + for _, v := range resourceVertexes { + g.ConnectDependent(v) + } + + // Module dependencies + for _, v := range moduleVertexes { + g.ConnectDependent(v) + } + + return nil +} + +// graphNodeOrphanModule is the graph vertex representing an orphan resource.. +type graphNodeOrphanModule struct { + Path []string + + dependentOn []string +} + +func (n *graphNodeOrphanModule) DependableName() []string { + return []string{n.dependableName()} +} + +func (n *graphNodeOrphanModule) DependentOn() []string { + return n.dependentOn +} + +func (n *graphNodeOrphanModule) Name() string { + return fmt.Sprintf("%s (orphan)", n.dependableName()) +} + +func (n *graphNodeOrphanModule) dependableName() string { + return fmt.Sprintf("module.%s", n.Path[len(n.Path)-1]) +} + +// GraphNodeExpandable +func (n *graphNodeOrphanModule) Expand(b GraphBuilder) (GraphNodeSubgraph, error) { + g, err := b.Build(n.Path) + if err != nil { + return nil, err + } + + return &GraphNodeBasicSubgraph{ + NameValue: n.Name(), + Graph: g, + }, nil +} + +// graphNodeOrphanResource is the graph vertex representing an orphan resource.. +type graphNodeOrphanResource struct { + ResourceName string + ResourceType string + + dependentOn []string +} + +func (n *graphNodeOrphanResource) DependableName() []string { + return []string{n.dependableName()} +} + +func (n *graphNodeOrphanResource) DependentOn() []string { + return n.dependentOn +} + +func (n *graphNodeOrphanResource) Name() string { + return fmt.Sprintf("%s (orphan)", n.ResourceName) +} + +func (n *graphNodeOrphanResource) ProvidedBy() []string { + return []string{resourceProvider(n.ResourceName)} +} + +// GraphNodeEvalable impl. +func (n *graphNodeOrphanResource) EvalTree() EvalNode { + var provider ResourceProvider + var state *InstanceState + + seq := &EvalSequence{Nodes: make([]EvalNode, 0, 5)} + + // Build instance info + info := &InstanceInfo{Id: n.ResourceName, Type: n.ResourceType} + seq.Nodes = append(seq.Nodes, &EvalInstanceInfo{Info: info}) + + // Refresh the resource + seq.Nodes = append(seq.Nodes, &EvalOpFilter{ + Ops: []walkOperation{walkRefresh}, + Node: &EvalSequence{ + Nodes: []EvalNode{ + &EvalGetProvider{ + Name: n.ProvidedBy()[0], + Output: &provider, + }, + &EvalReadState{ + Name: n.ResourceName, + Output: &state, + }, + &EvalRefresh{ + Info: info, + Provider: &provider, + State: &state, + Output: &state, + }, + &EvalWriteState{ + Name: n.ResourceName, + ResourceType: n.ResourceType, + Dependencies: n.DependentOn(), + State: &state, + }, + }, + }, + }) + + // Diff the resource + var diff *InstanceDiff + seq.Nodes = append(seq.Nodes, &EvalOpFilter{ + Ops: []walkOperation{walkPlan, walkPlanDestroy}, + Node: &EvalSequence{ + Nodes: []EvalNode{ + &EvalReadState{ + Name: n.ResourceName, + Output: &state, + }, + &EvalDiffDestroy{ + Info: info, + State: &state, + Output: &diff, + }, + &EvalWriteDiff{ + Name: n.ResourceName, + Diff: &diff, + }, + }, + }, + }) + + // Apply + seq.Nodes = append(seq.Nodes, &EvalOpFilter{ + Ops: []walkOperation{walkApply}, + Node: &EvalSequence{ + Nodes: []EvalNode{ + &EvalReadDiff{ + Name: n.ResourceName, + Diff: &diff, + }, + &EvalGetProvider{ + Name: n.ProvidedBy()[0], + Output: &provider, + }, + &EvalReadState{ + Name: n.ResourceName, + Output: &state, + }, + &EvalApply{ + Info: info, + State: &state, + Diff: &diff, + Provider: &provider, + Output: &state, + }, + &EvalWriteState{ + Name: n.ResourceName, + ResourceType: n.ResourceType, + Dependencies: n.DependentOn(), + State: &state, + }, + }, + }, + }) + + return seq +} + +func (n *graphNodeOrphanResource) dependableName() string { + return n.ResourceName +} diff --git a/terraform/transform_orphan_test.go b/terraform/transform_orphan_test.go new file mode 100644 index 000000000..17708c683 --- /dev/null +++ b/terraform/transform_orphan_test.go @@ -0,0 +1,381 @@ +package terraform + +import ( + "strings" + "testing" + + "github.com/hashicorp/terraform/dag" +) + +func TestOrphanTransformer(t *testing.T) { + mod := testModule(t, "transform-orphan-basic") + state := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: RootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.web": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + + // The orphan + "aws_instance.db": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + } + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + transform := &OrphanTransformer{State: state, Module: mod} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformOrphanBasicStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestOrphanTransformer_modules(t *testing.T) { + mod := testModule(t, "transform-orphan-modules") + state := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: RootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.foo": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + + // Orphan module + &ModuleState{ + Path: []string{RootModuleName, "foo"}, + Resources: map[string]*ResourceState{ + "aws_instance.web": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + } + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + transform := &OrphanTransformer{State: state, Module: mod} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformOrphanModulesStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestOrphanTransformer_modulesDeps(t *testing.T) { + mod := testModule(t, "transform-orphan-modules") + state := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: RootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.foo": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + + // Orphan module + &ModuleState{ + Path: []string{RootModuleName, "foo"}, + Resources: map[string]*ResourceState{ + "aws_instance.web": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + Dependencies: []string{ + "aws_instance.foo", + }, + }, + }, + } + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + transform := &OrphanTransformer{State: state, Module: mod} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformOrphanModulesDepsStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestOrphanTransformer_modulesDepsOrphan(t *testing.T) { + mod := testModule(t, "transform-orphan-modules") + state := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: RootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.web": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + + // Orphan module + &ModuleState{ + Path: []string{RootModuleName, "foo"}, + Resources: map[string]*ResourceState{ + "aws_instance.web": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + Dependencies: []string{ + "aws_instance.web", + }, + }, + }, + } + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + transform := &OrphanTransformer{State: state, Module: mod} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformOrphanModulesDepsOrphanStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestOrphanTransformer_modulesNoRoot(t *testing.T) { + mod := testModule(t, "transform-orphan-modules") + state := &State{ + Modules: []*ModuleState{ + // Orphan module + &ModuleState{ + Path: []string{RootModuleName, "foo"}, + Resources: map[string]*ResourceState{ + "aws_instance.web": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + } + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + transform := &OrphanTransformer{State: state, Module: mod} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformOrphanModulesNoRootStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestOrphanTransformer_resourceDepends(t *testing.T) { + mod := testModule(t, "transform-orphan-basic") + state := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: RootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.web": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + + // The orphan + "aws_instance.db": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo", + }, + Dependencies: []string{ + "aws_instance.web", + }, + }, + }, + }, + }, + } + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + transform := &OrphanTransformer{State: state, Module: mod} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformOrphanResourceDependsStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestOrphanTransformer_nilState(t *testing.T) { + mod := testModule(t, "transform-orphan-basic") + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + transform := &OrphanTransformer{State: nil, Module: mod} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformOrphanNilStateStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestGraphNodeOrphanModule_impl(t *testing.T) { + var _ dag.Vertex = new(graphNodeOrphanModule) + var _ dag.NamedVertex = new(graphNodeOrphanModule) + var _ GraphNodeExpandable = new(graphNodeOrphanModule) +} + +func TestGraphNodeOrphanResource_impl(t *testing.T) { + var _ dag.Vertex = new(graphNodeOrphanResource) + var _ dag.NamedVertex = new(graphNodeOrphanResource) + var _ GraphNodeProviderConsumer = new(graphNodeOrphanResource) +} + +func TestGraphNodeOrphanResource_ProvidedBy(t *testing.T) { + n := &graphNodeOrphanResource{ResourceName: "aws_instance.foo"} + if v := n.ProvidedBy(); v[0] != "aws" { + t.Fatalf("bad: %#v", v) + } +} + +const testTransformOrphanBasicStr = ` +aws_instance.db (orphan) +aws_instance.web +` + +const testTransformOrphanModulesStr = ` +aws_instance.foo +module.foo (orphan) +` + +const testTransformOrphanModulesDepsStr = ` +aws_instance.foo +module.foo (orphan) + aws_instance.foo +` + +const testTransformOrphanModulesDepsOrphanStr = ` +aws_instance.foo +aws_instance.web (orphan) +module.foo (orphan) + aws_instance.web (orphan) +` + +const testTransformOrphanNilStateStr = ` +aws_instance.web +` + +const testTransformOrphanResourceDependsStr = ` +aws_instance.db (orphan) + aws_instance.web +aws_instance.web +` + +const testTransformOrphanModulesNoRootStr = ` +aws_instance.foo +module.foo (orphan) +` diff --git a/terraform/transform_provider.go b/terraform/transform_provider.go new file mode 100644 index 000000000..f6c566b26 --- /dev/null +++ b/terraform/transform_provider.go @@ -0,0 +1,134 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform/dag" +) + +// GraphNodeProvider is an interface that nodes that can be a provider +// must implement. The ProviderName returned is the name of the provider +// they satisfy. +type GraphNodeProvider interface { + ProviderName() string +} + +// GraphNodeProviderConsumer is an interface that nodes that require +// a provider must implement. ProvidedBy must return the name of the provider +// to use. +type GraphNodeProviderConsumer interface { + ProvidedBy() []string +} + +// ProviderTransformer is a GraphTransformer that maps resources to +// providers within the graph. This will error if there are any resources +// that don't map to proper resources. +type ProviderTransformer struct{} + +func (t *ProviderTransformer) Transform(g *Graph) error { + // Go through the other nodes and match them to providers they need + var err error + m := providerVertexMap(g) + for _, v := range g.Vertices() { + if pv, ok := v.(GraphNodeProviderConsumer); ok { + for _, p := range pv.ProvidedBy() { + target := m[p] + if target == nil { + err = multierror.Append(err, fmt.Errorf( + "%s: provider %s couldn't be found", + dag.VertexName(v), p)) + continue + } + + g.Connect(dag.BasicEdge(v, target)) + } + } + } + + return err +} + +// MissingProviderTransformer is a GraphTransformer that adds nodes +// for missing providers into the graph. Specifically, it creates provider +// configuration nodes for all the providers that we support. These are +// pruned later during an optimization pass. +type MissingProviderTransformer struct { + // Providers is the list of providers we support. + Providers []string +} + +func (t *MissingProviderTransformer) Transform(g *Graph) error { + m := providerVertexMap(g) + for _, p := range t.Providers { + if _, ok := m[p]; ok { + // This provider already exists as a configured node + continue + } + + // Add our own missing provider node to the graph + g.Add(&graphNodeMissingProvider{ProviderNameValue: p}) + } + + return nil +} + +// PruneProviderTransformer is a GraphTransformer that prunes all the +// providers that aren't needed from the graph. A provider is unneeded if +// no resource or module is using that provider. +type PruneProviderTransformer struct{} + +func (t *PruneProviderTransformer) Transform(g *Graph) error { + for _, v := range g.Vertices() { + // We only care about the providers + if _, ok := v.(GraphNodeProvider); !ok { + continue + } + + // Does anything depend on this? If not, then prune it. + if s := g.UpEdges(v); s.Len() == 0 { + g.Remove(v) + } + } + + return nil +} + +type graphNodeMissingProvider struct { + ProviderNameValue string +} + +func (n *graphNodeMissingProvider) Name() string { + return fmt.Sprintf("provider.%s", n.ProviderNameValue) +} + +// GraphNodeEvalable impl. +func (n *graphNodeMissingProvider) EvalTree() EvalNode { + return ProviderEvalTree(n.ProviderNameValue, nil) +} + +func (n *graphNodeMissingProvider) ProviderName() string { + return n.ProviderNameValue +} + +// GraphNodeDotter impl. +func (n *graphNodeMissingProvider) Dot(name string) string { + return fmt.Sprintf( + "\"%s\" [\n"+ + "\tlabel=\"%s\"\n"+ + "\tshape=diamond\n"+ + "];", + name, + n.Name()) +} + +func providerVertexMap(g *Graph) map[string]dag.Vertex { + m := make(map[string]dag.Vertex) + for _, v := range g.Vertices() { + if pv, ok := v.(GraphNodeProvider); ok { + m[pv.ProviderName()] = v + } + } + + return m +} diff --git a/terraform/transform_provider_test.go b/terraform/transform_provider_test.go new file mode 100644 index 000000000..fdf25b7a4 --- /dev/null +++ b/terraform/transform_provider_test.go @@ -0,0 +1,124 @@ +package terraform + +import ( + "strings" + "testing" + + "github.com/hashicorp/terraform/dag" +) + +func TestProviderTransformer(t *testing.T) { + mod := testModule(t, "transform-provider-basic") + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + transform := &ProviderTransformer{} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformProviderBasicStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestMissingProviderTransformer(t *testing.T) { + mod := testModule(t, "transform-provider-basic") + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + transform := &MissingProviderTransformer{Providers: []string{"foo"}} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformMissingProviderBasicStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestPruneProviderTransformer(t *testing.T) { + mod := testModule(t, "transform-provider-prune") + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + transform := &MissingProviderTransformer{Providers: []string{"foo"}} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + transform := &ProviderTransformer{} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + transform := &PruneProviderTransformer{} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformPruneProviderBasicStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestGraphNodeMissingProvider_impl(t *testing.T) { + var _ dag.Vertex = new(graphNodeMissingProvider) + var _ dag.NamedVertex = new(graphNodeMissingProvider) + var _ GraphNodeProvider = new(graphNodeMissingProvider) +} + +func TestGraphNodeMissingProvider_ProviderName(t *testing.T) { + n := &graphNodeMissingProvider{ProviderNameValue: "foo"} + if v := n.ProviderName(); v != "foo" { + t.Fatalf("bad: %#v", v) + } +} + +const testTransformProviderBasicStr = ` +aws_instance.web + provider.aws +provider.aws +` + +const testTransformMissingProviderBasicStr = ` +aws_instance.web +provider.aws +provider.foo +` + +const testTransformPruneProviderBasicStr = ` +foo_instance.web + provider.foo +provider.foo +` diff --git a/terraform/transform_provisioner.go b/terraform/transform_provisioner.go new file mode 100644 index 000000000..316a8f31e --- /dev/null +++ b/terraform/transform_provisioner.go @@ -0,0 +1,123 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform/dag" +) + +// GraphNodeProvisioner is an interface that nodes that can be a provisioner +// must implement. The ProvisionerName returned is the name of the provisioner +// they satisfy. +type GraphNodeProvisioner interface { + ProvisionerName() string +} + +// GraphNodeProvisionerConsumer is an interface that nodes that require +// a provisioner must implement. ProvisionedBy must return the name of the +// provisioner to use. +type GraphNodeProvisionerConsumer interface { + ProvisionedBy() []string +} + +// ProvisionerTransformer is a GraphTransformer that maps resources to +// provisioners within the graph. This will error if there are any resources +// that don't map to proper resources. +type ProvisionerTransformer struct{} + +func (t *ProvisionerTransformer) Transform(g *Graph) error { + // Go through the other nodes and match them to provisioners they need + var err error + m := provisionerVertexMap(g) + for _, v := range g.Vertices() { + if pv, ok := v.(GraphNodeProvisionerConsumer); ok { + for _, provisionerName := range pv.ProvisionedBy() { + target := m[provisionerName] + if target == nil { + err = multierror.Append(err, fmt.Errorf( + "%s: provisioner %s couldn't be found", + dag.VertexName(v), provisionerName)) + continue + } + + g.Connect(dag.BasicEdge(v, target)) + } + } + } + + return err +} + +// MissingProvisionerTransformer is a GraphTransformer that adds nodes +// for missing provisioners into the graph. Specifically, it creates provisioner +// configuration nodes for all the provisioners that we support. These are +// pruned later during an optimization pass. +type MissingProvisionerTransformer struct { + // Provisioners is the list of provisioners we support. + Provisioners []string +} + +func (t *MissingProvisionerTransformer) Transform(g *Graph) error { + m := provisionerVertexMap(g) + for _, p := range t.Provisioners { + if _, ok := m[p]; ok { + // This provisioner already exists as a configured node + continue + } + + // Add our own missing provisioner node to the graph + g.Add(&graphNodeMissingProvisioner{ProvisionerNameValue: p}) + } + + return nil +} + +// PruneProvisionerTransformer is a GraphTransformer that prunes all the +// provisioners that aren't needed from the graph. A provisioner is unneeded if +// no resource or module is using that provisioner. +type PruneProvisionerTransformer struct{} + +func (t *PruneProvisionerTransformer) Transform(g *Graph) error { + for _, v := range g.Vertices() { + // We only care about the provisioners + if _, ok := v.(GraphNodeProvisioner); !ok { + continue + } + + // Does anything depend on this? If not, then prune it. + if s := g.UpEdges(v); s.Len() == 0 { + g.Remove(v) + } + } + + return nil +} + +type graphNodeMissingProvisioner struct { + ProvisionerNameValue string +} + +func (n *graphNodeMissingProvisioner) Name() string { + return fmt.Sprintf("provisioner.%s", n.ProvisionerNameValue) +} + +// GraphNodeEvalable impl. +func (n *graphNodeMissingProvisioner) EvalTree() EvalNode { + return &EvalInitProvisioner{Name: n.ProvisionerNameValue} +} + +func (n *graphNodeMissingProvisioner) ProvisionerName() string { + return n.ProvisionerNameValue +} + +func provisionerVertexMap(g *Graph) map[string]dag.Vertex { + m := make(map[string]dag.Vertex) + for _, v := range g.Vertices() { + if pv, ok := v.(GraphNodeProvisioner); ok { + m[pv.ProvisionerName()] = v + } + } + + return m +} diff --git a/terraform/transform_provisioner_test.go b/terraform/transform_provisioner_test.go new file mode 100644 index 000000000..f38a0a045 --- /dev/null +++ b/terraform/transform_provisioner_test.go @@ -0,0 +1,95 @@ +package terraform + +import ( + "strings" + "testing" + + "github.com/hashicorp/terraform/dag" +) + +func TestMissingProvisionerTransformer(t *testing.T) { + mod := testModule(t, "transform-provisioner-basic") + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + transform := &MissingProvisionerTransformer{Provisioners: []string{"foo"}} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformMissingProvisionerBasicStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestPruneProvisionerTransformer(t *testing.T) { + mod := testModule(t, "transform-provisioner-prune") + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + transform := &MissingProvisionerTransformer{ + Provisioners: []string{"foo", "bar"}} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + transform := &ProvisionerTransformer{} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + transform := &PruneProvisionerTransformer{} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformPruneProvisionerBasicStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestGraphNodeMissingProvisioner_impl(t *testing.T) { + var _ dag.Vertex = new(graphNodeMissingProvisioner) + var _ dag.NamedVertex = new(graphNodeMissingProvisioner) + var _ GraphNodeProvisioner = new(graphNodeMissingProvisioner) +} + +func TestGraphNodeMissingProvisioner_ProvisionerName(t *testing.T) { + n := &graphNodeMissingProvisioner{ProvisionerNameValue: "foo"} + if v := n.ProvisionerName(); v != "foo" { + t.Fatalf("bad: %#v", v) + } +} + +const testTransformMissingProvisionerBasicStr = ` +aws_instance.web +provisioner.foo +` + +const testTransformPruneProvisionerBasicStr = ` +aws_instance.web + provisioner.foo +provisioner.foo +` diff --git a/terraform/transform_resource.go b/terraform/transform_resource.go new file mode 100644 index 000000000..8c9c16a15 --- /dev/null +++ b/terraform/transform_resource.go @@ -0,0 +1,497 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/dag" +) + +// ResourceCountTransformer is a GraphTransformer that expands the count +// out for a specific resource. +type ResourceCountTransformer struct { + Resource *config.Resource + Destroy bool +} + +func (t *ResourceCountTransformer) Transform(g *Graph) error { + // Expand the resource count + count, err := t.Resource.Count() + if err != nil { + return err + } + + // Don't allow the count to be negative + if count < 0 { + return fmt.Errorf("negative count: %d", count) + } + + // For each count, build and add the node + nodes := make([]dag.Vertex, count) + for i := 0; i < count; i++ { + // Set the index. If our count is 1 we special case it so that + // we handle the "resource.0" and "resource" boundary properly. + index := i + if count == 1 { + index = -1 + } + + // Save the node for later so we can do connections. Make the + // proper node depending on if we're just a destroy node or if + // were a regular node. + var node dag.Vertex = &graphNodeExpandedResource{ + Index: index, + Resource: t.Resource, + } + if t.Destroy { + node = &graphNodeExpandedResourceDestroy{ + graphNodeExpandedResource: node.(*graphNodeExpandedResource), + } + } + + // Add the node now + nodes[i] = node + g.Add(nodes[i]) + } + + // Make the dependency connections + for _, n := range nodes { + // Connect the dependents. We ignore the return value for missing + // dependents since that should've been caught at a higher level. + g.ConnectDependent(n) + } + + return nil +} + +type graphNodeExpandedResource struct { + Index int + Resource *config.Resource +} + +func (n *graphNodeExpandedResource) Name() string { + if n.Index == -1 { + return n.Resource.Id() + } + + return fmt.Sprintf("%s #%d", n.Resource.Id(), n.Index) +} + +// GraphNodeDependable impl. +func (n *graphNodeExpandedResource) DependableName() []string { + return []string{ + n.Resource.Id(), + n.stateId(), + } +} + +// GraphNodeDependent impl. +func (n *graphNodeExpandedResource) DependentOn() []string { + config := &GraphNodeConfigResource{Resource: n.Resource} + return config.DependentOn() +} + +// GraphNodeProviderConsumer +func (n *graphNodeExpandedResource) ProvidedBy() []string { + return []string{resourceProvider(n.Resource.Type)} +} + +// GraphNodeEvalable impl. +func (n *graphNodeExpandedResource) EvalTree() EvalNode { + var diff *InstanceDiff + var provider ResourceProvider + var resourceConfig *ResourceConfig + var state *InstanceState + + // Build the resource. If we aren't part of a multi-resource, then + // we still consider ourselves as count index zero. + index := n.Index + if index < 0 { + index = 0 + } + resource := &Resource{CountIndex: index} + + seq := &EvalSequence{Nodes: make([]EvalNode, 0, 5)} + + // Validate the resource + vseq := &EvalSequence{Nodes: make([]EvalNode, 0, 5)} + vseq.Nodes = append(vseq.Nodes, &EvalGetProvider{ + Name: n.ProvidedBy()[0], + Output: &provider, + }) + vseq.Nodes = append(vseq.Nodes, &EvalInterpolate{ + Config: n.Resource.RawConfig, + Resource: resource, + Output: &resourceConfig, + }) + vseq.Nodes = append(vseq.Nodes, &EvalValidateResource{ + Provider: &provider, + Config: &resourceConfig, + ResourceName: n.Resource.Name, + ResourceType: n.Resource.Type, + }) + + // Validate all the provisioners + for _, p := range n.Resource.Provisioners { + var provisioner ResourceProvisioner + vseq.Nodes = append(vseq.Nodes, &EvalGetProvisioner{ + Name: p.Type, + Output: &provisioner, + }, &EvalInterpolate{ + Config: p.RawConfig, + Resource: resource, + Output: &resourceConfig, + }, &EvalValidateProvisioner{ + Provisioner: &provisioner, + Config: &resourceConfig, + }) + } + + // Add the validation operations + seq.Nodes = append(seq.Nodes, &EvalOpFilter{ + Ops: []walkOperation{walkValidate}, + Node: vseq, + }) + + // Build instance info + info := n.instanceInfo() + seq.Nodes = append(seq.Nodes, &EvalInstanceInfo{Info: info}) + + // Refresh the resource + seq.Nodes = append(seq.Nodes, &EvalOpFilter{ + Ops: []walkOperation{walkRefresh}, + Node: &EvalSequence{ + Nodes: []EvalNode{ + &EvalGetProvider{ + Name: n.ProvidedBy()[0], + Output: &provider, + }, + &EvalReadState{ + Name: n.stateId(), + Output: &state, + }, + &EvalRefresh{ + Info: info, + Provider: &provider, + State: &state, + Output: &state, + }, + &EvalWriteState{ + Name: n.stateId(), + ResourceType: n.Resource.Type, + Dependencies: n.DependentOn(), + State: &state, + }, + }, + }, + }) + + // Diff the resource + seq.Nodes = append(seq.Nodes, &EvalOpFilter{ + Ops: []walkOperation{walkPlan}, + Node: &EvalSequence{ + Nodes: []EvalNode{ + &EvalInterpolate{ + Config: n.Resource.RawConfig, + Resource: resource, + Output: &resourceConfig, + }, + &EvalGetProvider{ + Name: n.ProvidedBy()[0], + Output: &provider, + }, + &EvalReadState{ + Name: n.stateId(), + Output: &state, + }, + &EvalDiff{ + Info: info, + Config: &resourceConfig, + Provider: &provider, + State: &state, + Output: &diff, + OutputState: &state, + }, + &EvalWriteState{ + Name: n.stateId(), + ResourceType: n.Resource.Type, + Dependencies: n.DependentOn(), + State: &state, + }, + &EvalDiffTainted{ + Diff: &diff, + Name: n.stateId(), + }, + &EvalWriteDiff{ + Name: n.stateId(), + Diff: &diff, + }, + }, + }, + }) + + // Diff the resource for destruction + seq.Nodes = append(seq.Nodes, &EvalOpFilter{ + Ops: []walkOperation{walkPlanDestroy}, + Node: &EvalSequence{ + Nodes: []EvalNode{ + &EvalReadState{ + Name: n.stateId(), + Output: &state, + }, + &EvalDiffDestroy{ + Info: info, + State: &state, + Output: &diff, + }, + &EvalWriteDiff{ + Name: n.stateId(), + Diff: &diff, + }, + }, + }, + }) + + // Apply + var diffApply *InstanceDiff + var err error + var createNew, tainted bool + var createBeforeDestroyEnabled bool + seq.Nodes = append(seq.Nodes, &EvalOpFilter{ + Ops: []walkOperation{walkApply}, + Node: &EvalSequence{ + Nodes: []EvalNode{ + // Get the saved diff for apply + &EvalReadDiff{ + Name: n.stateId(), + Diff: &diffApply, + }, + + // We don't want to do any destroys + &EvalIf{ + If: func(ctx EvalContext) (bool, error) { + if diffApply == nil { + return true, EvalEarlyExitError{} + } + + if diffApply.Destroy && len(diffApply.Attributes) == 0 { + return true, EvalEarlyExitError{} + } + + diffApply.Destroy = false + return true, nil + }, + Node: EvalNoop{}, + }, + + &EvalIf{ + If: func(ctx EvalContext) (bool, error) { + destroy := false + if diffApply != nil { + destroy = diffApply.Destroy || diffApply.RequiresNew() + } + + createBeforeDestroyEnabled = + n.Resource.Lifecycle.CreateBeforeDestroy && + destroy + + return createBeforeDestroyEnabled, nil + }, + Node: &EvalDeposeState{ + Name: n.stateId(), + }, + }, + + &EvalInterpolate{ + Config: n.Resource.RawConfig, + Resource: resource, + Output: &resourceConfig, + }, + &EvalGetProvider{ + Name: n.ProvidedBy()[0], + Output: &provider, + }, + &EvalReadState{ + Name: n.stateId(), + Output: &state, + }, + + &EvalDiff{ + Info: info, + Config: &resourceConfig, + Provider: &provider, + State: &state, + Output: &diffApply, + }, + + // Get the saved diff + &EvalReadDiff{ + Name: n.stateId(), + Diff: &diff, + }, + + // Compare the diffs + &EvalCompareDiff{ + Info: info, + One: &diff, + Two: &diffApply, + }, + + &EvalGetProvider{ + Name: n.ProvidedBy()[0], + Output: &provider, + }, + &EvalReadState{ + Name: n.stateId(), + Output: &state, + }, + &EvalApply{ + Info: info, + State: &state, + Diff: &diffApply, + Provider: &provider, + Output: &state, + Error: &err, + CreateNew: &createNew, + }, + &EvalWriteState{ + Name: n.stateId(), + ResourceType: n.Resource.Type, + Dependencies: n.DependentOn(), + State: &state, + }, + &EvalApplyProvisioners{ + Info: info, + State: &state, + Resource: n.Resource, + InterpResource: resource, + CreateNew: &createNew, + Tainted: &tainted, + Error: &err, + }, + &EvalIf{ + If: func(ctx EvalContext) (bool, error) { + if createBeforeDestroyEnabled { + tainted = err != nil + } + + failure := tainted || err != nil + return createBeforeDestroyEnabled && failure, nil + }, + Node: &EvalUndeposeState{ + Name: n.stateId(), + }, + }, + &EvalWriteState{ + Name: n.stateId(), + ResourceType: n.Resource.Type, + Dependencies: n.DependentOn(), + State: &state, + Tainted: &tainted, + TaintedIndex: -1, + TaintedClearPrimary: !n.Resource.Lifecycle.CreateBeforeDestroy, + }, + &EvalApplyPost{ + Info: info, + State: &state, + Error: &err, + }, + }, + }, + }) + + return seq +} + +// instanceInfo is used for EvalTree. +func (n *graphNodeExpandedResource) instanceInfo() *InstanceInfo { + return &InstanceInfo{Id: n.stateId(), Type: n.Resource.Type} +} + +// stateId is the name used for the state key +func (n *graphNodeExpandedResource) stateId() string { + if n.Index == -1 { + return n.Resource.Id() + } + + return fmt.Sprintf("%s.%d", n.Resource.Id(), n.Index) +} + +// GraphNodeStateRepresentative impl. +func (n *graphNodeExpandedResource) StateId() []string { + return []string{n.stateId()} +} + +// graphNodeExpandedResourceDestroy represents an expanded resource that +// is to be destroyed. +type graphNodeExpandedResourceDestroy struct { + *graphNodeExpandedResource +} + +func (n *graphNodeExpandedResourceDestroy) Name() string { + return fmt.Sprintf("%s (destroy)", n.graphNodeExpandedResource.Name()) +} + +// GraphNodeEvalable impl. +func (n *graphNodeExpandedResourceDestroy) EvalTree() EvalNode { + info := n.instanceInfo() + + var diffApply *InstanceDiff + var provider ResourceProvider + var state *InstanceState + var err error + return &EvalOpFilter{ + Ops: []walkOperation{walkApply}, + Node: &EvalSequence{ + Nodes: []EvalNode{ + // Get the saved diff for apply + &EvalReadDiff{ + Name: n.stateId(), + Diff: &diffApply, + }, + + // If we're not destroying, then compare diffs + &EvalIf{ + If: func(ctx EvalContext) (bool, error) { + if diffApply != nil && diffApply.Destroy { + return true, nil + } + + return true, EvalEarlyExitError{} + }, + Node: EvalNoop{}, + }, + + &EvalGetProvider{ + Name: n.ProvidedBy()[0], + Output: &provider, + }, + &EvalReadState{ + Name: n.stateId(), + Output: &state, + Tainted: n.Resource.Lifecycle.CreateBeforeDestroy, + TaintedIndex: -1, + }, + &EvalApply{ + Info: info, + State: &state, + Diff: &diffApply, + Provider: &provider, + Output: &state, + Error: &err, + }, + &EvalWriteState{ + Name: n.stateId(), + ResourceType: n.Resource.Type, + Dependencies: n.DependentOn(), + State: &state, + }, + &EvalApplyPost{ + Info: info, + State: &state, + Error: &err, + }, + }, + }, + } +} diff --git a/terraform/transform_resource_test.go b/terraform/transform_resource_test.go new file mode 100644 index 000000000..15c29d43e --- /dev/null +++ b/terraform/transform_resource_test.go @@ -0,0 +1,47 @@ +package terraform + +import ( + "strings" + "testing" +) + +func TestResourceCountTransformer(t *testing.T) { + cfg := testModule(t, "transform-resource-count-basic").Config() + resource := cfg.Resources[0] + + g := Graph{Path: RootModulePath} + { + tf := &ResourceCountTransformer{Resource: resource} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testResourceCountTransformStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestResourceCountTransformer_countNegative(t *testing.T) { + cfg := testModule(t, "transform-resource-count-negative").Config() + resource := cfg.Resources[0] + + g := Graph{Path: RootModulePath} + { + tf := &ResourceCountTransformer{Resource: resource} + if err := tf.Transform(&g); err == nil { + t.Fatal("should error") + } + } +} + +const testResourceCountTransformStr = ` +aws_instance.foo #0 + aws_instance.foo #2 +aws_instance.foo #1 + aws_instance.foo #2 +aws_instance.foo #2 + aws_instance.foo #2 +` diff --git a/terraform/transform_root.go b/terraform/transform_root.go new file mode 100644 index 000000000..bf5640ac6 --- /dev/null +++ b/terraform/transform_root.go @@ -0,0 +1,44 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/dag" +) + +// RootTransformer is a GraphTransformer that adds a root to the graph. +type RootTransformer struct{} + +func (t *RootTransformer) Transform(g *Graph) error { + // If we already have a good root, we're done + if _, err := g.Root(); err == nil { + return nil + } + + // Add a root + var root graphNodeRoot + g.Add(root) + + // Connect the root to all the edges that need it + for _, v := range g.Vertices() { + if v == root { + continue + } + + if g.UpEdges(v).Len() == 0 { + g.Connect(dag.BasicEdge(root, v)) + } + } + + return nil +} + +type graphNodeRoot struct{} + +func (n graphNodeRoot) Name() string { + return "root" +} + +func (n graphNodeRoot) Dot(name string) string { + return fmt.Sprintf("\"%s\" [shape=circle];", name) +} diff --git a/terraform/transform_root_test.go b/terraform/transform_root_test.go new file mode 100644 index 000000000..68f5c5114 --- /dev/null +++ b/terraform/transform_root_test.go @@ -0,0 +1,58 @@ +package terraform + +import ( + "strings" + "testing" +) + +func TestRootTransformer(t *testing.T) { + mod := testModule(t, "transform-root-basic") + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + transform := &ProviderTransformer{} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + transform := &RootTransformer{} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformRootBasicStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } + + root, err := g.Root() + if err != nil { + t.Fatalf("err: %s", err) + } + if _, ok := root.(graphNodeRoot); !ok { + t.Fatalf("bad: %#v", root) + } +} + +const testTransformRootBasicStr = ` +aws_instance.foo + provider.aws +do_droplet.bar + provider.do +provider.aws +provider.do +root + aws_instance.foo + do_droplet.bar +` diff --git a/terraform/transform_tainted.go b/terraform/transform_tainted.go new file mode 100644 index 000000000..50223a9e8 --- /dev/null +++ b/terraform/transform_tainted.go @@ -0,0 +1,171 @@ +package terraform + +import ( + "fmt" +) + +// TraintedTransformer is a GraphTransformer that adds tainted resources +// to the graph. +type TaintedTransformer struct { + // State is the global state. We'll automatically find the correct + // ModuleState based on the Graph.Path that is being transformed. + State *State + + // View, if non-empty, is the ModuleState.View used around the state + // to find tainted resources. + View string + + // Deposed, if set to true, assumes that the last tainted index + // represents a "deposed" resource, or a resource that was previously + // a primary but is now tainted since it is demoted. + Deposed bool + DeposedInclude bool +} + +func (t *TaintedTransformer) Transform(g *Graph) error { + state := t.State.ModuleByPath(g.Path) + if state == nil { + // If there is no state for our module there can't be any tainted + // resources, since they live in the state. + return nil + } + + // If we have a view, apply it now + if t.View != "" { + state = state.View(t.View) + } + + // Go through all the resources in our state to look for tainted resources + for k, rs := range state.Resources { + // If we have no tainted resources, then move on + if len(rs.Tainted) == 0 { + continue + } + tainted := rs.Tainted + + // If we expect a deposed resource, then shuffle a bit + if t.Deposed { + if t.DeposedInclude { + // Only include the deposed resource + tainted = rs.Tainted[len(rs.Tainted)-1:] + } else { + // Exclude the deposed resource + tainted = rs.Tainted[:len(rs.Tainted)-1] + } + } + + for i, _ := range tainted { + // Add the graph node and make the connection from any untainted + // resources with this name to the tainted resource, so that + // the tainted resource gets destroyed first. + g.Add(&graphNodeTaintedResource{ + Index: i, + ResourceName: k, + ResourceType: rs.Type, + }) + } + } + + return nil +} + +// graphNodeTaintedResource is the graph vertex representing a tainted resource. +type graphNodeTaintedResource struct { + Index int + ResourceName string + ResourceType string +} + +func (n *graphNodeTaintedResource) Name() string { + return fmt.Sprintf("%s (tainted #%d)", n.ResourceName, n.Index+1) +} + +func (n *graphNodeTaintedResource) ProvidedBy() []string { + return []string{resourceProvider(n.ResourceName)} +} + +// GraphNodeEvalable impl. +func (n *graphNodeTaintedResource) EvalTree() EvalNode { + var provider ResourceProvider + var state *InstanceState + tainted := true + + seq := &EvalSequence{Nodes: make([]EvalNode, 0, 5)} + + // Build instance info + info := &InstanceInfo{Id: n.ResourceName, Type: n.ResourceType} + seq.Nodes = append(seq.Nodes, &EvalInstanceInfo{Info: info}) + + // Refresh the resource + seq.Nodes = append(seq.Nodes, &EvalOpFilter{ + Ops: []walkOperation{walkRefresh}, + Node: &EvalSequence{ + Nodes: []EvalNode{ + &EvalGetProvider{ + Name: n.ProvidedBy()[0], + Output: &provider, + }, + &EvalReadState{ + Name: n.ResourceName, + Tainted: true, + TaintedIndex: n.Index, + Output: &state, + }, + &EvalRefresh{ + Info: info, + Provider: &provider, + State: &state, + Output: &state, + }, + &EvalWriteState{ + Name: n.ResourceName, + ResourceType: n.ResourceType, + State: &state, + Tainted: &tainted, + TaintedIndex: n.Index, + }, + }, + }, + }) + + // Apply + var diff *InstanceDiff + seq.Nodes = append(seq.Nodes, &EvalOpFilter{ + Ops: []walkOperation{walkApply}, + Node: &EvalSequence{ + Nodes: []EvalNode{ + &EvalGetProvider{ + Name: n.ProvidedBy()[0], + Output: &provider, + }, + &EvalReadState{ + Name: n.ResourceName, + Tainted: true, + TaintedIndex: n.Index, + Output: &state, + }, + &EvalDiffDestroy{ + Info: info, + State: &state, + Output: &diff, + }, + &EvalApply{ + Info: info, + State: &state, + Diff: &diff, + Provider: &provider, + Output: &state, + }, + &EvalWriteState{ + Name: n.ResourceName, + ResourceType: n.ResourceType, + State: &state, + Tainted: &tainted, + TaintedIndex: n.Index, + }, + }, + }, + }) + + return seq +} diff --git a/terraform/transform_tainted_test.go b/terraform/transform_tainted_test.go new file mode 100644 index 000000000..f78e86b5c --- /dev/null +++ b/terraform/transform_tainted_test.go @@ -0,0 +1,64 @@ +package terraform + +import ( + "strings" + "testing" + + "github.com/hashicorp/terraform/dag" +) + +func TestTaintedTransformer(t *testing.T) { + mod := testModule(t, "transform-tainted-basic") + state := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: RootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.web": &ResourceState{ + Type: "aws_instance", + Tainted: []*InstanceState{ + &InstanceState{ID: "foo"}, + }, + }, + }, + }, + }, + } + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + transform := &TaintedTransformer{State: state} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformTaintedBasicStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestGraphNodeTaintedResource_impl(t *testing.T) { + var _ dag.Vertex = new(graphNodeTaintedResource) + var _ dag.NamedVertex = new(graphNodeTaintedResource) + var _ GraphNodeProviderConsumer = new(graphNodeTaintedResource) +} + +func TestGraphNodeTaintedResource_ProvidedBy(t *testing.T) { + n := &graphNodeTaintedResource{ResourceName: "aws_instance.foo"} + if v := n.ProvidedBy(); v[0] != "aws" { + t.Fatalf("bad: %#v", v) + } +} + +const testTransformTaintedBasicStr = ` +aws_instance.web +aws_instance.web (tainted #1) +` diff --git a/terraform/transform_vertex.go b/terraform/transform_vertex.go new file mode 100644 index 000000000..6b1293fc2 --- /dev/null +++ b/terraform/transform_vertex.go @@ -0,0 +1,44 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/dag" +) + +// VertexTransformer is a GraphTransformer that transforms vertices +// using the GraphVertexTransformers. The Transforms are run in sequential +// order. If a transform replaces a vertex then the next transform will see +// the new vertex. +type VertexTransformer struct { + Transforms []GraphVertexTransformer +} + +func (t *VertexTransformer) Transform(g *Graph) error { + for _, v := range g.Vertices() { + for _, vt := range t.Transforms { + newV, err := vt.Transform(v) + if err != nil { + return err + } + + // If the vertex didn't change, then don't do anything more + if newV == v { + continue + } + + // Vertex changed, replace it within the graph + if ok := g.Replace(v, newV); !ok { + // This should never happen, big problem + return fmt.Errorf( + "Failed to replace %s with %s!\n\nSource: %#v\n\nTarget: %#v", + dag.VertexName(v), dag.VertexName(newV), v, newV) + } + + // Replace v so that future transforms use the proper vertex + v = newV + } + } + + return nil +} diff --git a/terraform/transform_vertex_test.go b/terraform/transform_vertex_test.go new file mode 100644 index 000000000..05d117689 --- /dev/null +++ b/terraform/transform_vertex_test.go @@ -0,0 +1,58 @@ +package terraform + +import ( + "strings" + "testing" + + "github.com/hashicorp/terraform/dag" +) + +func TestVertexTransformer_impl(t *testing.T) { + var _ GraphTransformer = new(VertexTransformer) +} + +func TestVertexTransformer(t *testing.T) { + var g Graph + g.Add(1) + g.Add(2) + g.Add(3) + g.Connect(dag.BasicEdge(1, 2)) + g.Connect(dag.BasicEdge(2, 3)) + + { + tf := &VertexTransformer{ + Transforms: []GraphVertexTransformer{ + &testVertexTransform{Source: 2, Target: 42}, + }, + } + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testVertexTransformerStr) + if actual != expected { + t.Fatalf("bad: %s", actual) + } +} + +type testVertexTransform struct { + Source, Target dag.Vertex +} + +func (t *testVertexTransform) Transform(v dag.Vertex) (dag.Vertex, error) { + if t.Source == v { + v = t.Target + } + + return v, nil +} + +const testVertexTransformerStr = ` +1 + 42 +3 +42 + 3 +` diff --git a/terraform/ui_output_callback.go b/terraform/ui_output_callback.go index 147515b95..135a91c5f 100644 --- a/terraform/ui_output_callback.go +++ b/terraform/ui_output_callback.go @@ -1,5 +1,9 @@ package terraform type CallbackUIOutput struct { - OutputFun func(string) + OutputFn func(string) +} + +func (o *CallbackUIOutput) Output(v string) { + o.OutputFn(v) } diff --git a/terraform/ui_output_callback_test.go b/terraform/ui_output_callback_test.go new file mode 100644 index 000000000..1dd5ccddf --- /dev/null +++ b/terraform/ui_output_callback_test.go @@ -0,0 +1,9 @@ +package terraform + +import ( + "testing" +) + +func TestCallbackUIOutput_impl(t *testing.T) { + var _ UIOutput = new(CallbackUIOutput) +} diff --git a/terraform/util.go b/terraform/util.go index ce7198d5b..7c8e0aaa2 100644 --- a/terraform/util.go +++ b/terraform/util.go @@ -1,5 +1,9 @@ package terraform +import ( + "strings" +) + // Semaphore is a wrapper around a channel to provide // utility methods to clarify that we are treating the // channel as a semaphore @@ -42,6 +46,16 @@ func (s Semaphore) Release() { } } +// resourceProvider returns the provider name for the given type. +func resourceProvider(t string) string { + idx := strings.IndexRune(t, '_') + if idx == -1 { + return "" + } + + return t[:idx] +} + // strSliceContains checks if a given string is contained in a slice // When anybody asks why Go needs generics, here you go. func strSliceContains(haystack []string, needle string) bool { diff --git a/terraform/walkoperation_string.go b/terraform/walkoperation_string.go new file mode 100644 index 000000000..ddd894f53 --- /dev/null +++ b/terraform/walkoperation_string.go @@ -0,0 +1,16 @@ +// generated by stringer -type=walkOperation graph_walk_operation.go; DO NOT EDIT + +package terraform + +import "fmt" + +const _walkOperation_name = "walkInvalidwalkInputwalkApplywalkPlanwalkPlanDestroywalkRefreshwalkValidate" + +var _walkOperation_index = [...]uint8{0, 11, 20, 29, 37, 52, 63, 75} + +func (i walkOperation) String() string { + if i+1 >= walkOperation(len(_walkOperation_index)) { + return fmt.Sprintf("walkOperation(%d)", i) + } + return _walkOperation_name[_walkOperation_index[i]:_walkOperation_index[i+1]] +}