Merge pull request #317 from hashicorp/f-create-before

Adding support for `lifecycle` and `create_before_destroy`
This commit is contained in:
Mitchell Hashimoto 2014-09-30 13:17:41 -07:00
commit a621525741
18 changed files with 728 additions and 13 deletions

View File

@ -60,6 +60,13 @@ type Resource struct {
RawConfig *RawConfig
Provisioners []*Provisioner
DependsOn []string
Lifecycle ResourceLifecycle
}
// ResourceLifecycle is used to store the lifecycle tuning parameters
// to allow customized behavior
type ResourceLifecycle struct {
CreateBeforeDestroy bool `hcl:"create_before_destroy"`
}
// Provisioner is a configured provisioner step on a resource.

View File

@ -394,6 +394,7 @@ func loadResourcesHcl(os *hclobj.Object) ([]*Resource, error) {
delete(config, "count")
delete(config, "depends_on")
delete(config, "provisioner")
delete(config, "lifecycle")
rawConfig, err := NewRawConfig(config)
if err != nil {
@ -457,6 +458,20 @@ func loadResourcesHcl(os *hclobj.Object) ([]*Resource, error) {
}
}
// Check if the resource should be re-created before
// destroying the existing instance
var lifecycle ResourceLifecycle
if o := obj.Get("lifecycle", false); o != nil {
err = hcl.DecodeObject(&lifecycle, o)
if err != nil {
return nil, fmt.Errorf(
"Error parsing lifecycle for %s[%s]: %s",
t.Key,
k,
err)
}
}
result = append(result, &Resource{
Name: k,
Type: t.Key,
@ -464,6 +479,7 @@ func loadResourcesHcl(os *hclobj.Object) ([]*Resource, error) {
RawConfig: rawConfig,
Provisioners: provisioners,
DependsOn: dependsOn,
Lifecycle: lifecycle,
})
}
}

View File

@ -346,6 +346,43 @@ func TestLoad_connections(t *testing.T) {
}
}
func TestLoad_createBeforeDestroy(t *testing.T) {
c, err := Load(filepath.Join(fixtureDir, "create-before-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(createBeforeDestroyResourcesStr) {
t.Fatalf("bad:\n%s", actual)
}
// Check for the flag value
r := c.Resources[0]
if r.Name != "web" && r.Type != "aws_instance" {
t.Fatalf("Bad: %#v", r)
}
// Should enable create before destroy
if !r.Lifecycle.CreateBeforeDestroy {
t.Fatalf("Bad: %#v", r)
}
r = c.Resources[1]
if r.Name != "bar" && r.Type != "aws_instance" {
t.Fatalf("Bad: %#v", r)
}
// Should not enable create before destroy
if r.Lifecycle.CreateBeforeDestroy {
t.Fatalf("Bad: %#v", r)
}
}
const basicOutputsStr = `
web_ip
vars
@ -523,3 +560,10 @@ foo (required)
<>
<>
`
const createBeforeDestroyResourcesStr = `
aws_instance[bar] (x1)
ami
aws_instance[web] (x1)
ami
`

View File

@ -0,0 +1,14 @@
resource "aws_instance" "web" {
ami = "foo"
lifecycle {
create_before_destroy = true
}
}
resource "aws_instance" "bar" {
ami = "foo"
lifecycle {
create_before_destroy = false
}
}

View File

@ -357,3 +357,23 @@ func (g *Graph) Walk(fn WalkFunc) error {
return err
}
}
// DependsOn returns the set of nouns that have a
// dependency on a given noun. This can be used to find
// the incoming edges to a noun.
func (g *Graph) DependsOn(n *Noun) []*Noun {
var incoming []*Noun
OUTER:
for _, other := range g.Nouns {
if other == n {
continue
}
for _, d := range other.Deps {
if d.Target == n {
incoming = append(incoming, other)
continue OUTER
}
}
}
return incoming
}

View File

@ -429,3 +429,39 @@ g -> h`)
}
}
}
func TestGraph_DependsOn(t *testing.T) {
nodes := ParseNouns(`a -> b
a -> c
b -> d
b -> e
c -> d
c -> e`)
g := &Graph{
Name: "Test",
Nouns: NounMapToList(nodes),
}
dNoun := g.Noun("d")
incoming := g.DependsOn(dNoun)
if len(incoming) != 2 {
t.Fatalf("bad: %#v", incoming)
}
var hasB, hasC bool
for _, in := range incoming {
switch in.Name {
case "b":
hasB = true
case "c":
hasC = true
default:
t.Fatalf("Bad: %#v", in)
}
}
if !hasB || !hasC {
t.Fatalf("missing incoming edge")
}
}

View File

@ -1264,17 +1264,46 @@ func (c *walkContext) persistState(r *Resource) {
rs.Dependencies = r.Dependencies
// Assign the instance state to the proper location
if r.Flags&FlagTainted != 0 {
if r.Flags&FlagDeposed != 0 {
// We were previously the primary and have been deposed, so
// now we are the final tainted resource
r.TaintedIndex = len(rs.Tainted) - 1
rs.Tainted[r.TaintedIndex] = r.State
} else if r.Flags&FlagTainted != 0 {
if r.TaintedIndex >= 0 {
// Tainted with a pre-existing index, just update that spot
rs.Tainted[r.TaintedIndex] = r.State
} else if r.Flags&FlagReplacePrimary != 0 {
// We just replaced the primary, so restore the primary
rs.Primary = rs.Tainted[len(rs.Tainted)-1]
// Set ourselves as tainted
rs.Tainted[len(rs.Tainted)-1] = r.State
} else {
// Newly tainted, so append it to the list, update the
// index, and remove the primary.
rs.Tainted = append(rs.Tainted, r.State)
rs.Primary = nil
r.TaintedIndex = len(rs.Tainted) - 1
rs.Primary = nil
}
} else if r.Flags&FlagReplacePrimary != 0 {
// If the ID is blank (there was an error), then we leave
// the primary that exists, and do not store this as a tainted
// instance
if r.State.ID == "" {
return
}
// Push the old primary into the tainted state
rs.Tainted = append(rs.Tainted, rs.Primary)
// Set this as the new primary
rs.Primary = r.State
} else {
// The primary instance, so just set it directly
rs.Primary = r.State

View File

@ -582,6 +582,58 @@ func TestContextApply(t *testing.T) {
}
}
func TestContextApply_createBeforeDestroy(t *testing.T) {
m := testModule(t, "apply-good-create-before")
p := testProvider("aws")
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.bar": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "bar",
Attributes: map[string]string{
"require_new": "abc",
},
},
},
},
},
},
}
ctx := testContext(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
State: state,
})
if _, err := ctx.Plan(nil); err != nil {
t.Fatalf("err: %s", err)
}
state, err := ctx.Apply()
if err != nil {
t.Fatalf("err: %s", err)
}
mod := state.RootModule()
if len(mod.Resources) != 1 {
t.Fatalf("bad: %#v", mod.Resources)
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyCreateBeforeStr)
if actual != expected {
t.Fatalf("bad: \n%s", actual)
}
}
func TestContextApply_Minimal(t *testing.T) {
m := testModule(t, "apply-minimal")
p := testProvider("aws")
@ -880,6 +932,168 @@ func TestContextApply_provisionerFail(t *testing.T) {
}
}
func TestContextApply_provisionerFail_createBeforeDestroy(t *testing.T) {
m := testModule(t, "apply-provisioner-fail-create-before")
p := testProvider("aws")
pr := testProvisioner()
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
pr.ApplyFn = func(*InstanceState, *ResourceConfig) error {
return fmt.Errorf("EXPLOSION")
}
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.bar": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "bar",
Attributes: map[string]string{
"require_new": "abc",
},
},
},
},
},
},
}
ctx := testContext(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
Provisioners: map[string]ResourceProvisionerFactory{
"shell": testProvisionerFuncFixed(pr),
},
State: state,
})
if _, err := ctx.Plan(nil); err != nil {
t.Fatalf("err: %s", err)
}
state, err := ctx.Apply()
if err == nil {
t.Fatal("should error")
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyProvisionerFailCreateBeforeDestroyStr)
if actual != expected {
t.Fatalf("bad: \n%s", actual)
}
}
func TestContextApply_error_createBeforeDestroy(t *testing.T) {
m := testModule(t, "apply-error-create-before")
p := testProvider("aws")
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.bar": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "bar",
Attributes: map[string]string{
"require_new": "abc",
},
},
},
},
},
},
}
ctx := testContext(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
State: state,
})
p.ApplyFn = func(info *InstanceInfo, is *InstanceState, id *InstanceDiff) (*InstanceState, error) {
return nil, fmt.Errorf("error")
}
p.DiffFn = testDiffFn
if _, err := ctx.Plan(nil); err != nil {
t.Fatalf("err: %s", err)
}
state, err := ctx.Apply()
if err == nil {
t.Fatal("should have error")
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyErrorCreateBeforeDestroyStr)
if actual != expected {
t.Fatalf("bad: \n%s\n\n\n%s", actual, expected)
}
}
func TestContextApply_errorDestroy_createBeforeDestroy(t *testing.T) {
m := testModule(t, "apply-error-create-before")
p := testProvider("aws")
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.bar": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "bar",
Attributes: map[string]string{
"require_new": "abc",
},
},
},
},
},
},
}
ctx := testContext(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
State: state,
})
p.ApplyFn = func(info *InstanceInfo, is *InstanceState, id *InstanceDiff) (*InstanceState, error) {
// Fail the destroy!
if id.Destroy {
return is, fmt.Errorf("error")
}
// Create should work
is = &InstanceState{
ID: "foo",
}
return is, nil
}
p.DiffFn = testDiffFn
if _, err := ctx.Plan(nil); err != nil {
t.Fatalf("err: %s", err)
}
state, err := ctx.Apply()
if err == nil {
t.Fatal("should have error")
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyErrorDestroyCreateBeforeDestroyStr)
if actual != expected {
t.Fatalf("bad: actual:\n%s\n\nexpected:\n%s", actual, expected)
}
}
func TestContextApply_provisionerResourceRef(t *testing.T) {
m := testModule(t, "apply-provisioner-resource-ref")
p := testProvider("aws")
@ -1698,6 +1912,85 @@ func TestContextApply_vars(t *testing.T) {
}
}
func TestContextApply_createBefore_depends(t *testing.T) {
m := testModule(t, "apply-depends-create-before")
h := new(HookRecordApplyOrder)
p := testProvider("aws")
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.web": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "bar",
Attributes: map[string]string{
"require_new": "ami-old",
},
},
},
"aws_instance.lb": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "baz",
Attributes: map[string]string{
"instance": "bar",
},
},
},
},
},
},
}
ctx := testContext(t, &ContextOpts{
Module: m,
Hooks: []Hook{h},
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
State: state,
})
if _, err := ctx.Plan(nil); err != nil {
t.Fatalf("err: %s", err)
}
h.Active = true
state, err := ctx.Apply()
if err != nil {
t.Fatalf("err: %s", err)
}
mod := state.RootModule()
if len(mod.Resources) < 2 {
t.Fatalf("bad: %#v", mod.Resources)
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyDependsCreateBeforeStr)
if actual != expected {
t.Fatalf("bad: \n%s\n%s", actual, expected)
}
// Test that things were managed _in the right order_
order := h.States
diffs := h.Diffs
if order[0].ID != "bar" || diffs[0].Destroy {
t.Fatalf("should create new instance first: %#v", order)
}
if order[1].ID != "baz" {
t.Fatalf("update must happen after create: %#v", order)
}
if order[2].ID != "bar" || !diffs[2].Destroy {
t.Fatalf("destroy must happen after update: %#v", order)
}
}
func TestContextPlan(t *testing.T) {
m := testModule(t, "plan-good")
p := testProvider("aws")
@ -3121,6 +3414,9 @@ func testDiffFn(
New: v.(string),
}
if k == "require_new" {
attrDiff.RequiresNew = true
}
diff.Attributes[k] = attrDiff
}

View File

@ -299,6 +299,15 @@ func graphEncodeDependencies(g *depgraph.Graph) {
}
r := rn.Resource
// If we are using create-before-destroy, there
// are some special depedencies injected on the
// deposed node that would cause a circular depedency
// chain if persisted. We must only handle the new node,
// node the deposed node.
if r.Flags&FlagDeposed != 0 {
continue
}
// Update the dependencies
var inject []string
for _, dep := range n.Deps {
@ -482,6 +491,7 @@ func graphAddConfigResources(
// these nodes for you.
func graphAddDiff(g *depgraph.Graph, d *ModuleDiff) error {
var nlist []*depgraph.Noun
injected := make(map[*depgraph.Dependency]struct{})
for _, n := range g.Nouns {
rn, ok := n.Meta.(*GraphNodeResource)
if !ok {
@ -530,13 +540,70 @@ func graphAddDiff(g *depgraph.Graph, d *ModuleDiff) error {
newDiff.Destroy = false
rd = newDiff
// Add to the new noun to our dependencies so that the destroy
// happens before the apply.
n.Deps = append(n.Deps, &depgraph.Dependency{
Name: newN.Name,
Source: n,
Target: newN,
})
// The dependency ordering depends on if the CreateBeforeDestroy
// flag is enabled. If so, we must create the replacement first,
// and then destroy the old instance.
if rn.Config != nil && rn.Config.Lifecycle.CreateBeforeDestroy && !rd.Empty() {
dep := &depgraph.Dependency{
Name: n.Name,
Source: newN,
Target: n,
}
// Add the old noun to the new noun dependencies so that
// the create happens before the destroy.
newN.Deps = append(newN.Deps, dep)
// Mark that this dependency has been injected so that
// we do not invert the direction below.
injected[dep] = struct{}{}
// Add a depedency from the root, since the create node
// does not depend on us
g.Root.Deps = append(g.Root.Deps, &depgraph.Dependency{
Name: newN.Name,
Source: g.Root,
Target: newN,
})
// Set the ReplacePrimary flag on the new instance so that
// it will become the new primary, and Diposed flag on the
// existing instance so that it will step down
rn.Resource.Flags |= FlagReplacePrimary
newNode.Resource.Flags |= FlagDeposed
// This logic is not intuitive, but we need to make the
// destroy depend upon any resources that depend on the
// create. The reason is suppose you have a LB depend on
// a web server. You need the order to be create, update LB,
// destroy. Without this, the update LB and destroy can
// be executed in an arbitrary order (likely in parallel).
incoming := g.DependsOn(n)
for _, inc := range incoming {
// Ignore the root...
if inc == g.Root {
continue
}
dep := &depgraph.Dependency{
Name: inc.Name,
Source: newN,
Target: inc,
}
injected[dep] = struct{}{}
newN.Deps = append(newN.Deps, dep)
}
} else {
dep := &depgraph.Dependency{
Name: newN.Name,
Source: n,
Target: newN,
}
// Add the new noun to our dependencies so that
// the destroy happens before the apply.
n.Deps = append(n.Deps, dep)
}
}
rn.Resource.Diff = rd
@ -544,7 +611,6 @@ func graphAddDiff(g *depgraph.Graph, d *ModuleDiff) error {
// Go through each noun and make sure we calculate all the dependencies
// properly.
injected := make(map[*depgraph.Dependency]struct{})
for _, n := range nlist {
deps := n.Deps
num := len(deps)
@ -948,6 +1014,7 @@ func graphAddRoot(g *depgraph.Graph) {
})
}
g.Nouns = append(g.Nouns, root)
g.Root = root
}
// graphAddVariableDeps inspects all the nouns and adds any dependencies

View File

@ -652,6 +652,81 @@ func TestGraphAddDiff_module(t *testing.T) {
}
}
func TestGraphAddDiff_createBeforeDestroy(t *testing.T) {
m := testModule(t, "graph-diff-create-before")
diff := &Diff{
Modules: []*ModuleDiff{
&ModuleDiff{
Path: rootModulePath,
Resources: map[string]*InstanceDiff{
"aws_instance.bar": &InstanceDiff{
Destroy: true,
Attributes: map[string]*ResourceAttrDiff{
"ami": &ResourceAttrDiff{
Old: "abc",
New: "xyz",
RequiresNew: true,
},
},
},
},
},
},
}
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.bar": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "bar",
Attributes: map[string]string{
"ami": "abc",
},
},
},
},
},
},
}
diffHash := checksumStruct(t, diff)
g, err := Graph(&GraphOpts{
Module: m,
Diff: diff,
State: state,
})
if err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTerraformGraphDiffCreateBeforeDestroyStr)
if actual != expected {
t.Fatalf("bad:\n\n%s\n\nexpected:\n\n%s", actual, expected)
}
// Verify the flags are set
r := g.Noun("aws_instance.bar")
if r.Meta.(*GraphNodeResource).Resource.Flags&FlagReplacePrimary == 0 {
t.Fatalf("missing FlagReplacePrimary")
}
r = g.Noun("aws_instance.bar (destroy)")
if r.Meta.(*GraphNodeResource).Resource.Flags&FlagDeposed == 0 {
t.Fatalf("missing FlagDeposed")
}
// Verify that our original structure has not been modified
diffHash2 := checksumStruct(t, diff)
if diffHash != diffHash2 {
t.Fatal("diff has been modified")
}
}
func TestGraphAddDiff_moduleDestroy(t *testing.T) {
m := testModule(t, "graph-diff-module")
diff := &Diff{
@ -1044,8 +1119,19 @@ aws_load_balancer.weblb
aws_load_balancer.weblb -> provider.aws
provider.aws
root
root -> aws_load_balancer.weblb
`
root -> aws_load_balancer.weblb`
const testTerraformGraphDiffCreateBeforeDestroyStr = `
root: root
aws_instance.bar
aws_instance.bar -> provider.aws
aws_instance.bar (destroy)
aws_instance.bar (destroy) -> aws_instance.bar
aws_instance.bar (destroy) -> provider.aws
provider.aws
root
root -> aws_instance.bar
root -> aws_instance.bar (destroy)`
const testTerraformGraphStateStr = `
root: root

View File

@ -47,6 +47,8 @@ const (
FlagTainted
FlagOrphan
FlagHasTainted
FlagReplacePrimary
FlagDeposed
)
// InstanceInfo is used to hold information about the instance and/or

View File

@ -150,6 +150,27 @@ aws_instance.foo:
type = aws_instance
`
const testTerraformApplyDependsCreateBeforeStr = `
aws_instance.lb:
ID = foo
instance = foo
type = aws_instance
Dependencies:
aws_instance.web
aws_instance.web:
ID = foo
require_new = ami-new
type = aws_instance
`
const testTerraformApplyCreateBeforeStr = `
aws_instance.bar:
ID = foo
require_new = xyz
type = aws_instance
`
const testTerraformApplyCancelStr = `
aws_instance.foo:
ID = foo
@ -218,6 +239,13 @@ aws_instance.foo:
type = aws_instance
`
const testTerraformApplyProvisionerFailCreateBeforeDestroyStr = `
aws_instance.bar: (1 tainted)
ID = bar
require_new = abc
Tainted ID 1 = foo
`
const testTerraformApplyProvisionerResourceRefStr = `
aws_instance.bar:
ID = foo
@ -247,6 +275,18 @@ aws_instance.foo:
num = 2
`
const testTerraformApplyErrorCreateBeforeDestroyStr = `
aws_instance.bar:
ID = bar
require_new = abc
`
const testTerraformApplyErrorDestroyCreateBeforeDestroyStr = `
aws_instance.bar: (1 tainted)
ID = foo
Tainted ID 1 = bar
`
const testTerraformApplyErrorPartialStr = `
aws_instance.bar:
ID = bar

View File

@ -0,0 +1,11 @@
resource "aws_instance" "web" {
require_new = "ami-new"
lifecycle {
create_before_destroy = true
}
}
resource "aws_instance" "lb" {
instance = "${aws_instance.web.id}"
}

View File

@ -0,0 +1,6 @@
resource "aws_instance" "bar" {
require_new = "xyz"
lifecycle {
create_before_destroy = true
}
}

View File

@ -0,0 +1,6 @@
resource "aws_instance" "bar" {
require_new = "xyz"
lifecycle {
create_before_destroy = true
}
}

View File

@ -0,0 +1,7 @@
resource "aws_instance" "bar" {
require_new = "xyz"
provisioner "shell" {}
lifecycle {
create_before_destroy = true
}
}

View File

@ -0,0 +1,8 @@
provider "aws" {}
resource "aws_instance" "bar" {
ami = "abc"
lifecycle {
create_before_destroy = true
}
}

View File

@ -49,6 +49,17 @@ There are **meta-parameters** available to all resources:
resource. The dependencies are in the format of `TYPE.NAME`,
for example `aws_instance.web`.
* `lifecycle` (configuration block) - Customizes the lifecycle
behavior of the resource. The specific options are documented
below.
The `lifecycle` block allows the following keys to be set:
* `create_before_destroy` (bool) - This flag is used to ensure
the replacement of a resource is created before the original
instance is destroyed. As an example, this can be used to
create an new DNS record before removing an old record.
-------------
Within a resource, you can optionally have a **connection block**.
@ -87,7 +98,8 @@ The full syntax is:
resource TYPE NAME {
CONFIG ...
[count = COUNT]
[depends_on = [RESOURCE NAME, ...]]
[depends_on = [RESOURCE NAME, ...]]
[LIFECYCLE]
[CONNECTION]
[PROVISIONER ...]
@ -104,6 +116,14 @@ KEY {
}
```
where `LIFECYCLE` is:
```
lifecycle {
[create_before_destroy = true|false]
}
```
where `CONNECTION` is:
```