diff --git a/depgraph/graph.go b/depgraph/graph.go index 4e34e361f..acca4ce6c 100644 --- a/depgraph/graph.go +++ b/depgraph/graph.go @@ -9,6 +9,7 @@ import ( "bytes" "fmt" "sort" + "strings" "sync" "github.com/hashicorp/terraform/digraph" @@ -42,7 +43,34 @@ type ValidateError struct { } func (v *ValidateError) Error() string { - return "The depedency graph is not valid" + var msgs []string + + if v.MissingRoot { + msgs = append(msgs, "The graph has no single root") + } + + for _, n := range v.Unreachable { + msgs = append(msgs, fmt.Sprintf( + "Unreachable node: %s", n.Name)) + } + + for _, c := range v.Cycles { + cycleNodes := make([]string, len(c)) + for i, n := range c { + cycleNodes[i] = n.Name + } + + msgs = append(msgs, fmt.Sprintf( + "Cycle: %s", strings.Join(cycleNodes, " -> "))) + } + + for i, m := range msgs { + msgs[i] = fmt.Sprintf("* %s", m) + } + + return fmt.Sprintf( + "The dependency graph is not valid:\n\n%s", + strings.Join(msgs, "\n")) } // ConstraintError is used to return detailed violation diff --git a/terraform/graph.go b/terraform/graph.go index 486996cc4..01ef19cfc 100644 --- a/terraform/graph.go +++ b/terraform/graph.go @@ -45,8 +45,12 @@ type GraphOpts struct { // graph. This node is just a placemarker and has no associated functionality. const GraphRootNode = "root" -// GraphNodeResource is a node type in the graph that represents a resource. +// 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 Type string Config *config.Resource Orphan bool @@ -54,6 +58,15 @@ type GraphNodeResource struct { ResourceProviderID string } +// GraphNodeResourceMeta is a node type in the graph that represents the +// metadata for a resource. There will be one meta node for every resource +// in the configuration. +type GraphNodeResourceMeta struct { + Name string + Type string + Count int +} + // GraphNodeResourceProvider is a node type in the graph that represents // the configuration for a resource provider. type GraphNodeResourceProvider struct { @@ -152,18 +165,57 @@ func graphAddConfigResources( } } - noun := &depgraph.Noun{ - Name: r.Id(), - Meta: &GraphNodeResource{ - Type: r.Type, - Config: r, - Resource: &Resource{ - Id: r.Id(), - State: state, + resourceNouns := make([]*depgraph.Noun, r.Count) + for i := 0; i < r.Count; i++ { + name := r.Id() + + // 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) + } + + resourceNouns[i] = &depgraph.Noun{ + Name: name, + Meta: &GraphNodeResource{ + Type: r.Type, + Config: r, + Resource: &Resource{ + Id: name, + State: state, + }, }, - }, + } + } + + // 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{ + Name: r.Id(), + 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 } - nouns[noun.Name] = noun } // Build the list of nouns that we iterate over @@ -357,7 +409,10 @@ func graphAddProviderConfigs(g *depgraph.Graph, c *config.Config) { nounsList := make([]*depgraph.Noun, 0, 2) pcNouns := make(map[string]*depgraph.Noun) for _, noun := range g.Nouns { - resourceNode := noun.Meta.(*GraphNodeResource) + resourceNode, ok := noun.Meta.(*GraphNodeResource) + if !ok { + continue + } // Look up the provider config for this resource pcName := config.ProviderConfigName(resourceNode.Type, c.ProviderConfigs) @@ -401,11 +456,6 @@ func graphAddProviderConfigs(g *depgraph.Graph, c *config.Config) { func graphAddRoot(g *depgraph.Graph) { root := &depgraph.Noun{Name: GraphRootNode} for _, n := range g.Nouns { - // The root only needs to depend on all the resources - if _, ok := n.Meta.(*GraphNodeResource); !ok { - continue - } - root.Deps = append(root.Deps, &depgraph.Dependency{ Name: n.Name, Source: root, diff --git a/terraform/graph_test.go b/terraform/graph_test.go index 4e2b89e2e..d395f0b35 100644 --- a/terraform/graph_test.go +++ b/terraform/graph_test.go @@ -27,6 +27,21 @@ func TestGraph_configRequired(t *testing.T) { } } +func TestGraph_count(t *testing.T) { + config := testConfig(t, "graph-count") + + g, err := Graph(&GraphOpts{Config: config}) + 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_cycle(t *testing.T) { config := testConfig(t, "graph-cycle") @@ -226,6 +241,25 @@ root root -> openstack_floating_ip.random ` +const testTerraformGraphCountStr = ` +root: root +aws_instance.web + aws_instance.web -> aws_instance.web.0 + aws_instance.web -> aws_instance.web.1 + aws_instance.web -> aws_instance.web.2 +aws_instance.web.0 +aws_instance.web.1 +aws_instance.web.2 +aws_load_balancer.weblb + aws_load_balancer.weblb -> aws_instance.web +root + root -> aws_instance.web + root -> aws_instance.web.0 + root -> aws_instance.web.1 + root -> aws_instance.web.2 + root -> aws_load_balancer.weblb +` + const testTerraformGraphDiffStr = ` root: root aws_instance.foo diff --git a/terraform/test-fixtures/graph-count/main.tf b/terraform/test-fixtures/graph-count/main.tf new file mode 100644 index 000000000..b35995faa --- /dev/null +++ b/terraform/test-fixtures/graph-count/main.tf @@ -0,0 +1,7 @@ +resource "aws_instance" "web" { + count = 3 +} + +resource "aws_load_balancer" "weblb" { + members = "${aws_instance.web.*.id}" +}