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 module *module.Tree
providers map[string]ResourceProviderFactory providers map[string]ResourceProviderFactory
provisioners map[string]ResourceProvisionerFactory provisioners map[string]ResourceProvisionerFactory
sh *stopHook
state *State state *State
stateLock sync.RWMutex stateLock sync.RWMutex
variables map[string]string variables map[string]string
l sync.Mutex // Lock acquired during any task
runCh <-chan struct{}
} }
// NewContext creates a new Context structure. // 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 // should not be mutated in any way, since the pointers are copied, not
// the values themselves. // the values themselves.
func NewContext2(opts *ContextOpts) *Context2 { 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 state := opts.State
if state == nil { if state == nil {
state = new(State) state = new(State)
@ -51,10 +62,11 @@ func NewContext2(opts *ContextOpts) *Context2 {
return &Context2{ return &Context2{
diff: opts.Diff, diff: opts.Diff,
hooks: opts.Hooks, hooks: hooks,
module: opts.Module, module: opts.Module,
providers: opts.Providers, providers: opts.Providers,
provisioners: opts.Provisioners, provisioners: opts.Provisioners,
sh: sh,
state: state, state: state,
variables: opts.Variables, variables: opts.Variables,
} }
@ -88,6 +100,9 @@ func (c *Context2) GraphBuilder() GraphBuilder {
// In addition to returning the resulting state, this context is updated // In addition to returning the resulting state, this context is updated
// with the latest state. // with the latest state.
func (c *Context2) Apply() (*State, error) { func (c *Context2) Apply() (*State, error) {
v := c.acquireRun()
defer c.releaseRun(v)
// Copy our own state // Copy our own state
c.state = c.state.deepcopy() 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 // Plan also updates the diff of this context to be the diff generated
// by the plan, so Apply can be called after. // by the plan, so Apply can be called after.
func (c *Context2) Plan(opts *PlanOpts) (*Plan, error) { func (c *Context2) Plan(opts *PlanOpts) (*Plan, error) {
v := c.acquireRun()
defer c.releaseRun(v)
p := &Plan{ p := &Plan{
Module: c.module, Module: c.module,
Vars: c.variables, 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 // Even in the case an error is returned, the state will be returned and
// will potentially be partially updated. // will potentially be partially updated.
func (c *Context2) Refresh() (*State, []error) { func (c *Context2) Refresh() (*State, []error) {
v := c.acquireRun()
defer c.releaseRun(v)
// Copy our own state // Copy our own state
c.state = c.state.deepcopy() c.state = c.state.deepcopy()
@ -172,8 +193,32 @@ func (c *Context2) Refresh() (*State, []error) {
return c.state, nil 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. // Validate validates the configuration and returns any warnings or errors.
func (c *Context2) Validate() ([]string, []error) { func (c *Context2) Validate() ([]string, []error) {
v := c.acquireRun()
defer c.releaseRun(v)
var errs error var errs error
// Validate the configuration itself // Validate the configuration itself
@ -201,6 +246,32 @@ func (c *Context2) Validate() ([]string, []error) {
return walker.ValidationWarnings, rerrs.Errors 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) { func (c *Context2) walk(operation walkOperation) (*ContextGraphWalker, error) {
// Build the graph // Build the graph
graph, err := c.GraphBuilder().Build(RootModulePath) graph, err := c.GraphBuilder().Build(RootModulePath)

View File

@ -2846,13 +2846,12 @@ func TestContext2Apply_badDiff(t *testing.T) {
} }
} }
/* func TestContext2Apply_cancel(t *testing.T) {
func TestContextApply_cancel(t *testing.T) {
stopped := false stopped := false
m := testModule(t, "apply-cancel") m := testModule(t, "apply-cancel")
p := testProvider("aws") p := testProvider("aws")
ctx := testContext(t, &ContextOpts{ ctx := testContext2(t, &ContextOpts{
Module: m, Module: m,
Providers: map[string]ResourceProviderFactory{ Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p), "aws": testProviderFuncFixed(p),
@ -2907,7 +2906,7 @@ func TestContextApply_cancel(t *testing.T) {
mod := state.RootModule() mod := state.RootModule()
if len(mod.Resources) != 1 { if len(mod.Resources) != 1 {
t.Fatalf("bad: %#v", mod.Resources) t.Fatalf("bad: %s", state.String())
} }
actual := strings.TrimSpace(state.String()) actual := strings.TrimSpace(state.String())
@ -2916,7 +2915,6 @@ func TestContextApply_cancel(t *testing.T) {
t.Fatalf("bad: \n%s", actual) t.Fatalf("bad: \n%s", actual)
} }
} }
*/
func TestContext2Apply_compute(t *testing.T) { func TestContext2Apply_compute(t *testing.T) {
m := testModule(t, "apply-compute") 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 // Eval evaluates the given EvalNode with the given context, properly
// evaluating all args in the correct order. // evaluating all args in the correct order.
func Eval(n EvalNode, ctx EvalContext) (interface{}, error) { 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() argNodes, _ := n.Args()
args := make([]interface{}, len(argNodes)) args := make([]interface{}, len(argNodes))
for i, n := range argNodes { for i, n := range argNodes {
v, err := Eval(n, ctx) v, err := eval(n, ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -43,7 +43,7 @@ func (n *EvalApply) Eval(
} }
} }
/* {
// Call pre-apply hook // Call pre-apply hook
err := ctx.Hook(func(h Hook) (HookAction, error) { err := ctx.Hook(func(h Hook) (HookAction, error) {
return h.PreApply(n.Info, state, diff) return h.PreApply(n.Info, state, diff)
@ -51,7 +51,7 @@ func (n *EvalApply) Eval(
if err != nil { if err != nil {
return nil, err return nil, err
} }
*/ }
// With the completed diff, apply! // With the completed diff, apply!
log.Printf("[DEBUG] apply: %s: executing Apply", n.Info.Id) log.Printf("[DEBUG] apply: %s: executing Apply", n.Info.Id)

View File

@ -2,6 +2,7 @@ package terraform
import ( import (
"fmt" "fmt"
"log"
"sync" "sync"
"github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config"
@ -40,6 +41,7 @@ func (ctx *BuiltinEvalContext) Hook(fn func(Hook) (HookAction, error)) error {
continue continue
case HookActionHalt: case HookActionHalt:
// Return an early exit error to trigger an early exit // Return an early exit error to trigger an early exit
log.Printf("[WARN] Early exit triggered by hook: %T", h)
return EvalEarlyExitError{} return EvalEarlyExitError{}
} }
} }