From 23d097ee534736d57edadbdf7b90c423c39632f0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 Feb 2015 17:01:08 -0800 Subject: [PATCH] terraform: module inputs are passed through to subgraphs --- terraform/context.go | 3 ++ terraform/context_test.go | 6 +-- terraform/eval_context.go | 13 ++++++ terraform/eval_context_builtin.go | 40 ++++++------------ terraform/eval_variable.go | 64 +++++++++++++++++++++++++++++ terraform/graph_config_node.go | 53 +++++++++++++++++++++++- terraform/graph_config_node_test.go | 2 +- terraform/graph_walk_context.go | 28 ++++++++++++- terraform/path.go | 24 +++++++++++ terraform/transform_expand.go | 28 +------------ terraform/transform_module.go | 47 +++++++++++++++++++++ terraform/transform_module_test.go | 41 ++++++++++++++++++ 12 files changed, 287 insertions(+), 62 deletions(-) create mode 100644 terraform/eval_variable.go create mode 100644 terraform/path.go create mode 100644 terraform/transform_module.go create mode 100644 terraform/transform_module_test.go diff --git a/terraform/context.go b/terraform/context.go index 68e7f0ef8..44a128e9f 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -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 diff --git a/terraform/context_test.go b/terraform/context_test.go index dba9644ea..ff8a9c993 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -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") diff --git a/terraform/eval_context.go b/terraform/eval_context.go index 50c35ef2d..78fbd0d7b 100644 --- a/terraform/eval_context.go +++ b/terraform/eval_context.go @@ -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 diff --git a/terraform/eval_context_builtin.go b/terraform/eval_context_builtin.go index e2fbd121f..866297c7c 100644 --- a/terraform/eval_context_builtin.go +++ b/terraform/eval_context_builtin.go @@ -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)) -} diff --git a/terraform/eval_variable.go b/terraform/eval_variable.go new file mode 100644 index 000000000..6c8aade78 --- /dev/null +++ b/terraform/eval_variable.go @@ -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 +} diff --git a/terraform/graph_config_node.go b/terraform/graph_config_node.go index 4cb5b3c71..52ac95322 100644 --- a/terraform/graph_config_node.go +++ b/terraform/graph_config_node.go @@ -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 +} diff --git a/terraform/graph_config_node_test.go b/terraform/graph_config_node_test.go index 408226abc..7cca96160 100644 --- a/terraform/graph_config_node_test.go +++ b/terraform/graph_config_node_test.go @@ -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) diff --git a/terraform/graph_walk_context.go b/terraform/graph_walk_context.go index 9e31df5fe..b27387aef 100644 --- a/terraform/graph_walk_context.go +++ b/terraform/graph_walk_context.go @@ -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) diff --git a/terraform/path.go b/terraform/path.go new file mode 100644 index 000000000..ca99685ad --- /dev/null +++ b/terraform/path.go @@ -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)) +} diff --git a/terraform/transform_expand.go b/terraform/transform_expand.go index def09ce27..6cdda3e39 100644 --- a/terraform/transform_expand.go +++ b/terraform/transform_expand.go @@ -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) } diff --git a/terraform/transform_module.go b/terraform/transform_module.go new file mode 100644 index 000000000..816ccc455 --- /dev/null +++ b/terraform/transform_module.go @@ -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} +} diff --git a/terraform/transform_module_test.go b/terraform/transform_module_test.go new file mode 100644 index 000000000..b857108b2 --- /dev/null +++ b/terraform/transform_module_test.go @@ -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 +`