terraform: dynamic subgraph expansion for count

This commit is contained in:
Mitchell Hashimoto 2015-02-08 14:00:13 -08:00
parent ffe9ccacf0
commit 28a23a45f4
12 changed files with 223 additions and 37 deletions

View File

@ -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")

View File

@ -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
}

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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) {}

View File

@ -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,

View File

@ -0,0 +1,4 @@
resource "aws_instance" "foo" {
count = 3
value = "${aws_instance.foo.0.value}"
}

View File

@ -0,0 +1,4 @@
resource "aws_instance" "foo" {
count = -5
value = "${aws_instance.foo.0.value}"
}

View File

@ -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 {

View File

@ -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(),
},
},
}
}

View File

@ -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
`