terraform: early exit and cancellation
This commit is contained in:
parent
d0c77d268a
commit
10e82375f2
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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{}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue