From 58347617e8bd1d01ab307fa3a11d4dd2a726dcf7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 7 Feb 2015 13:29:55 -0800 Subject: [PATCH] terraform: trying this graphwalker thing --- terraform/context.go | 64 +++------------------------------ terraform/graph.go | 40 +++++++++++++++++++++ terraform/graph_walk.go | 28 +++++++++++++++ terraform/graph_walk_context.go | 60 +++++++++++++++++++++++++++++++ terraform/graph_walk_test.go | 9 +++++ 5 files changed, 142 insertions(+), 59 deletions(-) create mode 100644 terraform/graph_walk.go create mode 100644 terraform/graph_walk_context.go create mode 100644 terraform/graph_walk_test.go diff --git a/terraform/context.go b/terraform/context.go index 4edb1237b..bcbe38acb 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -1,13 +1,10 @@ package terraform import ( - "fmt" "sync" - "github.com/hashicorp/errwrap" "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform/config/module" - "github.com/hashicorp/terraform/dag" ) // ContextOpts are the user-configurable options to create a context with @@ -68,7 +65,6 @@ func (c *Context2) GraphBuilder() GraphBuilder { // Validate validates the configuration and returns any warnings or errors. func (c *Context2) Validate() ([]string, []error) { - var warns []string var errs error // Validate the configuration itself @@ -85,8 +81,6 @@ func (c *Context2) Validate() ([]string, []error) { } } - evalCtx := c.evalContext(walkValidate) - // Build the graph graph, err := c.GraphBuilder().Build(RootModulePath) if err != nil { @@ -94,58 +88,10 @@ func (c *Context2) Validate() ([]string, []error) { } // Walk the graph - var lock sync.Mutex - graph.Walk(func(v dag.Vertex) { - ev, ok := v.(GraphNodeEvalable) - if !ok { - return - } + walker := &ContextGraphWalker{Context: c, Operation: walkValidate} + graph.Walk(walker) - tree := ev.EvalTree() - if tree == nil { - panic(fmt.Sprintf("%s (%T): nil eval tree", dag.VertexName(v), v)) - } - - _, err := Eval(tree, evalCtx) - if err == nil { - return - } - - lock.Lock() - defer lock.Unlock() - - verr, ok := err.(*EvalValidateError) - if !ok { - errs = multierror.Append(errs, err) - return - } - - warns = append(warns, verr.Warnings...) - errs = multierror.Append(errs, verr.Errors...) - }) - - // Get the actual list of errors out. We do this by checking if the - // error is a error wrapper (multierror is one) and grabbing all the - // wrapped errors. - var rerrs []error - if w, ok := errs.(errwrap.Wrapper); ok { - rerrs = w.WrappedErrors() - } - - return warns, rerrs -} - -func (c *Context2) evalContext(op walkOperation) *BuiltinEvalContext { - return &BuiltinEvalContext{ - Path: RootModulePath, - Providers: c.providers, - - Interpolater: &Interpolater{ - Operation: op, - Module: c.module, - State: c.state, - StateLock: &c.stateLock, - Variables: nil, - }, - } + // Return the result + rerrs := multierror.Append(errs, walker.ValidationErrors...) + return walker.ValidationWarnings, rerrs.Errors } diff --git a/terraform/graph.go b/terraform/graph.go index d0e68648c..539150772 100644 --- a/terraform/graph.go +++ b/terraform/graph.go @@ -1,6 +1,7 @@ package terraform import ( + "fmt" "sync" "github.com/hashicorp/terraform/dag" @@ -111,12 +112,51 @@ func (g *Graph) Dependable(n string) dag.Vertex { return nil } +// Walk walks the graph with the given walker for callbacks. The graph +// will be walked with full parallelism, so the walker should expect +// to be called in concurrently. +// +// There is no way to tell the walker to halt the walk. If you want to +// halt the walk, you should set a flag in your GraphWalker to ignore +// future callbacks. +func (g *Graph) Walk(walker GraphWalker) { + // TODO: test + g.walk(walker) +} + func (g *Graph) init() { if g.dependableMap == nil { g.dependableMap = make(map[string]dag.Vertex) } } +func (g *Graph) walk(walker GraphWalker) { + // The callbacks for enter/exiting a graph + ctx := walker.EnterGraph(g) + defer walker.ExitGraph(g) + + // Walk the graph + g.AcyclicGraph.Walk(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 { + tree := ev.EvalTree() + if tree == nil { + panic(fmt.Sprintf( + "%s (%T): nil eval tree", dag.VertexName(v), v)) + } + + // Allow the walker to change our tree if needed. Eval, + // then callback with the output. + tree = walker.EnterEvalTree(v, tree) + output, err := Eval(tree, ctx) + walker.ExitEvalTree(v, output, err) + } + }) +} + // GraphNodeDependable is an interface which says that a node can be // depended on (an edge can be placed between this node and another) according // to the well-known name returned by DependableName. diff --git a/terraform/graph_walk.go b/terraform/graph_walk.go new file mode 100644 index 000000000..0eba6df09 --- /dev/null +++ b/terraform/graph_walk.go @@ -0,0 +1,28 @@ +package terraform + +import ( + "github.com/hashicorp/terraform/dag" +) + +// GraphWalker is an interface that can be implemented that when used +// with Graph.Walk will invoke the given callbacks under certain events. +type GraphWalker interface { + EnterGraph(*Graph) EvalContext + ExitGraph(*Graph) + EnterVertex(dag.Vertex) + ExitVertex(dag.Vertex) + EnterEvalTree(dag.Vertex, EvalNode) EvalNode + ExitEvalTree(dag.Vertex, interface{}, error) +} + +// NullGraphWalker is a GraphWalker implementation that does nothing. +// This can be embedded within other GraphWalker implementations for easily +// implementing all the required functions. +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) EnterEvalTree(v dag.Vertex, n EvalNode) EvalNode { return n } +func (NullGraphWalker) ExitEvalTree(dag.Vertex, interface{}, error) {} diff --git a/terraform/graph_walk_context.go b/terraform/graph_walk_context.go new file mode 100644 index 000000000..e8931e01f --- /dev/null +++ b/terraform/graph_walk_context.go @@ -0,0 +1,60 @@ +package terraform + +import ( + "sync" + + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform/dag" +) + +// ContextGraphWalker is the GraphWalker implementation used with the +// Context struct to walk and evaluate the graph. +type ContextGraphWalker struct { + NullGraphWalker + + Context *Context2 + Operation walkOperation + + ErrorLock sync.Mutex + EvalError error + ValidationWarnings []string + ValidationErrors []error +} + +func (w *ContextGraphWalker) EnterGraph(g *Graph) EvalContext { + return &BuiltinEvalContext{ + Path: g.Path, + Providers: w.Context.providers, + Interpolater: &Interpolater{ + Operation: w.Operation, + Module: w.Context.module, + State: w.Context.state, + StateLock: &w.Context.stateLock, + Variables: nil, + }, + } +} + +func (w *ContextGraphWalker) ExitEvalTree( + v dag.Vertex, output interface{}, err error) { + if err == nil { + return + } + + // Acquire the lock because anything is going to require a lock. + w.ErrorLock.Lock() + defer w.ErrorLock.Unlock() + + // Try to get a validation error out of it. If its not a validation + // error, then just record the normal error. + verr, ok := err.(*EvalValidateError) + if !ok { + // Some other error, record it + w.EvalError = multierror.Append(w.EvalError, err) + return + } + + // Record the validation error + w.ValidationWarnings = append(w.ValidationWarnings, verr.Warnings...) + w.ValidationErrors = append(w.ValidationErrors, verr.Errors...) +} diff --git a/terraform/graph_walk_test.go b/terraform/graph_walk_test.go new file mode 100644 index 000000000..88b52a748 --- /dev/null +++ b/terraform/graph_walk_test.go @@ -0,0 +1,9 @@ +package terraform + +import ( + "testing" +) + +func TestNullGraphWalker_impl(t *testing.T) { + var _ GraphWalker = NullGraphWalker{} +}