Merge pull request #317 from hashicorp/f-create-before
Adding support for `lifecycle` and `create_before_destroy`
This commit is contained in:
commit
a621525741
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
`
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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{
|
||||
// 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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -47,6 +47,8 @@ const (
|
|||
FlagTainted
|
||||
FlagOrphan
|
||||
FlagHasTainted
|
||||
FlagReplacePrimary
|
||||
FlagDeposed
|
||||
)
|
||||
|
||||
// InstanceInfo is used to hold information about the instance and/or
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}"
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
resource "aws_instance" "bar" {
|
||||
require_new = "xyz"
|
||||
lifecycle {
|
||||
create_before_destroy = true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
resource "aws_instance" "bar" {
|
||||
require_new = "xyz"
|
||||
lifecycle {
|
||||
create_before_destroy = true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
resource "aws_instance" "bar" {
|
||||
require_new = "xyz"
|
||||
provisioner "shell" {}
|
||||
lifecycle {
|
||||
create_before_destroy = true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
provider "aws" {}
|
||||
|
||||
resource "aws_instance" "bar" {
|
||||
ami = "abc"
|
||||
lifecycle {
|
||||
create_before_destroy = true
|
||||
}
|
||||
}
|
|
@ -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**.
|
||||
|
@ -88,6 +99,7 @@ resource TYPE NAME {
|
|||
CONFIG ...
|
||||
[count = COUNT]
|
||||
[depends_on = [RESOURCE NAME, ...]]
|
||||
[LIFECYCLE]
|
||||
|
||||
[CONNECTION]
|
||||
[PROVISIONER ...]
|
||||
|
@ -104,6 +116,14 @@ KEY {
|
|||
}
|
||||
```
|
||||
|
||||
where `LIFECYCLE` is:
|
||||
|
||||
```
|
||||
lifecycle {
|
||||
[create_before_destroy = true|false]
|
||||
}
|
||||
```
|
||||
|
||||
where `CONNECTION` is:
|
||||
|
||||
```
|
||||
|
|
Loading…
Reference in New Issue