terraform: dynamic subgraph expansion for count
This commit is contained in:
parent
ffe9ccacf0
commit
28a23a45f4
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
resource "aws_instance" "foo" {
|
||||
count = 3
|
||||
value = "${aws_instance.foo.0.value}"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
resource "aws_instance" "foo" {
|
||||
count = -5
|
||||
value = "${aws_instance.foo.0.value}"
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
|
@ -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
|
||||
`
|
Loading…
Reference in New Issue