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) {
|
func TestContext2Validate_moduleBadOutput(t *testing.T) {
|
||||||
p := testProvider("aws")
|
p := testProvider("aws")
|
||||||
m := testModule(t, "validate-bad-module-output")
|
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) {
|
func TestContextValidate_countVariable(t *testing.T) {
|
||||||
p := testProvider("aws")
|
p := testProvider("aws")
|
||||||
m := testModule(t, "apply-count-variable")
|
m := testModule(t, "apply-count-variable")
|
||||||
|
|
|
@ -6,6 +6,9 @@ import (
|
||||||
|
|
||||||
// EvalContext is the interface that is given to eval nodes to execute.
|
// EvalContext is the interface that is given to eval nodes to execute.
|
||||||
type EvalContext interface {
|
type EvalContext interface {
|
||||||
|
// Path is the current module path.
|
||||||
|
Path() []string
|
||||||
|
|
||||||
// InitProvider initializes the provider with the given name and
|
// InitProvider initializes the provider with the given name and
|
||||||
// returns the implementation of the resource provider or an error.
|
// returns the implementation of the resource provider or an error.
|
||||||
//
|
//
|
||||||
|
@ -41,6 +44,9 @@ type MockEvalContext struct {
|
||||||
InterpolateResource *Resource
|
InterpolateResource *Resource
|
||||||
InterpolateConfigResult *ResourceConfig
|
InterpolateConfigResult *ResourceConfig
|
||||||
InterpolateError error
|
InterpolateError error
|
||||||
|
|
||||||
|
PathCalled bool
|
||||||
|
PathPath []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *MockEvalContext) InitProvider(n string) (ResourceProvider, error) {
|
func (c *MockEvalContext) InitProvider(n string) (ResourceProvider, error) {
|
||||||
|
@ -62,3 +68,8 @@ func (c *MockEvalContext) Interpolate(
|
||||||
c.InterpolateResource = resource
|
c.InterpolateResource = resource
|
||||||
return c.InterpolateConfigResult, c.InterpolateError
|
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
|
// BuiltinEvalContext is an EvalContext implementation that is used by
|
||||||
// Terraform by default.
|
// Terraform by default.
|
||||||
type BuiltinEvalContext struct {
|
type BuiltinEvalContext struct {
|
||||||
Path []string
|
PathValue []string
|
||||||
Interpolater *Interpolater
|
Interpolater *Interpolater
|
||||||
Providers map[string]ResourceProviderFactory
|
Providers map[string]ResourceProviderFactory
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ func (ctx *BuiltinEvalContext) Interpolate(
|
||||||
cfg *config.RawConfig, r *Resource) (*ResourceConfig, error) {
|
cfg *config.RawConfig, r *Resource) (*ResourceConfig, error) {
|
||||||
if cfg != nil {
|
if cfg != nil {
|
||||||
scope := &InterpolationScope{
|
scope := &InterpolationScope{
|
||||||
Path: ctx.Path,
|
Path: ctx.Path(),
|
||||||
Resource: r,
|
Resource: r,
|
||||||
}
|
}
|
||||||
vs, err := ctx.Interpolater.Values(scope, cfg.Variables)
|
vs, err := ctx.Interpolater.Values(scope, cfg.Variables)
|
||||||
|
@ -67,6 +67,10 @@ func (ctx *BuiltinEvalContext) Interpolate(
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctx *BuiltinEvalContext) Path() []string {
|
||||||
|
return ctx.PathValue
|
||||||
|
}
|
||||||
|
|
||||||
func (ctx *BuiltinEvalContext) init() {
|
func (ctx *BuiltinEvalContext) init() {
|
||||||
// We nil-check the things below because they're meant to be configured,
|
// We nil-check the things below because they're meant to be configured,
|
||||||
// and we just default them to non-nil.
|
// and we just default them to non-nil.
|
||||||
|
|
|
@ -135,10 +135,10 @@ func (g *Graph) walk(walker GraphWalker) {
|
||||||
ctx := walker.EnterGraph(g)
|
ctx := walker.EnterGraph(g)
|
||||||
defer walker.ExitGraph(g)
|
defer walker.ExitGraph(g)
|
||||||
|
|
||||||
// Walk the graph
|
// Walk the graph.
|
||||||
g.AcyclicGraph.Walk(func(v dag.Vertex) {
|
var walkFn func(v dag.Vertex)
|
||||||
|
walkFn = func(v dag.Vertex) {
|
||||||
walker.EnterVertex(v)
|
walker.EnterVertex(v)
|
||||||
defer walker.ExitVertex(v)
|
|
||||||
|
|
||||||
// If the node is eval-able, then evaluate it.
|
// If the node is eval-able, then evaluate it.
|
||||||
if ev, ok := v.(GraphNodeEvalable); ok {
|
if ev, ok := v.(GraphNodeEvalable); ok {
|
||||||
|
@ -154,7 +154,24 @@ func (g *Graph) walk(walker GraphWalker) {
|
||||||
output, err := Eval(tree, ctx)
|
output, err := Eval(tree, ctx)
|
||||||
walker.ExitEvalTree(v, output, err)
|
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
|
// 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()}
|
return []string{n.Resource.Id()}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GraphNodeDependent impl.
|
||||||
func (n *GraphNodeConfigResource) DependentOn() []string {
|
func (n *GraphNodeConfigResource) DependentOn() []string {
|
||||||
result := make([]string, len(n.Resource.DependsOn),
|
result := make([]string, len(n.Resource.DependsOn),
|
||||||
len(n.Resource.RawCount.Variables)+
|
len(n.Resource.RawCount.Variables)+
|
||||||
|
@ -122,17 +123,17 @@ func (n *GraphNodeConfigResource) Name() string {
|
||||||
return n.Resource.Id()
|
return n.Resource.Id()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GraphNodeEvalable impl.
|
// GraphNodeDynamicExpandable impl.
|
||||||
func (n *GraphNodeConfigResource) EvalTree() EvalNode {
|
func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error) {
|
||||||
return &EvalSequence{
|
// Build the graph
|
||||||
Nodes: []EvalNode{
|
b := &BasicGraphBuilder{
|
||||||
&EvalValidateResource{
|
Steps: []GraphTransformer{
|
||||||
Provider: &EvalGetProvider{Name: n.ProvidedBy()},
|
&ResourceCountTransformer{Resource: n.Resource},
|
||||||
Config: &EvalInterpolate{Config: n.Resource.RawConfig},
|
&RootTransformer{},
|
||||||
ProviderType: n.ProvidedBy(),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return b.Build(ctx.Path())
|
||||||
}
|
}
|
||||||
|
|
||||||
// GraphNodeProviderConsumer
|
// GraphNodeProviderConsumer
|
||||||
|
|
|
@ -10,7 +10,7 @@ type GraphWalker interface {
|
||||||
EnterGraph(*Graph) EvalContext
|
EnterGraph(*Graph) EvalContext
|
||||||
ExitGraph(*Graph)
|
ExitGraph(*Graph)
|
||||||
EnterVertex(dag.Vertex)
|
EnterVertex(dag.Vertex)
|
||||||
ExitVertex(dag.Vertex)
|
ExitVertex(dag.Vertex, error)
|
||||||
EnterEvalTree(dag.Vertex, EvalNode) EvalNode
|
EnterEvalTree(dag.Vertex, EvalNode) EvalNode
|
||||||
ExitEvalTree(dag.Vertex, interface{}, error)
|
ExitEvalTree(dag.Vertex, interface{}, error)
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,6 @@ type NullGraphWalker struct{}
|
||||||
func (NullGraphWalker) EnterGraph(*Graph) EvalContext { return nil }
|
func (NullGraphWalker) EnterGraph(*Graph) EvalContext { return nil }
|
||||||
func (NullGraphWalker) ExitGraph(*Graph) {}
|
func (NullGraphWalker) ExitGraph(*Graph) {}
|
||||||
func (NullGraphWalker) EnterVertex(dag.Vertex) {}
|
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) EnterEvalTree(v dag.Vertex, n EvalNode) EvalNode { return n }
|
||||||
func (NullGraphWalker) ExitEvalTree(dag.Vertex, interface{}, error) {}
|
func (NullGraphWalker) ExitEvalTree(dag.Vertex, interface{}, error) {}
|
||||||
|
|
|
@ -27,7 +27,7 @@ type ContextGraphWalker struct {
|
||||||
|
|
||||||
func (w *ContextGraphWalker) EnterGraph(g *Graph) EvalContext {
|
func (w *ContextGraphWalker) EnterGraph(g *Graph) EvalContext {
|
||||||
return &BuiltinEvalContext{
|
return &BuiltinEvalContext{
|
||||||
Path: g.Path,
|
PathValue: g.Path,
|
||||||
Providers: w.Context.providers,
|
Providers: w.Context.providers,
|
||||||
Interpolater: &Interpolater{
|
Interpolater: &Interpolater{
|
||||||
Operation: w.Operation,
|
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)
|
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
|
// GraphNodeSubgraph is an interface a node can implement if it has
|
||||||
// a larger subgraph that should be walked.
|
// a larger subgraph that should be walked.
|
||||||
type GraphNodeSubgraph interface {
|
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