terraform: module inputs are passed through to subgraphs

This commit is contained in:
Mitchell Hashimoto 2015-02-11 17:01:08 -08:00
parent 5595229430
commit 23d097ee53
12 changed files with 287 additions and 62 deletions

View File

@ -118,6 +118,9 @@ func (c *Context2) Plan(opts *PlanOpts) (*Plan, error) {
// Do the walk
walker, err := c.walk(operation)
if err != nil {
return nil, err
}
p.Diff = walker.Diff
// Update the diff so that our context is up-to-date

View File

@ -109,12 +109,11 @@ func TestContext2Plan_modules(t *testing.T) {
}
}
/*
func TestContextPlan_moduleInput(t *testing.T) {
func TestContext2Plan_moduleInput(t *testing.T) {
m := testModule(t, "plan-module-input")
p := testProvider("aws")
p.DiffFn = testDiffFn
ctx := testContext(t, &ContextOpts{
ctx := testContext2(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
@ -133,6 +132,7 @@ func TestContextPlan_moduleInput(t *testing.T) {
}
}
/*
func TestContextPlan_moduleInputComputed(t *testing.T) {
m := testModule(t, "plan-module-input-computed")
p := testProvider("aws")

View File

@ -49,6 +49,11 @@ type EvalContext interface {
// that is currently being acted upon.
Interpolate(*config.RawConfig, *Resource) (*ResourceConfig, error)
// SetVariables sets the variables for interpolation. These variables
// should not have a "var." prefix. For example: "var.foo" should be
// "foo" as the key.
SetVariables(map[string]string)
// Diff returns the global diff as well as the lock that should
// be used to modify that diff.
Diff() (*Diff, *sync.RWMutex)
@ -100,6 +105,9 @@ type MockEvalContext struct {
PathCalled bool
PathPath []string
SetVariablesCalled bool
SetVariablesVariables map[string]string
DiffCalled bool
DiffDiff *Diff
DiffLock *sync.RWMutex
@ -164,6 +172,11 @@ func (c *MockEvalContext) Path() []string {
return c.PathPath
}
func (c *MockEvalContext) SetVariables(vs map[string]string) {
c.SetVariablesCalled = true
c.SetVariablesVariables = vs
}
func (c *MockEvalContext) Diff() (*Diff, *sync.RWMutex) {
c.DiffCalled = true
return c.DiffDiff, c.DiffLock

View File

@ -1,8 +1,6 @@
package terraform
import (
"crypto/md5"
"encoding/hex"
"fmt"
"sync"
@ -72,7 +70,7 @@ func (ctx *BuiltinEvalContext) InitProvider(n string) (ResourceProvider, error)
return nil, err
}
ctx.ProviderCache[ctx.pathCacheKey(ctx.Path())] = p
ctx.ProviderCache[PathCacheKey(ctx.Path())] = p
return p, nil
}
@ -82,7 +80,7 @@ func (ctx *BuiltinEvalContext) Provider(n string) ResourceProvider {
ctx.ProviderLock.Lock()
defer ctx.ProviderLock.Unlock()
return ctx.ProviderCache[ctx.pathCacheKey(ctx.Path())]
return ctx.ProviderCache[PathCacheKey(ctx.Path())]
}
func (ctx *BuiltinEvalContext) ConfigureProvider(
@ -94,7 +92,7 @@ func (ctx *BuiltinEvalContext) ConfigureProvider(
// Save the configuration
ctx.ProviderLock.Lock()
ctx.ProviderConfigCache[ctx.pathCacheKey(ctx.Path())] = cfg
ctx.ProviderConfigCache[PathCacheKey(ctx.Path())] = cfg
ctx.ProviderLock.Unlock()
return p.Configure(cfg)
@ -106,7 +104,7 @@ func (ctx *BuiltinEvalContext) ParentProviderConfig(n string) *ResourceConfig {
path := ctx.Path()
for i := len(path) - 1; i >= 1; i-- {
k := ctx.pathCacheKey(path[:i])
k := PathCacheKey(path[:i])
if v, ok := ctx.ProviderConfigCache[k]; ok {
return v
}
@ -139,7 +137,7 @@ func (ctx *BuiltinEvalContext) InitProvisioner(
return nil, err
}
ctx.ProvisionerCache[ctx.pathCacheKey(ctx.Path())] = p
ctx.ProvisionerCache[PathCacheKey(ctx.Path())] = p
return p, nil
}
@ -149,7 +147,7 @@ func (ctx *BuiltinEvalContext) Provisioner(n string) ResourceProvisioner {
ctx.ProvisionerLock.Lock()
defer ctx.ProvisionerLock.Unlock()
return ctx.ProvisionerCache[ctx.pathCacheKey(ctx.Path())]
return ctx.ProvisionerCache[PathCacheKey(ctx.Path())]
}
func (ctx *BuiltinEvalContext) Interpolate(
@ -179,6 +177,12 @@ func (ctx *BuiltinEvalContext) Path() []string {
return ctx.PathValue
}
func (ctx *BuiltinEvalContext) SetVariables(vs map[string]string) {
for k, v := range vs {
ctx.Interpolater.Variables[k] = v
}
}
func (ctx *BuiltinEvalContext) Diff() (*Diff, *sync.RWMutex) {
return ctx.DiffValue, ctx.DiffLock
}
@ -194,23 +198,3 @@ func (ctx *BuiltinEvalContext) init() {
ctx.Providers = make(map[string]ResourceProviderFactory)
}
}
// pathCacheKey returns a cache key for the current module path, unique to
// the module path.
//
// This is used because there is a variety of information that needs to be
// cached per-path, rather than per-context.
func (ctx *BuiltinEvalContext) pathCacheKey(path []string) string {
// There is probably a better way to do this, but this is working for now.
// We just create an MD5 hash of all the MD5 hashes of all the path
// elements. This gets us the property that it is unique per ordering.
hash := md5.New()
for _, p := range path {
single := md5.Sum([]byte(p))
if _, err := hash.Write(single[:]); err != nil {
panic(err)
}
}
return hex.EncodeToString(hash.Sum(nil))
}

View File

@ -0,0 +1,64 @@
package terraform
import (
"github.com/hashicorp/terraform/config"
)
// EvalSetVariables is an EvalNode implementation that sets the variables
// explicitly for interpolation later.
type EvalSetVariables struct {
Variables map[string]string
}
func (n *EvalSetVariables) Args() ([]EvalNode, []EvalType) {
return nil, nil
}
// TODO: test
func (n *EvalSetVariables) Eval(
ctx EvalContext, args []interface{}) (interface{}, error) {
ctx.SetVariables(n.Variables)
return nil, nil
}
func (n *EvalSetVariables) Type() EvalType {
return EvalTypeNull
}
// EvalVariableBlock is an EvalNode implementation that evaluates the
// given configuration, and uses the final values as a way to set the
// mapping.
type EvalVariableBlock struct {
Config EvalNode
Variables map[string]string
}
func (n *EvalVariableBlock) Args() ([]EvalNode, []EvalType) {
return []EvalNode{n.Config}, []EvalType{EvalTypeConfig}
}
// TODO: test
func (n *EvalVariableBlock) Eval(
ctx EvalContext, args []interface{}) (interface{}, error) {
// Clear out the existing mapping
for k, _ := range n.Variables {
delete(n.Variables, k)
}
// Get our configuration
rc := args[0].(*ResourceConfig)
for k, v := range rc.Config {
n.Variables[k] = v.(string)
}
for k, _ := range rc.Raw {
if _, ok := n.Variables[k]; !ok {
n.Variables[k] = config.UnknownVariableValue
}
}
return nil, nil
}
func (n *EvalVariableBlock) Type() EvalType {
return EvalTypeNull
}

View File

@ -48,8 +48,26 @@ func (n *GraphNodeConfigModule) Name() string {
}
// GraphNodeExpandable
func (n *GraphNodeConfigModule) Expand(b GraphBuilder) (*Graph, error) {
return b.Build(n.Path)
func (n *GraphNodeConfigModule) Expand(b GraphBuilder) (GraphNodeSubgraph, error) {
// Build the graph first
graph, err := b.Build(n.Path)
if err != nil {
return nil, err
}
// Add the parameters node to the module
t := &ModuleInputTransformer{Variables: make(map[string]string)}
if err := t.Transform(graph); err != nil {
return nil, err
}
// Build the actual subgraph node
return &graphNodeModuleExpanded{
Original: n,
Graph: graph,
InputConfig: n.Module.RawConfig,
Variables: t.Variables,
}, nil
}
// GraphNodeExpandable
@ -181,3 +199,34 @@ func (n *GraphNodeConfigResource) ProvisionedBy() []string {
return result
}
// graphNodeModuleExpanded represents a module where the graph has
// been expanded. It stores the graph of the module as well as a reference
// to the map of variables.
type graphNodeModuleExpanded struct {
Original dag.Vertex
Graph *Graph
InputConfig *config.RawConfig
// Variables is a map of the input variables. This reference should
// be shared with ModuleInputTransformer in order to create a connection
// where the variables are set properly.
Variables map[string]string
}
func (n *graphNodeModuleExpanded) Name() string {
return fmt.Sprintf("%s (expanded)", dag.VertexName(n.Original))
}
// GraphNodeEvalable impl.
func (n *graphNodeModuleExpanded) EvalTree() EvalNode {
return &EvalVariableBlock{
Config: &EvalInterpolate{Config: n.InputConfig},
Variables: n.Variables,
}
}
// GraphNodeSubgraph impl.
func (n *graphNodeModuleExpanded) Subgraph() *Graph {
return n.Graph
}

View File

@ -34,7 +34,7 @@ func TestGraphNodeConfigModuleExpand(t *testing.T) {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(g.String())
actual := strings.TrimSpace(g.Subgraph().String())
expected := strings.TrimSpace(testGraphNodeModuleExpandStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)

View File

@ -26,6 +26,8 @@ type ContextGraphWalker struct {
errorLock sync.Mutex
once sync.Once
diffLock sync.RWMutex
contexts map[string]*BuiltinEvalContext
contextLock sync.Mutex
providerCache map[string]ResourceProvider
providerConfigCache map[string]*ResourceConfig
providerLock sync.Mutex
@ -36,7 +38,25 @@ type ContextGraphWalker struct {
func (w *ContextGraphWalker) EnterGraph(g *Graph) EvalContext {
w.once.Do(w.init)
return &BuiltinEvalContext{
w.contextLock.Lock()
defer w.contextLock.Unlock()
// If we already have a context for this path cached, use that
key := PathCacheKey(g.Path)
if ctx, ok := w.contexts[key]; ok {
return ctx
}
// Variables should be our context variables, but these only apply
// to the root module. As we enter subgraphs, we don't want to set
// variables, which is set by the SetVariables EvalContext function.
variables := w.Context.variables
if len(g.Path) > 1 {
// We're in a submodule, the variables should be empty
variables = make(map[string]string)
}
ctx := &BuiltinEvalContext{
PathValue: g.Path,
Hooks: w.Context.hooks,
Providers: w.Context.providers,
@ -55,9 +75,12 @@ func (w *ContextGraphWalker) EnterGraph(g *Graph) EvalContext {
Module: w.Context.module,
State: w.Context.state,
StateLock: &w.Context.stateLock,
Variables: w.Context.variables,
Variables: variables,
},
}
w.contexts[key] = ctx
return ctx
}
func (w *ContextGraphWalker) EnterEvalTree(v dag.Vertex, n EvalNode) EvalNode {
@ -94,6 +117,7 @@ func (w *ContextGraphWalker) init() {
w.Diff = new(Diff)
w.Diff.init()
w.contexts = make(map[string]*BuiltinEvalContext, 5)
w.providerCache = make(map[string]ResourceProvider, 5)
w.providerConfigCache = make(map[string]*ResourceConfig, 5)
w.provisionerCache = make(map[string]ResourceProvisioner, 5)

24
terraform/path.go Normal file
View File

@ -0,0 +1,24 @@
package terraform
import (
"crypto/md5"
"encoding/hex"
)
// PathCacheKey returns a cache key for a module path.
//
// TODO: test
func PathCacheKey(path []string) string {
// There is probably a better way to do this, but this is working for now.
// We just create an MD5 hash of all the MD5 hashes of all the path
// elements. This gets us the property that it is unique per ordering.
hash := md5.New()
for _, p := range path {
single := md5.Sum([]byte(p))
if _, err := hash.Write(single[:]); err != nil {
panic(err)
}
}
return hex.EncodeToString(hash.Sum(nil))
}

View File

@ -1,8 +1,6 @@
package terraform
import (
"fmt"
"github.com/hashicorp/terraform/dag"
)
@ -10,7 +8,7 @@ import (
// signal that they can be expanded. Expanded nodes turn into
// GraphNodeSubgraph nodes within the graph.
type GraphNodeExpandable interface {
Expand(GraphBuilder) (*Graph, error)
Expand(GraphBuilder) (GraphNodeSubgraph, error)
}
// GraphNodeDynamicExpandable is an interface that nodes can implement
@ -43,27 +41,5 @@ func (t *ExpandTransform) Transform(v dag.Vertex) (dag.Vertex, error) {
}
// Expand the subgraph!
g, err := ev.Expand(t.Builder)
if err != nil {
return nil, err
}
// Replace with our special node
return &graphNodeExpanded{
Graph: g,
OriginalName: dag.VertexName(v),
}, nil
}
type graphNodeExpanded struct {
Graph *Graph
OriginalName string
}
func (n *graphNodeExpanded) Name() string {
return fmt.Sprintf("%s (expanded)", n.OriginalName)
}
func (n *graphNodeExpanded) Subgraph() *Graph {
return n.Graph
return ev.Expand(t.Builder)
}

View File

@ -0,0 +1,47 @@
package terraform
import (
"github.com/hashicorp/terraform/dag"
)
// ModuleInputTransformer is a GraphTransformer that adds a node to the
// graph for setting the module input variables for the remainder of the
// graph.
type ModuleInputTransformer struct {
Variables map[string]string
}
func (t *ModuleInputTransformer) Transform(g *Graph) error {
// Create the node
n := &graphNodeModuleInput{Variables: t.Variables}
// Add it to the graph
g.Add(n)
// Connect the inputs to the bottom of the graph so that it happens
// first.
for _, v := range g.Vertices() {
if v == n {
continue
}
if g.DownEdges(v).Len() == 0 {
g.Connect(dag.BasicEdge(v, n))
}
}
return nil
}
type graphNodeModuleInput struct {
Variables map[string]string
}
func (n *graphNodeModuleInput) Name() string {
return "module inputs"
}
// GraphNodeEvalable impl.
func (n *graphNodeModuleInput) EvalTree() EvalNode {
return &EvalSetVariables{Variables: n.Variables}
}

View File

@ -0,0 +1,41 @@
package terraform
import (
"strings"
"testing"
"github.com/hashicorp/terraform/dag"
)
func TestModuleInputTransformer(t *testing.T) {
var g Graph
g.Add(1)
g.Add(2)
g.Add(3)
g.Connect(dag.BasicEdge(1, 2))
g.Connect(dag.BasicEdge(1, 3))
{
tf := &ModuleInputTransformer{}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testModuleInputTransformStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
const testModuleInputTransformStr = `
1
2
3
2
module inputs
3
module inputs
module inputs
`