From 10e82375f2770eb91392dcdd2b14df013ba149bb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 13 Feb 2015 09:05:09 -0800 Subject: [PATCH] terraform: early exit and cancellation --- terraform/context.go | 73 ++++++++++++++++++++++++++++++- terraform/context_test.go | 8 ++-- terraform/eval.go | 15 ++++++- terraform/eval_apply.go | 4 +- terraform/eval_context_builtin.go | 2 + 5 files changed, 93 insertions(+), 9 deletions(-) diff --git a/terraform/context.go b/terraform/context.go index 7dfbdbc5d..85299649e 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -32,9 +32,13 @@ type Context2 struct { module *module.Tree providers map[string]ResourceProviderFactory provisioners map[string]ResourceProvisionerFactory + sh *stopHook state *State stateLock sync.RWMutex variables map[string]string + + l sync.Mutex // Lock acquired during any task + runCh <-chan struct{} } // NewContext creates a new Context structure. @@ -43,6 +47,13 @@ type Context2 struct { // should not be mutated in any way, since the pointers are copied, not // the values themselves. func NewContext2(opts *ContextOpts) *Context2 { + // Copy all the hooks and add our stop hook. We don't append directly + // to the Config so that we're not modifying that in-place. + sh := new(stopHook) + hooks := make([]Hook, len(opts.Hooks)+1) + copy(hooks, opts.Hooks) + hooks[len(opts.Hooks)] = sh + state := opts.State if state == nil { state = new(State) @@ -51,10 +62,11 @@ func NewContext2(opts *ContextOpts) *Context2 { return &Context2{ diff: opts.Diff, - hooks: opts.Hooks, + hooks: hooks, module: opts.Module, providers: opts.Providers, provisioners: opts.Provisioners, + sh: sh, state: state, variables: opts.Variables, } @@ -88,6 +100,9 @@ func (c *Context2) GraphBuilder() GraphBuilder { // In addition to returning the resulting state, this context is updated // with the latest state. func (c *Context2) Apply() (*State, error) { + v := c.acquireRun() + defer c.releaseRun(v) + // Copy our own state c.state = c.state.deepcopy() @@ -108,6 +123,9 @@ func (c *Context2) Apply() (*State, error) { // Plan also updates the diff of this context to be the diff generated // by the plan, so Apply can be called after. func (c *Context2) Plan(opts *PlanOpts) (*Plan, error) { + v := c.acquireRun() + defer c.releaseRun(v) + p := &Plan{ Module: c.module, Vars: c.variables, @@ -157,6 +175,9 @@ func (c *Context2) Plan(opts *PlanOpts) (*Plan, error) { // Even in the case an error is returned, the state will be returned and // will potentially be partially updated. func (c *Context2) Refresh() (*State, []error) { + v := c.acquireRun() + defer c.releaseRun(v) + // Copy our own state c.state = c.state.deepcopy() @@ -172,8 +193,32 @@ func (c *Context2) Refresh() (*State, []error) { return c.state, nil } +// Stop stops the running task. +// +// Stop will block until the task completes. +func (c *Context2) Stop() { + c.l.Lock() + ch := c.runCh + + // If we aren't running, then just return + if ch == nil { + c.l.Unlock() + return + } + + // Tell the hook we want to stop + c.sh.Stop() + + // Wait for us to stop + c.l.Unlock() + <-ch +} + // Validate validates the configuration and returns any warnings or errors. func (c *Context2) Validate() ([]string, []error) { + v := c.acquireRun() + defer c.releaseRun(v) + var errs error // Validate the configuration itself @@ -201,6 +246,32 @@ func (c *Context2) Validate() ([]string, []error) { return walker.ValidationWarnings, rerrs.Errors } +func (c *Context2) acquireRun() chan<- struct{} { + c.l.Lock() + defer c.l.Unlock() + + // Wait for no channel to exist + for c.runCh != nil { + c.l.Unlock() + ch := c.runCh + <-ch + c.l.Lock() + } + + ch := make(chan struct{}) + c.runCh = ch + return ch +} + +func (c *Context2) releaseRun(ch chan<- struct{}) { + c.l.Lock() + defer c.l.Unlock() + + close(ch) + c.runCh = nil + c.sh.Reset() +} + func (c *Context2) walk(operation walkOperation) (*ContextGraphWalker, error) { // Build the graph graph, err := c.GraphBuilder().Build(RootModulePath) diff --git a/terraform/context_test.go b/terraform/context_test.go index 65efb168a..073d8b7ec 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -2846,13 +2846,12 @@ func TestContext2Apply_badDiff(t *testing.T) { } } -/* -func TestContextApply_cancel(t *testing.T) { +func TestContext2Apply_cancel(t *testing.T) { stopped := false m := testModule(t, "apply-cancel") p := testProvider("aws") - ctx := testContext(t, &ContextOpts{ + ctx := testContext2(t, &ContextOpts{ Module: m, Providers: map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -2907,7 +2906,7 @@ func TestContextApply_cancel(t *testing.T) { mod := state.RootModule() if len(mod.Resources) != 1 { - t.Fatalf("bad: %#v", mod.Resources) + t.Fatalf("bad: %s", state.String()) } actual := strings.TrimSpace(state.String()) @@ -2916,7 +2915,6 @@ func TestContextApply_cancel(t *testing.T) { t.Fatalf("bad: \n%s", actual) } } -*/ func TestContext2Apply_compute(t *testing.T) { m := testModule(t, "apply-compute") diff --git a/terraform/eval.go b/terraform/eval.go index f60e48284..875e3440a 100644 --- a/terraform/eval.go +++ b/terraform/eval.go @@ -36,10 +36,23 @@ func (EvalEarlyExitError) Error() string { return "early exit" } // Eval evaluates the given EvalNode with the given context, properly // evaluating all args in the correct order. func Eval(n EvalNode, ctx EvalContext) (interface{}, error) { + // Call the lower level eval which doesn't understand early exit, + // and if we early exit, it isn't an error. + result, err := eval(n, ctx) + if err != nil { + if _, ok := err.(EvalEarlyExitError); ok { + return nil, nil + } + } + + return result, err +} + +func eval(n EvalNode, ctx EvalContext) (interface{}, error) { argNodes, _ := n.Args() args := make([]interface{}, len(argNodes)) for i, n := range argNodes { - v, err := Eval(n, ctx) + v, err := eval(n, ctx) if err != nil { return nil, err } diff --git a/terraform/eval_apply.go b/terraform/eval_apply.go index 06b0ff824..be954b305 100644 --- a/terraform/eval_apply.go +++ b/terraform/eval_apply.go @@ -43,7 +43,7 @@ func (n *EvalApply) Eval( } } - /* + { // Call pre-apply hook err := ctx.Hook(func(h Hook) (HookAction, error) { return h.PreApply(n.Info, state, diff) @@ -51,7 +51,7 @@ func (n *EvalApply) Eval( if err != nil { return nil, err } - */ + } // With the completed diff, apply! log.Printf("[DEBUG] apply: %s: executing Apply", n.Info.Id) diff --git a/terraform/eval_context_builtin.go b/terraform/eval_context_builtin.go index 866297c7c..ea664f2fe 100644 --- a/terraform/eval_context_builtin.go +++ b/terraform/eval_context_builtin.go @@ -2,6 +2,7 @@ package terraform import ( "fmt" + "log" "sync" "github.com/hashicorp/terraform/config" @@ -40,6 +41,7 @@ func (ctx *BuiltinEvalContext) Hook(fn func(Hook) (HookAction, error)) error { continue case HookActionHalt: // Return an early exit error to trigger an early exit + log.Printf("[WARN] Early exit triggered by hook: %T", h) return EvalEarlyExitError{} } }