Merge pull request #11329 from hashicorp/f-destroy-prov

Destroy Provisioners
This commit is contained in:
Mitchell Hashimoto 2017-01-26 14:32:21 -08:00 committed by GitHub
commit b35b263015
32 changed files with 1034 additions and 51 deletions

View File

@ -136,6 +136,9 @@ type Provisioner struct {
Type string
RawConfig *RawConfig
ConnInfo *RawConfig
When ProvisionerWhen
OnFailure ProvisionerOnFailure
}
// Copy returns a copy of this Provisioner
@ -144,6 +147,8 @@ func (p *Provisioner) Copy() *Provisioner {
Type: p.Type,
RawConfig: p.RawConfig.Copy(),
ConnInfo: p.ConnInfo.Copy(),
When: p.When,
OnFailure: p.OnFailure,
}
}
@ -553,7 +558,7 @@ func (c *Config) Validate() error {
// Validate DependsOn
errs = append(errs, c.validateDependsOn(n, r.DependsOn, resources, modules)...)
// Verify provisioners don't contain any splats
// Verify provisioners
for _, p := range r.Provisioners {
// This validation checks that there are now splat variables
// referencing ourself. This currently is not allowed.
@ -585,6 +590,17 @@ func (c *Config) Validate() error {
break
}
}
// Check for invalid when/onFailure values, though this should be
// picked up by the loader we check here just in case.
if p.When == ProvisionerWhenInvalid {
errs = append(errs, fmt.Errorf(
"%s: provisioner 'when' value is invalid", n))
}
if p.OnFailure == ProvisionerOnFailureInvalid {
errs = append(errs, fmt.Errorf(
"%s: provisioner 'on_failure' value is invalid", n))
}
}
// Verify ignore_changes contains valid entries

View File

@ -214,7 +214,16 @@ func resourcesStr(rs []*Resource) string {
if len(r.Provisioners) > 0 {
result += fmt.Sprintf(" provisioners\n")
for _, p := range r.Provisioners {
result += fmt.Sprintf(" %s\n", p.Type)
when := ""
if p.When != ProvisionerWhenCreate {
when = fmt.Sprintf(" (%s)", p.When.String())
}
result += fmt.Sprintf(" %s%s\n", p.Type, when)
if p.OnFailure != ProvisionerOnFailureFail {
result += fmt.Sprintf(" on_failure = %s\n", p.OnFailure.String())
}
ks := make([]string, 0, len(p.RawConfig.Raw))
for k, _ := range p.RawConfig.Raw {

View File

@ -168,6 +168,13 @@ func TestConfigValidate_table(t *testing.T) {
true,
"data sources cannot have",
},
{
"basic provisioners",
"validate-basic-provisioners",
false,
"",
},
}
for i, tc := range cases {

View File

@ -849,8 +849,40 @@ func loadProvisionersHcl(list *ast.ObjectList, connInfo map[string]interface{})
return nil, err
}
// Delete the "connection" section, handle separately
// Parse the "when" value
when := ProvisionerWhenCreate
if v, ok := config["when"]; ok {
switch v {
case "create":
when = ProvisionerWhenCreate
case "destroy":
when = ProvisionerWhenDestroy
default:
return nil, fmt.Errorf(
"position %s: 'provisioner' when must be 'create' or 'destroy'",
item.Pos())
}
}
// Parse the "on_failure" value
onFailure := ProvisionerOnFailureFail
if v, ok := config["on_failure"]; ok {
switch v {
case "continue":
onFailure = ProvisionerOnFailureContinue
case "fail":
onFailure = ProvisionerOnFailureFail
default:
return nil, fmt.Errorf(
"position %s: 'provisioner' on_failure must be 'continue' or 'fail'",
item.Pos())
}
}
// Delete fields we special case
delete(config, "connection")
delete(config, "when")
delete(config, "on_failure")
rawConfig, err := NewRawConfig(config)
if err != nil {
@ -889,6 +921,8 @@ func loadProvisionersHcl(list *ast.ObjectList, connInfo map[string]interface{})
Type: n,
RawConfig: rawConfig,
ConnInfo: connRaw,
When: when,
OnFailure: onFailure,
})
}

View File

@ -629,6 +629,22 @@ func TestLoadFile_provisioners(t *testing.T) {
}
}
func TestLoadFile_provisionersDestroy(t *testing.T) {
c, err := LoadFile(filepath.Join(fixtureDir, "provisioners-destroy.tf"))
if err != nil {
t.Fatalf("err: %s", err)
}
if c == nil {
t.Fatal("config should not be nil")
}
actual := resourcesStr(c.Resources)
if actual != strings.TrimSpace(provisionerDestroyResourcesStr) {
t.Fatalf("bad:\n%s", actual)
}
}
func TestLoadFile_unnamedOutput(t *testing.T) {
_, err := LoadFile(filepath.Join(fixtureDir, "output-unnamed.tf"))
if err == nil {
@ -1126,6 +1142,17 @@ aws_instance.web (x1)
user: var.foo
`
const provisionerDestroyResourcesStr = `
aws_instance.web (x1)
provisioners
shell
shell (destroy)
path
shell (destroy)
on_failure = continue
path
`
const connectionResourcesStr = `
aws_instance.web (x1)
ami

View File

@ -0,0 +1,40 @@
package config
// ProvisionerWhen is an enum for valid values for when to run provisioners.
type ProvisionerWhen int
const (
ProvisionerWhenInvalid ProvisionerWhen = iota
ProvisionerWhenCreate
ProvisionerWhenDestroy
)
var provisionerWhenStrs = map[ProvisionerWhen]string{
ProvisionerWhenInvalid: "invalid",
ProvisionerWhenCreate: "create",
ProvisionerWhenDestroy: "destroy",
}
func (v ProvisionerWhen) String() string {
return provisionerWhenStrs[v]
}
// ProvisionerOnFailure is an enum for valid values for on_failure options
// for provisioners.
type ProvisionerOnFailure int
const (
ProvisionerOnFailureInvalid ProvisionerOnFailure = iota
ProvisionerOnFailureContinue
ProvisionerOnFailureFail
)
var provisionerOnFailureStrs = map[ProvisionerOnFailure]string{
ProvisionerOnFailureInvalid: "invalid",
ProvisionerOnFailureContinue: "continue",
ProvisionerOnFailureFail: "fail",
}
func (v ProvisionerOnFailure) String() string {
return provisionerOnFailureStrs[v]
}

View File

@ -0,0 +1,14 @@
resource "aws_instance" "web" {
provisioner "shell" {}
provisioner "shell" {
path = "foo"
when = "destroy"
}
provisioner "shell" {
path = "foo"
when = "destroy"
on_failure = "continue"
}
}

View File

@ -0,0 +1,14 @@
resource "aws_instance" "web" {
provisioner "shell" {}
provisioner "shell" {
path = "foo"
when = "destroy"
}
provisioner "shell" {
path = "foo"
when = "destroy"
on_failure = "continue"
}
}

View File

@ -3998,6 +3998,432 @@ aws_instance.web:
`)
}
// Verify that a normal provisioner with on_failure "continue" set won't
// taint the resource and continues executing.
func TestContext2Apply_provisionerFailContinue(t *testing.T) {
m := testModule(t, "apply-provisioner-fail-continue")
p := testProvider("aws")
pr := testProvisioner()
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
pr.ApplyFn = func(rs *InstanceState, c *ResourceConfig) error {
return fmt.Errorf("provisioner error")
}
ctx := testContext2(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
Provisioners: map[string]ResourceProvisionerFactory{
"shell": testProvisionerFuncFixed(pr),
},
})
if _, err := ctx.Plan(); err != nil {
t.Fatalf("err: %s", err)
}
state, err := ctx.Apply()
if err != nil {
t.Fatalf("err: %s", err)
}
checkStateString(t, state, `
aws_instance.foo:
ID = foo
foo = bar
type = aws_instance
`)
// Verify apply was invoked
if !pr.ApplyCalled {
t.Fatalf("provisioner not invoked")
}
}
// Verify that a normal provisioner with on_failure "continue" records
// the error with the hook.
func TestContext2Apply_provisionerFailContinueHook(t *testing.T) {
h := new(MockHook)
m := testModule(t, "apply-provisioner-fail-continue")
p := testProvider("aws")
pr := testProvisioner()
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
pr.ApplyFn = func(rs *InstanceState, c *ResourceConfig) error {
return fmt.Errorf("provisioner error")
}
ctx := testContext2(t, &ContextOpts{
Module: m,
Hooks: []Hook{h},
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
Provisioners: map[string]ResourceProvisionerFactory{
"shell": testProvisionerFuncFixed(pr),
},
})
if _, err := ctx.Plan(); err != nil {
t.Fatalf("err: %s", err)
}
if _, err := ctx.Apply(); err != nil {
t.Fatalf("err: %s", err)
}
if !h.PostProvisionCalled {
t.Fatal("PostProvision not called")
}
if h.PostProvisionErrorArg == nil {
t.Fatal("should have error")
}
}
func TestContext2Apply_provisionerDestroy(t *testing.T) {
m := testModule(t, "apply-provisioner-destroy")
p := testProvider("aws")
pr := testProvisioner()
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
pr.ApplyFn = func(rs *InstanceState, c *ResourceConfig) error {
val, ok := c.Config["foo"]
if !ok || val != "destroy" {
t.Fatalf("bad value for foo: %v %#v", val, c)
}
return nil
}
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.foo": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "bar",
},
},
},
},
},
}
ctx := testContext2(t, &ContextOpts{
Module: m,
State: state,
Destroy: true,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
Provisioners: map[string]ResourceProvisionerFactory{
"shell": testProvisionerFuncFixed(pr),
},
})
if _, err := ctx.Plan(); err != nil {
t.Fatalf("err: %s", err)
}
state, err := ctx.Apply()
if err != nil {
t.Fatalf("err: %s", err)
}
checkStateString(t, state, `<no state>`)
// Verify apply was invoked
if !pr.ApplyCalled {
t.Fatalf("provisioner not invoked")
}
}
// Verify that on destroy provisioner failure, nothing happens to the instance
func TestContext2Apply_provisionerDestroyFail(t *testing.T) {
m := testModule(t, "apply-provisioner-destroy")
p := testProvider("aws")
pr := testProvisioner()
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
pr.ApplyFn = func(rs *InstanceState, c *ResourceConfig) error {
return fmt.Errorf("provisioner error")
}
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.foo": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "bar",
},
},
},
},
},
}
ctx := testContext2(t, &ContextOpts{
Module: m,
State: state,
Destroy: true,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
Provisioners: map[string]ResourceProvisionerFactory{
"shell": testProvisionerFuncFixed(pr),
},
})
if _, err := ctx.Plan(); err != nil {
t.Fatalf("err: %s", err)
}
state, err := ctx.Apply()
if err == nil {
t.Fatal("should error")
}
checkStateString(t, state, `
aws_instance.foo:
ID = bar
`)
// Verify apply was invoked
if !pr.ApplyCalled {
t.Fatalf("provisioner not invoked")
}
}
// Verify that on destroy provisioner failure with "continue" that
// we continue to the next provisioner.
func TestContext2Apply_provisionerDestroyFailContinue(t *testing.T) {
m := testModule(t, "apply-provisioner-destroy-continue")
p := testProvider("aws")
pr := testProvisioner()
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
var calls []string
pr.ApplyFn = func(rs *InstanceState, c *ResourceConfig) error {
val, ok := c.Config["foo"]
if !ok {
t.Fatalf("bad value for foo: %v %#v", val, c)
}
calls = append(calls, val.(string))
return fmt.Errorf("provisioner error")
}
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.foo": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "bar",
},
},
},
},
},
}
ctx := testContext2(t, &ContextOpts{
Module: m,
State: state,
Destroy: true,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
Provisioners: map[string]ResourceProvisionerFactory{
"shell": testProvisionerFuncFixed(pr),
},
})
if _, err := ctx.Plan(); err != nil {
t.Fatalf("err: %s", err)
}
state, err := ctx.Apply()
if err != nil {
t.Fatalf("err: %s", err)
}
checkStateString(t, state, `<no state>`)
// Verify apply was invoked
if !pr.ApplyCalled {
t.Fatalf("provisioner not invoked")
}
expected := []string{"one", "two"}
if !reflect.DeepEqual(calls, expected) {
t.Fatalf("bad: %#v", calls)
}
}
// Verify that on destroy provisioner failure with "continue" that
// we continue to the next provisioner. But if the next provisioner defines
// to fail, then we fail after running it.
func TestContext2Apply_provisionerDestroyFailContinueFail(t *testing.T) {
m := testModule(t, "apply-provisioner-destroy-fail")
p := testProvider("aws")
pr := testProvisioner()
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
var calls []string
pr.ApplyFn = func(rs *InstanceState, c *ResourceConfig) error {
val, ok := c.Config["foo"]
if !ok {
t.Fatalf("bad value for foo: %v %#v", val, c)
}
calls = append(calls, val.(string))
return fmt.Errorf("provisioner error")
}
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.foo": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "bar",
},
},
},
},
},
}
ctx := testContext2(t, &ContextOpts{
Module: m,
State: state,
Destroy: true,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
Provisioners: map[string]ResourceProvisionerFactory{
"shell": testProvisionerFuncFixed(pr),
},
})
if _, err := ctx.Plan(); err != nil {
t.Fatalf("err: %s", err)
}
state, err := ctx.Apply()
if err == nil {
t.Fatal("should error")
}
checkStateString(t, state, `
aws_instance.foo:
ID = bar
`)
// Verify apply was invoked
if !pr.ApplyCalled {
t.Fatalf("provisioner not invoked")
}
expected := []string{"one", "two"}
if !reflect.DeepEqual(calls, expected) {
t.Fatalf("bad: %#v", calls)
}
}
// Verify destroy provisioners are not run for tainted instances.
func TestContext2Apply_provisionerDestroyTainted(t *testing.T) {
m := testModule(t, "apply-provisioner-destroy")
p := testProvider("aws")
pr := testProvisioner()
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
destroyCalled := false
pr.ApplyFn = func(rs *InstanceState, c *ResourceConfig) error {
expected := "create"
if rs.ID == "bar" {
destroyCalled = true
return nil
}
val, ok := c.Config["foo"]
if !ok || val != expected {
t.Fatalf("bad value for foo: %v %#v", val, c)
}
return nil
}
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.foo": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "bar",
Tainted: true,
},
},
},
},
},
}
ctx := testContext2(t, &ContextOpts{
Module: m,
State: state,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
Provisioners: map[string]ResourceProvisionerFactory{
"shell": testProvisionerFuncFixed(pr),
},
})
if _, err := ctx.Plan(); err != nil {
t.Fatalf("err: %s", err)
}
state, err := ctx.Apply()
if err != nil {
t.Fatalf("err: %s", err)
}
checkStateString(t, state, `
aws_instance.foo:
ID = foo
foo = bar
type = aws_instance
`)
// Verify apply was invoked
if !pr.ApplyCalled {
t.Fatalf("provisioner not invoked")
}
if destroyCalled {
t.Fatal("destroy should not be called")
}
}
func TestContext2Apply_provisionerResourceRef(t *testing.T) {
m := testModule(t, "apply-provisioner-resource-ref")
p := testProvider("aws")

View File

@ -413,7 +413,7 @@ func (*DebugHook) PreProvision(ii *InstanceInfo, s string) (HookAction, error) {
return HookActionContinue, nil
}
func (*DebugHook) PostProvision(ii *InstanceInfo, s string) (HookAction, error) {
func (*DebugHook) PostProvision(ii *InstanceInfo, s string, err error) (HookAction, error) {
if dbug == nil {
return HookActionContinue, nil
}

View File

@ -156,7 +156,7 @@ func TestDebugHook_nilArgs(t *testing.T) {
h.PostApply(nil, nil, nil)
h.PostDiff(nil, nil)
h.PostImportState(nil, nil)
h.PostProvision(nil, "")
h.PostProvision(nil, "", nil)
h.PostProvisionResource(nil, nil)
h.PostRefresh(nil, nil)
h.PostStateUpdate(nil)

View File

@ -52,16 +52,6 @@ func (n *EvalApply) Eval(ctx EvalContext) (interface{}, error) {
*n.CreateNew = state.ID == "" && !diff.GetDestroy() || diff.RequiresNew()
}
{
// Call pre-apply hook
err := ctx.Hook(func(h Hook) (HookAction, error) {
return h.PreApply(n.Info, state, diff)
})
if err != nil {
return nil, err
}
}
// With the completed diff, apply!
log.Printf("[DEBUG] apply: %s: executing Apply", n.Info.Id)
state, err := provider.Apply(n.Info, state, diff)
@ -104,6 +94,37 @@ func (n *EvalApply) Eval(ctx EvalContext) (interface{}, error) {
return nil, nil
}
// EvalApplyPre is an EvalNode implementation that does the pre-Apply work
type EvalApplyPre struct {
Info *InstanceInfo
State **InstanceState
Diff **InstanceDiff
}
// TODO: test
func (n *EvalApplyPre) Eval(ctx EvalContext) (interface{}, error) {
state := *n.State
diff := *n.Diff
// If the state is nil, make it non-nil
if state == nil {
state = new(InstanceState)
}
state.init()
{
// Call post-apply hook
err := ctx.Hook(func(h Hook) (HookAction, error) {
return h.PreApply(n.Info, state, diff)
})
if err != nil {
return nil, err
}
}
return nil, nil
}
// EvalApplyPost is an EvalNode implementation that does the post-Apply work
type EvalApplyPost struct {
Info *InstanceInfo
@ -140,25 +161,33 @@ type EvalApplyProvisioners struct {
InterpResource *Resource
CreateNew *bool
Error *error
// When is the type of provisioner to run at this point
When config.ProvisionerWhen
}
// TODO: test
func (n *EvalApplyProvisioners) Eval(ctx EvalContext) (interface{}, error) {
state := *n.State
if !*n.CreateNew {
if n.CreateNew != nil && !*n.CreateNew {
// If we're not creating a new resource, then don't run provisioners
return nil, nil
}
if len(n.Resource.Provisioners) == 0 {
provs := n.filterProvisioners()
if len(provs) == 0 {
// We have no provisioners, so don't do anything
return nil, nil
}
// taint tells us whether to enable tainting.
taint := n.When == config.ProvisionerWhenCreate
if n.Error != nil && *n.Error != nil {
// We're already errored creating, so mark as tainted and continue
state.Tainted = true
if taint {
state.Tainted = true
}
// We're already tainted, so just return out
return nil, nil
@ -176,10 +205,11 @@ func (n *EvalApplyProvisioners) Eval(ctx EvalContext) (interface{}, error) {
// If there are no errors, then we append it to our output error
// if we have one, otherwise we just output it.
err := n.apply(ctx)
err := n.apply(ctx, provs)
if err != nil {
// Provisioning failed, so mark the resource as tainted
state.Tainted = true
if taint {
state.Tainted = true
}
if n.Error != nil {
*n.Error = multierror.Append(*n.Error, err)
@ -201,7 +231,29 @@ func (n *EvalApplyProvisioners) Eval(ctx EvalContext) (interface{}, error) {
return nil, nil
}
func (n *EvalApplyProvisioners) apply(ctx EvalContext) error {
// filterProvisioners filters the provisioners on the resource to only
// the provisioners specified by the "when" option.
func (n *EvalApplyProvisioners) filterProvisioners() []*config.Provisioner {
// Fast path the zero case
if n.Resource == nil {
return nil
}
if len(n.Resource.Provisioners) == 0 {
return nil
}
result := make([]*config.Provisioner, 0, len(n.Resource.Provisioners))
for _, p := range n.Resource.Provisioners {
if p.When == n.When {
result = append(result, p)
}
}
return result
}
func (n *EvalApplyProvisioners) apply(ctx EvalContext, provs []*config.Provisioner) error {
state := *n.State
// Store the original connection info, restore later
@ -210,7 +262,7 @@ func (n *EvalApplyProvisioners) apply(ctx EvalContext) error {
state.Ephemeral.ConnInfo = origConnInfo
}()
for _, prov := range n.Resource.Provisioners {
for _, prov := range provs {
// Get the provisioner
provisioner := ctx.Provisioner(prov.Type)
@ -275,18 +327,30 @@ func (n *EvalApplyProvisioners) apply(ctx EvalContext) error {
// Invoke the Provisioner
output := CallbackUIOutput{OutputFn: outputFn}
if err := provisioner.Apply(&output, state, provConfig); err != nil {
return err
applyErr := provisioner.Apply(&output, state, provConfig)
// Call post hook
hookErr := ctx.Hook(func(h Hook) (HookAction, error) {
return h.PostProvision(n.Info, prov.Type, applyErr)
})
// Handle the error before we deal with the hook
if applyErr != nil {
// Determine failure behavior
switch prov.OnFailure {
case config.ProvisionerOnFailureContinue:
log.Printf(
"[INFO] apply: %s [%s]: error during provision, continue requested",
n.Info.Id, prov.Type)
case config.ProvisionerOnFailureFail:
return applyErr
}
}
{
// Call post hook
err := ctx.Hook(func(h Hook) (HookAction, error) {
return h.PostProvision(n.Info, prov.Type)
})
if err != nil {
return err
}
// Deal with the hook
if hookErr != nil {
return hookErr
}
}

View File

@ -96,13 +96,8 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer {
),
// Provisioner-related transformations
GraphTransformIf(
func() bool { return !b.Destroy },
GraphTransformMulti(
&MissingProvisionerTransformer{Provisioners: b.Provisioners},
&ProvisionerTransformer{},
),
),
&MissingProvisionerTransformer{Provisioners: b.Provisioners},
&ProvisionerTransformer{},
// Add root variables
&RootVariableTransformer{Module: b.Module},

View File

@ -232,6 +232,78 @@ func TestApplyGraphBuilder_moduleDestroy(t *testing.T) {
"module.A.null_resource.foo (destroy)")
}
func TestApplyGraphBuilder_provisioner(t *testing.T) {
diff := &Diff{
Modules: []*ModuleDiff{
&ModuleDiff{
Path: []string{"root"},
Resources: map[string]*InstanceDiff{
"null_resource.foo": &InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"name": &ResourceAttrDiff{
Old: "",
New: "foo",
},
},
},
},
},
},
}
b := &ApplyGraphBuilder{
Module: testModule(t, "graph-builder-apply-provisioner"),
Diff: diff,
Providers: []string{"null"},
Provisioners: []string{"local"},
}
g, err := b.Build(RootModulePath)
if err != nil {
t.Fatalf("err: %s", err)
}
testGraphContains(t, g, "provisioner.local")
testGraphHappensBefore(
t, g,
"provisioner.local",
"null_resource.foo")
}
func TestApplyGraphBuilder_provisionerDestroy(t *testing.T) {
diff := &Diff{
Modules: []*ModuleDiff{
&ModuleDiff{
Path: []string{"root"},
Resources: map[string]*InstanceDiff{
"null_resource.foo": &InstanceDiff{
Destroy: true,
},
},
},
},
}
b := &ApplyGraphBuilder{
Destroy: true,
Module: testModule(t, "graph-builder-apply-provisioner"),
Diff: diff,
Providers: []string{"null"},
Provisioners: []string{"local"},
}
g, err := b.Build(RootModulePath)
if err != nil {
t.Fatalf("err: %s", err)
}
testGraphContains(t, g, "provisioner.local")
testGraphHappensBefore(
t, g,
"provisioner.local",
"null_resource.foo (destroy)")
}
const testApplyGraphBuilderStr = `
aws_instance.create
provider.aws

View File

@ -88,6 +88,32 @@ func TestGraphWalk_panicWrap(t *testing.T) {
}
}
// testGraphContains is an assertion helper that tests that a node is
// contained in the graph.
func testGraphContains(t *testing.T, g *Graph, name string) {
for _, v := range g.Vertices() {
if dag.VertexName(v) == name {
return
}
}
t.Fatalf(
"Expected %q in:\n\n%s",
name, g.String())
}
// testGraphnotContains is an assertion helper that tests that a node is
// NOT contained in the graph.
func testGraphNotContains(t *testing.T, g *Graph, name string) {
for _, v := range g.Vertices() {
if dag.VertexName(v) == name {
t.Fatalf(
"Expected %q to NOT be in:\n\n%s",
name, g.String())
}
}
}
// testGraphHappensBefore is an assertion helper that tests that node
// A (dag.VertexName value) happens before node B.
func testGraphHappensBefore(t *testing.T, g *Graph, A, B string) {

View File

@ -42,7 +42,7 @@ type Hook interface {
PreProvisionResource(*InstanceInfo, *InstanceState) (HookAction, error)
PostProvisionResource(*InstanceInfo, *InstanceState) (HookAction, error)
PreProvision(*InstanceInfo, string) (HookAction, error)
PostProvision(*InstanceInfo, string) (HookAction, error)
PostProvision(*InstanceInfo, string, error) (HookAction, error)
ProvisionOutput(*InstanceInfo, string, string)
// PreRefresh and PostRefresh are called before and after a single
@ -92,7 +92,7 @@ func (*NilHook) PreProvision(*InstanceInfo, string) (HookAction, error) {
return HookActionContinue, nil
}
func (*NilHook) PostProvision(*InstanceInfo, string) (HookAction, error) {
func (*NilHook) PostProvision(*InstanceInfo, string, error) (HookAction, error) {
return HookActionContinue, nil
}

View File

@ -55,6 +55,7 @@ type MockHook struct {
PostProvisionCalled bool
PostProvisionInfo *InstanceInfo
PostProvisionProvisionerId string
PostProvisionErrorArg error
PostProvisionReturn HookAction
PostProvisionError error
@ -170,13 +171,14 @@ func (h *MockHook) PreProvision(n *InstanceInfo, provId string) (HookAction, err
return h.PreProvisionReturn, h.PreProvisionError
}
func (h *MockHook) PostProvision(n *InstanceInfo, provId string) (HookAction, error) {
func (h *MockHook) PostProvision(n *InstanceInfo, provId string, err error) (HookAction, error) {
h.Lock()
defer h.Unlock()
h.PostProvisionCalled = true
h.PostProvisionInfo = n
h.PostProvisionProvisionerId = provId
h.PostProvisionErrorArg = err
return h.PostProvisionReturn, h.PostProvisionError
}

View File

@ -38,7 +38,7 @@ func (h *stopHook) PreProvision(*InstanceInfo, string) (HookAction, error) {
return h.hook()
}
func (h *stopHook) PostProvision(*InstanceInfo, string) (HookAction, error) {
func (h *stopHook) PostProvision(*InstanceInfo, string, error) (HookAction, error) {
return h.hook()
}

View File

@ -298,6 +298,12 @@ func (n *NodeApplyableResource) evalTreeManagedResource(
Name: stateId,
Output: &state,
},
// Call pre-apply hook
&EvalApplyPre{
Info: info,
State: &state,
Diff: &diffApply,
},
&EvalApply{
Info: info,
State: &state,
@ -321,6 +327,7 @@ func (n *NodeApplyableResource) evalTreeManagedResource(
InterpResource: resource,
CreateNew: &createNew,
Error: &err,
When: config.ProvisionerWhenCreate,
},
&EvalIf{
If: func(ctx EvalContext) (bool, error) {

View File

@ -107,6 +107,17 @@ func (n *NodeDestroyResource) EvalTree() EvalNode {
uniqueExtra: "destroy",
}
// Build the resource for eval
addr := n.Addr
resource := &Resource{
Name: addr.Name,
Type: addr.Type,
CountIndex: addr.Index,
}
if resource.CountIndex < 0 {
resource.CountIndex = 0
}
// Get our state
rs := n.ResourceState
if rs == nil {
@ -160,6 +171,48 @@ func (n *NodeDestroyResource) EvalTree() EvalNode {
&EvalRequireState{
State: &state,
},
// Call pre-apply hook
&EvalApplyPre{
Info: info,
State: &state,
Diff: &diffApply,
},
// Run destroy provisioners if not tainted
&EvalIf{
If: func(ctx EvalContext) (bool, error) {
if state != nil && state.Tainted {
return false, nil
}
return true, nil
},
Then: &EvalApplyProvisioners{
Info: info,
State: &state,
Resource: n.Config,
InterpResource: resource,
Error: &err,
When: config.ProvisionerWhenDestroy,
},
},
// If we have a provisioning error, then we just call
// the post-apply hook now.
&EvalIf{
If: func(ctx EvalContext) (bool, error) {
return err != nil, nil
},
Then: &EvalApplyPost{
Info: info,
State: &state,
Error: &err,
},
},
// Make sure we handle data sources properly.
&EvalIf{
If: func(ctx EvalContext) (bool, error) {

View File

@ -179,6 +179,10 @@ func (h *HookRecordApplyOrder) PreApply(
info *InstanceInfo,
s *InstanceState,
d *InstanceDiff) (HookAction, error) {
if d.Empty() {
return HookActionContinue, nil
}
if h.Active {
h.l.Lock()
defer h.l.Unlock()

View File

@ -0,0 +1,15 @@
resource "aws_instance" "foo" {
foo = "bar"
provisioner "shell" {
foo = "one"
when = "destroy"
on_failure = "continue"
}
provisioner "shell" {
foo = "two"
when = "destroy"
on_failure = "continue"
}
}

View File

@ -0,0 +1,14 @@
resource "aws_instance" "foo" {
foo = "bar"
provisioner "shell" {
foo = "one"
when = "destroy"
on_failure = "continue"
}
provisioner "shell" {
foo = "two"
when = "destroy"
}
}

View File

@ -0,0 +1,12 @@
resource "aws_instance" "foo" {
foo = "bar"
provisioner "shell" {
foo = "create"
}
provisioner "shell" {
foo = "destroy"
when = "destroy"
}
}

View File

@ -0,0 +1,7 @@
resource "aws_instance" "foo" {
foo = "bar"
provisioner "shell" {
on_failure = "continue"
}
}

View File

@ -0,0 +1,3 @@
resource "null_resource" "foo" {
provisioner "local" {}
}

View File

@ -127,6 +127,12 @@ func (n *graphNodeDeposedResource) EvalTree() EvalNode {
State: &state,
Output: &diff,
},
// Call pre-apply hook
&EvalApplyPre{
Info: info,
State: &state,
Diff: &diff,
},
&EvalApply{
Info: info,
State: &state,

View File

@ -321,6 +321,11 @@ func (n *graphNodeOrphanResource) managedResourceEvalNodes(info *InstanceInfo) [
Name: n.ResourceKey.String(),
Output: &state,
},
&EvalApplyPre{
Info: info,
State: &state,
Diff: &diff,
},
&EvalApply{
Info: info,
State: &state,

View File

@ -500,6 +500,11 @@ func (n *graphNodeExpandedResource) managedResourceEvalNodes(resource *Resource,
Name: n.stateId(),
Output: &state,
},
&EvalApplyPre{
Info: info,
State: &state,
Diff: &diffApply,
},
&EvalApply{
Info: info,
State: &state,

View File

@ -295,6 +295,9 @@ where `PROVISIONER` is:
provisioner NAME {
CONFIG ...
[when = "create"|"destroy"]
[on_failure = "continue"|"fail"]
[CONNECTION]
}
```

View File

@ -3,15 +3,104 @@ layout: "docs"
page_title: "Provisioners"
sidebar_current: "docs-provisioners"
description: |-
When a resource is initially created, provisioners can be executed to initialize that resource. This can be used to add resources to an inventory management system, run a configuration management tool, bootstrap the resource into a cluster, etc.
Provisioners are used to execute scripts on a local or remote machine as part of resource creation or destruction.
---
# Provisioners
When a resource is initially created, provisioners can be executed to
initialize that resource. This can be used to add resources to an inventory
management system, run a configuration management tool, bootstrap the
resource into a cluster, etc.
Provisioners are used to execute scripts on a local or remote machine
as part of resource creation or destruction. Provisioners can be used to
bootstrap a resource, cleanup before destroy, run configuration management, etc.
Use the navigation to the left to read about the available provisioners.
Provisioners are added directly to any resource:
```
resource "aws_instance" "web" {
# ...
provisioner "local-exec" {
command = "echo ${self.private_ip_address} > file.txt"
}
}
```
For provisioners other than local execution, you must specify
[connection settings](/docs/provisioners/connection.html) so Terraform knows
how to communicate with the resource.
## Creation-Time Provisioners
Provisioners by default run when the resource they are defined within is
created. Creation-time provisioners are only run during _creation_, not
during updating or any other lifecycle. They are meant as a means to perform
bootstrapping of a system.
If a creation-time provisioner fails, the resource is marked as **tainted**.
A tainted resource will be planned for destruction and recreation upon the
next `terraform apply`. Terraform does this because a failed provisioner
can leave a resource in a semi-configured state. Because Terraform cannot
reason about what the provisioner does, the only way to ensure proper creation
of a resource is to recreate it. This is tainting.
You can change this behavior by setting the `on_failure` attribute,
which is covered in detail below.
## Destroy-Time Provisioners
If `when = "destroy"` is specified, the provisioner will run when the
resource it is defined within is _destroyed_.
Destroy provisioners are run before the resource is destroyed. If they
fail, Terraform will error and rerun the provisioners again on the next
`terraform apply`. Due to this behavior, care should be taken for destroy
provisioners to be safe to run multiple times.
## Multiple Provisioners
Multiple provisioners can be specified within a resource. Multiple provisioners
are executed in the order they're defined in the configuration file.
You may also mix and match creation and destruction provisioners. Only
the provisioners that are valid for a given operation will be run. Those
valid provisioners will be run in the order they're defined in the configuration
file.
Example of multiple provisioners:
```
resource "aws_instance" "web" {
# ...
provisioner "local-exec" {
command = "echo first"
}
provisioner "local-exec" {
command = "echo second"
}
}
```
## Failure Behavior
By default, provisioners that fail will also cause the Terraform apply
itself to error. The `on_failure` setting can be used to change this. The
allowed values are:
* `"continue"` - Ignore the error and continue with creation or destruction.
* `"fail"` - Error (the default behavior). If this is a creation provisioner,
taint the resource.
Example:
```
resource "aws_instance" "web" {
# ...
provisioner "local-exec" {
command = "echo ${self.private_ip_address} > file.txt"
on_failure = "continue"
}
}
```

View File

@ -100,6 +100,20 @@ If you create an execution plan with a tainted resource, however, the
plan will clearly state that the resource will be destroyed because
it is tainted.
## Destroy Provisioners
Provisioners can also be defined that run only during a destroy
operation. These are useful for performing system cleanup, extracting
data, etc.
For many resources, using built-in cleanup mechanisms is recommended
if possible (such as init scripts), but provisioners can be used if
necessary.
The getting started guide won't show any destroy provisioner examples.
If you need to use destroy provisioners, please
[see the provisioner documentation](/docs/provisioners).
## Next
Provisioning is important for being able to bootstrap instances.