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
|
RawConfig *RawConfig
|
||||||
Provisioners []*Provisioner
|
Provisioners []*Provisioner
|
||||||
DependsOn []string
|
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.
|
// 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, "count")
|
||||||
delete(config, "depends_on")
|
delete(config, "depends_on")
|
||||||
delete(config, "provisioner")
|
delete(config, "provisioner")
|
||||||
|
delete(config, "lifecycle")
|
||||||
|
|
||||||
rawConfig, err := NewRawConfig(config)
|
rawConfig, err := NewRawConfig(config)
|
||||||
if err != nil {
|
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{
|
result = append(result, &Resource{
|
||||||
Name: k,
|
Name: k,
|
||||||
Type: t.Key,
|
Type: t.Key,
|
||||||
|
@ -464,6 +479,7 @@ func loadResourcesHcl(os *hclobj.Object) ([]*Resource, error) {
|
||||||
RawConfig: rawConfig,
|
RawConfig: rawConfig,
|
||||||
Provisioners: provisioners,
|
Provisioners: provisioners,
|
||||||
DependsOn: dependsOn,
|
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 = `
|
const basicOutputsStr = `
|
||||||
web_ip
|
web_ip
|
||||||
vars
|
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
|
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
|
rs.Dependencies = r.Dependencies
|
||||||
|
|
||||||
// Assign the instance state to the proper location
|
// 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 {
|
if r.TaintedIndex >= 0 {
|
||||||
// Tainted with a pre-existing index, just update that spot
|
// Tainted with a pre-existing index, just update that spot
|
||||||
rs.Tainted[r.TaintedIndex] = r.State
|
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 {
|
} else {
|
||||||
// Newly tainted, so append it to the list, update the
|
// Newly tainted, so append it to the list, update the
|
||||||
// index, and remove the primary.
|
// index, and remove the primary.
|
||||||
rs.Tainted = append(rs.Tainted, r.State)
|
rs.Tainted = append(rs.Tainted, r.State)
|
||||||
rs.Primary = nil
|
|
||||||
r.TaintedIndex = len(rs.Tainted) - 1
|
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 {
|
} else {
|
||||||
// The primary instance, so just set it directly
|
// The primary instance, so just set it directly
|
||||||
rs.Primary = r.State
|
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) {
|
func TestContextApply_Minimal(t *testing.T) {
|
||||||
m := testModule(t, "apply-minimal")
|
m := testModule(t, "apply-minimal")
|
||||||
p := testProvider("aws")
|
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) {
|
func TestContextApply_provisionerResourceRef(t *testing.T) {
|
||||||
m := testModule(t, "apply-provisioner-resource-ref")
|
m := testModule(t, "apply-provisioner-resource-ref")
|
||||||
p := testProvider("aws")
|
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) {
|
func TestContextPlan(t *testing.T) {
|
||||||
m := testModule(t, "plan-good")
|
m := testModule(t, "plan-good")
|
||||||
p := testProvider("aws")
|
p := testProvider("aws")
|
||||||
|
@ -3121,6 +3414,9 @@ func testDiffFn(
|
||||||
New: v.(string),
|
New: v.(string),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if k == "require_new" {
|
||||||
|
attrDiff.RequiresNew = true
|
||||||
|
}
|
||||||
diff.Attributes[k] = attrDiff
|
diff.Attributes[k] = attrDiff
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -299,6 +299,15 @@ func graphEncodeDependencies(g *depgraph.Graph) {
|
||||||
}
|
}
|
||||||
r := rn.Resource
|
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
|
// Update the dependencies
|
||||||
var inject []string
|
var inject []string
|
||||||
for _, dep := range n.Deps {
|
for _, dep := range n.Deps {
|
||||||
|
@ -482,6 +491,7 @@ func graphAddConfigResources(
|
||||||
// these nodes for you.
|
// these nodes for you.
|
||||||
func graphAddDiff(g *depgraph.Graph, d *ModuleDiff) error {
|
func graphAddDiff(g *depgraph.Graph, d *ModuleDiff) error {
|
||||||
var nlist []*depgraph.Noun
|
var nlist []*depgraph.Noun
|
||||||
|
injected := make(map[*depgraph.Dependency]struct{})
|
||||||
for _, n := range g.Nouns {
|
for _, n := range g.Nouns {
|
||||||
rn, ok := n.Meta.(*GraphNodeResource)
|
rn, ok := n.Meta.(*GraphNodeResource)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -530,13 +540,70 @@ func graphAddDiff(g *depgraph.Graph, d *ModuleDiff) error {
|
||||||
newDiff.Destroy = false
|
newDiff.Destroy = false
|
||||||
rd = newDiff
|
rd = newDiff
|
||||||
|
|
||||||
// Add to the new noun to our dependencies so that the destroy
|
// The dependency ordering depends on if the CreateBeforeDestroy
|
||||||
// happens before the apply.
|
// flag is enabled. If so, we must create the replacement first,
|
||||||
n.Deps = append(n.Deps, &depgraph.Dependency{
|
// 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,
|
Name: newN.Name,
|
||||||
Source: n,
|
Source: n,
|
||||||
Target: newN,
|
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
|
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
|
// Go through each noun and make sure we calculate all the dependencies
|
||||||
// properly.
|
// properly.
|
||||||
injected := make(map[*depgraph.Dependency]struct{})
|
|
||||||
for _, n := range nlist {
|
for _, n := range nlist {
|
||||||
deps := n.Deps
|
deps := n.Deps
|
||||||
num := len(deps)
|
num := len(deps)
|
||||||
|
@ -948,6 +1014,7 @@ func graphAddRoot(g *depgraph.Graph) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
g.Nouns = append(g.Nouns, root)
|
g.Nouns = append(g.Nouns, root)
|
||||||
|
g.Root = root
|
||||||
}
|
}
|
||||||
|
|
||||||
// graphAddVariableDeps inspects all the nouns and adds any dependencies
|
// 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) {
|
func TestGraphAddDiff_moduleDestroy(t *testing.T) {
|
||||||
m := testModule(t, "graph-diff-module")
|
m := testModule(t, "graph-diff-module")
|
||||||
diff := &Diff{
|
diff := &Diff{
|
||||||
|
@ -1044,8 +1119,19 @@ aws_load_balancer.weblb
|
||||||
aws_load_balancer.weblb -> provider.aws
|
aws_load_balancer.weblb -> provider.aws
|
||||||
provider.aws
|
provider.aws
|
||||||
root
|
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 = `
|
const testTerraformGraphStateStr = `
|
||||||
root: root
|
root: root
|
||||||
|
|
|
@ -47,6 +47,8 @@ const (
|
||||||
FlagTainted
|
FlagTainted
|
||||||
FlagOrphan
|
FlagOrphan
|
||||||
FlagHasTainted
|
FlagHasTainted
|
||||||
|
FlagReplacePrimary
|
||||||
|
FlagDeposed
|
||||||
)
|
)
|
||||||
|
|
||||||
// InstanceInfo is used to hold information about the instance and/or
|
// InstanceInfo is used to hold information about the instance and/or
|
||||||
|
|
|
@ -150,6 +150,27 @@ aws_instance.foo:
|
||||||
type = aws_instance
|
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 = `
|
const testTerraformApplyCancelStr = `
|
||||||
aws_instance.foo:
|
aws_instance.foo:
|
||||||
ID = foo
|
ID = foo
|
||||||
|
@ -218,6 +239,13 @@ aws_instance.foo:
|
||||||
type = aws_instance
|
type = aws_instance
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const testTerraformApplyProvisionerFailCreateBeforeDestroyStr = `
|
||||||
|
aws_instance.bar: (1 tainted)
|
||||||
|
ID = bar
|
||||||
|
require_new = abc
|
||||||
|
Tainted ID 1 = foo
|
||||||
|
`
|
||||||
|
|
||||||
const testTerraformApplyProvisionerResourceRefStr = `
|
const testTerraformApplyProvisionerResourceRefStr = `
|
||||||
aws_instance.bar:
|
aws_instance.bar:
|
||||||
ID = foo
|
ID = foo
|
||||||
|
@ -247,6 +275,18 @@ aws_instance.foo:
|
||||||
num = 2
|
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 = `
|
const testTerraformApplyErrorPartialStr = `
|
||||||
aws_instance.bar:
|
aws_instance.bar:
|
||||||
ID = 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`,
|
resource. The dependencies are in the format of `TYPE.NAME`,
|
||||||
for example `aws_instance.web`.
|
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**.
|
Within a resource, you can optionally have a **connection block**.
|
||||||
|
@ -88,6 +99,7 @@ resource TYPE NAME {
|
||||||
CONFIG ...
|
CONFIG ...
|
||||||
[count = COUNT]
|
[count = COUNT]
|
||||||
[depends_on = [RESOURCE NAME, ...]]
|
[depends_on = [RESOURCE NAME, ...]]
|
||||||
|
[LIFECYCLE]
|
||||||
|
|
||||||
[CONNECTION]
|
[CONNECTION]
|
||||||
[PROVISIONER ...]
|
[PROVISIONER ...]
|
||||||
|
@ -104,6 +116,14 @@ KEY {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
where `LIFECYCLE` is:
|
||||||
|
|
||||||
|
```
|
||||||
|
lifecycle {
|
||||||
|
[create_before_destroy = true|false]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
where `CONNECTION` is:
|
where `CONNECTION` is:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
Loading…
Reference in New Issue