terraform: early exit and cancellation

This commit is contained in:
Mitchell Hashimoto 2015-02-13 09:05:09 -08:00
parent d0c77d268a
commit 10e82375f2
5 changed files with 93 additions and 9 deletions

View File

@ -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)

View File

@ -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")

View File

@ -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
}

View File

@ -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)

View File

@ -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{}
}
}