diff --git a/terraform/context_test.go b/terraform/context_test.go index 46dce5f5a..cabc77203 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -44,6 +44,25 @@ func TestContext2Validate_badVar(t *testing.T) { } } +func TestContext2Validate_countNegative(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "validate-count-negative") + 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_moduleBadOutput(t *testing.T) { p := testProvider("aws") m := testModule(t, "validate-bad-module-output") @@ -263,25 +282,6 @@ func TestContext2Validate_selfRefMultiAll(t *testing.T) { } /* -func TestContextValidate_countNegative(t *testing.T) { - p := testProvider("aws") - m := testModule(t, "validate-count-negative") - 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_countVariable(t *testing.T) { p := testProvider("aws") m := testModule(t, "apply-count-variable") diff --git a/terraform/eval_context.go b/terraform/eval_context.go index 7ae383f0a..4c8806987 100644 --- a/terraform/eval_context.go +++ b/terraform/eval_context.go @@ -6,6 +6,9 @@ import ( // EvalContext is the interface that is given to eval nodes to execute. type EvalContext interface { + // Path is the current module path. + Path() []string + // InitProvider initializes the provider with the given name and // returns the implementation of the resource provider or an error. // @@ -41,6 +44,9 @@ type MockEvalContext struct { InterpolateResource *Resource InterpolateConfigResult *ResourceConfig InterpolateError error + + PathCalled bool + PathPath []string } func (c *MockEvalContext) InitProvider(n string) (ResourceProvider, error) { @@ -62,3 +68,8 @@ func (c *MockEvalContext) Interpolate( c.InterpolateResource = resource return c.InterpolateConfigResult, c.InterpolateError } + +func (c *MockEvalContext) Path() []string { + c.PathCalled = true + return c.PathPath +} diff --git a/terraform/eval_context_builtin.go b/terraform/eval_context_builtin.go index 2a1a298c6..dbd1dc02b 100644 --- a/terraform/eval_context_builtin.go +++ b/terraform/eval_context_builtin.go @@ -10,7 +10,7 @@ import ( // BuiltinEvalContext is an EvalContext implementation that is used by // Terraform by default. type BuiltinEvalContext struct { - Path []string + PathValue []string Interpolater *Interpolater Providers map[string]ResourceProviderFactory @@ -48,7 +48,7 @@ func (ctx *BuiltinEvalContext) Interpolate( cfg *config.RawConfig, r *Resource) (*ResourceConfig, error) { if cfg != nil { scope := &InterpolationScope{ - Path: ctx.Path, + Path: ctx.Path(), Resource: r, } vs, err := ctx.Interpolater.Values(scope, cfg.Variables) @@ -67,6 +67,10 @@ func (ctx *BuiltinEvalContext) Interpolate( return result, nil } +func (ctx *BuiltinEvalContext) Path() []string { + return ctx.PathValue +} + 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. diff --git a/terraform/graph.go b/terraform/graph.go index 539150772..36b19b797 100644 --- a/terraform/graph.go +++ b/terraform/graph.go @@ -135,10 +135,10 @@ func (g *Graph) walk(walker GraphWalker) { ctx := walker.EnterGraph(g) defer walker.ExitGraph(g) - // Walk the graph - g.AcyclicGraph.Walk(func(v dag.Vertex) { + // Walk the graph. + var walkFn func(v dag.Vertex) + walkFn = func(v dag.Vertex) { walker.EnterVertex(v) - defer walker.ExitVertex(v) // If the node is eval-able, then evaluate it. if ev, ok := v.(GraphNodeEvalable); ok { @@ -154,7 +154,24 @@ func (g *Graph) walk(walker GraphWalker) { output, err := Eval(tree, ctx) walker.ExitEvalTree(v, output, err) } - }) + + // If the node is dynamically expanded, then expand it + if ev, ok := v.(GraphNodeDynamicExpandable); ok { + g, err := ev.DynamicExpand(ctx) + if err != nil { + walker.ExitVertex(v, err) + return + } + + // Walk the subgraph + g.walk(walker) + } + + // Exit the vertex + walker.ExitVertex(v, nil) + } + + g.AcyclicGraph.Walk(walkFn) } // GraphNodeDependable is an interface which says that a node can be diff --git a/terraform/graph_config_node.go b/terraform/graph_config_node.go index f5de5833e..da14549b3 100644 --- a/terraform/graph_config_node.go +++ b/terraform/graph_config_node.go @@ -98,6 +98,7 @@ 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)+ @@ -122,17 +123,17 @@ func (n *GraphNodeConfigResource) Name() string { return n.Resource.Id() } -// GraphNodeEvalable impl. -func (n *GraphNodeConfigResource) EvalTree() EvalNode { - return &EvalSequence{ - Nodes: []EvalNode{ - &EvalValidateResource{ - Provider: &EvalGetProvider{Name: n.ProvidedBy()}, - Config: &EvalInterpolate{Config: n.Resource.RawConfig}, - ProviderType: n.ProvidedBy(), - }, +// GraphNodeDynamicExpandable impl. +func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error) { + // Build the graph + b := &BasicGraphBuilder{ + Steps: []GraphTransformer{ + &ResourceCountTransformer{Resource: n.Resource}, + &RootTransformer{}, }, } + + return b.Build(ctx.Path()) } // GraphNodeProviderConsumer diff --git a/terraform/graph_walk.go b/terraform/graph_walk.go index 0eba6df09..13b169559 100644 --- a/terraform/graph_walk.go +++ b/terraform/graph_walk.go @@ -10,7 +10,7 @@ type GraphWalker interface { EnterGraph(*Graph) EvalContext ExitGraph(*Graph) EnterVertex(dag.Vertex) - ExitVertex(dag.Vertex) + ExitVertex(dag.Vertex, error) EnterEvalTree(dag.Vertex, EvalNode) EvalNode ExitEvalTree(dag.Vertex, interface{}, error) } @@ -23,6 +23,6 @@ type NullGraphWalker struct{} func (NullGraphWalker) EnterGraph(*Graph) EvalContext { return nil } func (NullGraphWalker) ExitGraph(*Graph) {} func (NullGraphWalker) EnterVertex(dag.Vertex) {} -func (NullGraphWalker) ExitVertex(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) {} diff --git a/terraform/graph_walk_context.go b/terraform/graph_walk_context.go index d018c4a88..d576375eb 100644 --- a/terraform/graph_walk_context.go +++ b/terraform/graph_walk_context.go @@ -27,7 +27,7 @@ type ContextGraphWalker struct { func (w *ContextGraphWalker) EnterGraph(g *Graph) EvalContext { return &BuiltinEvalContext{ - Path: g.Path, + PathValue: g.Path, Providers: w.Context.providers, Interpolater: &Interpolater{ Operation: w.Operation, 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/transform_expand.go b/terraform/transform_expand.go index ada9775f2..def09ce27 100644 --- a/terraform/transform_expand.go +++ b/terraform/transform_expand.go @@ -13,6 +13,14 @@ type GraphNodeExpandable interface { Expand(GraphBuilder) (*Graph, 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 { diff --git a/terraform/transform_resource.go b/terraform/transform_resource.go new file mode 100644 index 000000000..49ef0fc6e --- /dev/null +++ b/terraform/transform_resource.go @@ -0,0 +1,90 @@ +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 +} + +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++ { + // Save the node for later so we can do connections + nodes[i] = &graphNodeExpandedResource{ + Index: i, + Resource: t.Resource, + } + + // Add the node now + 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 { + return fmt.Sprintf("%s #%d", n.Resource.Id(), n.Index) +} + +// GraphNodeDependable impl. +func (n *graphNodeExpandedResource) DependableName() []string { + return []string{ + n.Resource.Id(), + fmt.Sprintf("%s.%d", n.Resource.Id(), n.Index), + } +} + +// GraphNodeDependent impl. +func (n *graphNodeExpandedResource) DependentOn() []string { + config := &GraphNodeConfigResource{Resource: n.Resource} + return config.DependentOn() +} + +// GraphNodeProviderConsumer +func (n *graphNodeExpandedResource) ProvidedBy() string { + return resourceProvider(n.Resource.Type) +} + +// GraphNodeEvalable impl. +func (n *graphNodeExpandedResource) EvalTree() EvalNode { + return &EvalSequence{ + Nodes: []EvalNode{ + &EvalValidateResource{ + Provider: &EvalGetProvider{Name: n.ProvidedBy()}, + Config: &EvalInterpolate{Config: n.Resource.RawConfig}, + ProviderType: n.ProvidedBy(), + }, + }, + } +} 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 +`