Merge pull request #10404 from hashicorp/b-plan-deposed

terraform: diff shows pure deposed destroy
This commit is contained in:
Mitchell Hashimoto 2016-11-30 16:57:44 -08:00 committed by GitHub
commit a2f3259e51
16 changed files with 305 additions and 9 deletions

View File

@ -114,14 +114,21 @@ func formatPlanModuleExpand(
symbol = "-" symbol = "-"
} }
taintStr := "" var extraAttr []string
if rdiff.DestroyTainted { if rdiff.DestroyTainted {
taintStr = " (tainted)" extraAttr = append(extraAttr, "tainted")
}
if rdiff.DestroyDeposed {
extraAttr = append(extraAttr, "deposed")
}
var extraStr string
if len(extraAttr) > 0 {
extraStr = fmt.Sprintf(" (%s)", strings.Join(extraAttr, ", "))
} }
buf.WriteString(opts.Color.Color(fmt.Sprintf( buf.WriteString(opts.Color.Color(fmt.Sprintf(
"[%s]%s %s%s\n", "[%s]%s %s%s\n",
color, symbol, name, taintStr))) color, symbol, name, extraStr)))
// Get all the attributes that are changing, and sort them. Also // Get all the attributes that are changing, and sort them. Also
// determine the longest key so that we can align them all. // determine the longest key so that we can align them all.

View File

@ -8,6 +8,41 @@ import (
"github.com/mitchellh/colorstring" "github.com/mitchellh/colorstring"
) )
// Test that a root level data source gets a special plan output on create
func TestFormatPlan_destroyDeposed(t *testing.T) {
plan := &terraform.Plan{
Diff: &terraform.Diff{
Modules: []*terraform.ModuleDiff{
&terraform.ModuleDiff{
Path: []string{"root"},
Resources: map[string]*terraform.InstanceDiff{
"aws_instance.foo": &terraform.InstanceDiff{
DestroyDeposed: true,
},
},
},
},
},
}
opts := &FormatPlanOpts{
Plan: plan,
Color: &colorstring.Colorize{
Colors: colorstring.DefaultColors,
Disable: true,
},
ModuleDepth: 1,
}
actual := FormatPlan(opts)
expected := strings.TrimSpace(`
- aws_instance.foo (deposed)
`)
if actual != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, actual)
}
}
// Test that a root level data source gets a special plan output on create // Test that a root level data source gets a special plan output on create
func TestFormatPlan_rootDataSource(t *testing.T) { func TestFormatPlan_rootDataSource(t *testing.T) {
plan := &terraform.Plan{ plan := &terraform.Plan{

View File

@ -11,6 +11,31 @@ func TestCountHook_impl(t *testing.T) {
var _ terraform.Hook = new(CountHook) var _ terraform.Hook = new(CountHook)
} }
func TestCountHookPostDiff_DestroyDeposed(t *testing.T) {
h := new(CountHook)
resources := map[string]*terraform.InstanceDiff{
"lorem": &terraform.InstanceDiff{DestroyDeposed: true},
}
n := &terraform.InstanceInfo{} // TODO
for _, d := range resources {
h.PostDiff(n, d)
}
expected := new(CountHook)
expected.ToAdd = 0
expected.ToChange = 0
expected.ToRemoveAndAdd = 0
expected.ToRemove = 1
if !reflect.DeepEqual(expected, h) {
t.Fatalf("Expected %#v, got %#v instead.",
expected, h)
}
}
func TestCountHookPostDiff_DestroyOnly(t *testing.T) { func TestCountHookPostDiff_DestroyOnly(t *testing.T) {
h := new(CountHook) h := new(CountHook)

View File

@ -1014,6 +1014,61 @@ aws_instance.bar.1:
`) `)
} }
// Test that when we have a deposed instance but a good primary, we still
// destroy the deposed instance.
func TestContext2Apply_createBeforeDestroy_deposedOnly(t *testing.T) {
m := testModule(t, "apply-cbd-deposed-only")
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",
},
Deposed: []*InstanceState{
&InstanceState{
ID: "foo",
},
},
},
},
},
},
}
ctx := testContext2(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
State: state,
})
if p, err := ctx.Plan(); err != nil {
t.Fatalf("err: %s", err)
} else {
t.Logf(p.String())
}
state, err := ctx.Apply()
if err != nil {
t.Fatalf("err: %s", err)
}
checkStateString(t, state, `
aws_instance.bar:
ID = bar
`)
}
func TestContext2Apply_destroyComputed(t *testing.T) { func TestContext2Apply_destroyComputed(t *testing.T) {
m := testModule(t, "apply-destroy-computed") m := testModule(t, "apply-destroy-computed")
p := testProvider("aws") p := testProvider("aws")

View File

@ -38,6 +38,60 @@ func TestContext2Plan_basic(t *testing.T) {
} }
} }
func TestContext2Plan_createBefore_deposed(t *testing.T) {
m := testModule(t, "plan-cbd")
p := testProvider("aws")
p.DiffFn = testDiffFn
s := &State{
Modules: []*ModuleState{
&ModuleState{
Path: []string{"root"},
Resources: map[string]*ResourceState{
"aws_instance.foo": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "baz",
},
Deposed: []*InstanceState{
&InstanceState{ID: "foo"},
},
},
},
},
},
}
ctx := testContext2(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
State: s,
})
plan, err := ctx.Plan()
if err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(plan.String())
expected := strings.TrimSpace(`
DIFF:
DESTROY: aws_instance.foo (deposed only)
STATE:
aws_instance.foo: (1 deposed)
ID = baz
Deposed ID 1 = foo
`)
if actual != expected {
t.Fatalf("expected:\n%s, got:\n%s", expected, actual)
}
}
func TestContext2Plan_createBefore_maintainRoot(t *testing.T) { func TestContext2Plan_createBefore_maintainRoot(t *testing.T) {
m := testModule(t, "plan-cbd-maintain-root") m := testModule(t, "plan-cbd-maintain-root")
p := testProvider("aws") p := testProvider("aws")

View File

@ -291,16 +291,22 @@ func (d *ModuleDiff) String() string {
switch { switch {
case rdiff.RequiresNew() && (rdiff.GetDestroy() || rdiff.GetDestroyTainted()): case rdiff.RequiresNew() && (rdiff.GetDestroy() || rdiff.GetDestroyTainted()):
crud = "DESTROY/CREATE" crud = "DESTROY/CREATE"
case rdiff.GetDestroy(): case rdiff.GetDestroy() || rdiff.GetDestroyDeposed():
crud = "DESTROY" crud = "DESTROY"
case rdiff.RequiresNew(): case rdiff.RequiresNew():
crud = "CREATE" crud = "CREATE"
} }
extra := ""
if !rdiff.GetDestroy() && rdiff.GetDestroyDeposed() {
extra = " (deposed only)"
}
buf.WriteString(fmt.Sprintf( buf.WriteString(fmt.Sprintf(
"%s: %s\n", "%s: %s%s\n",
crud, crud,
name)) name,
extra))
keyLen := 0 keyLen := 0
rdiffAttrs := rdiff.CopyAttributes() rdiffAttrs := rdiff.CopyAttributes()
@ -356,6 +362,7 @@ type InstanceDiff struct {
mu sync.Mutex mu sync.Mutex
Attributes map[string]*ResourceAttrDiff Attributes map[string]*ResourceAttrDiff
Destroy bool Destroy bool
DestroyDeposed bool
DestroyTainted bool DestroyTainted bool
} }
@ -430,7 +437,7 @@ func (d *InstanceDiff) ChangeType() DiffChangeType {
return DiffDestroyCreate return DiffDestroyCreate
} }
if d.GetDestroy() { if d.GetDestroy() || d.GetDestroyDeposed() {
return DiffDestroy return DiffDestroy
} }
@ -449,7 +456,10 @@ func (d *InstanceDiff) Empty() bool {
d.mu.Lock() d.mu.Lock()
defer d.mu.Unlock() defer d.mu.Unlock()
return !d.Destroy && !d.DestroyTainted && len(d.Attributes) == 0 return !d.Destroy &&
!d.DestroyTainted &&
!d.DestroyDeposed &&
len(d.Attributes) == 0
} }
// Equal compares two diffs for exact equality. // Equal compares two diffs for exact equality.
@ -482,6 +492,7 @@ func (d *InstanceDiff) GoString() string {
Attributes: d.Attributes, Attributes: d.Attributes,
Destroy: d.Destroy, Destroy: d.Destroy,
DestroyTainted: d.DestroyTainted, DestroyTainted: d.DestroyTainted,
DestroyDeposed: d.DestroyDeposed,
}) })
} }
@ -516,6 +527,20 @@ func (d *InstanceDiff) requiresNew() bool {
return false return false
} }
func (d *InstanceDiff) GetDestroyDeposed() bool {
d.mu.Lock()
defer d.mu.Unlock()
return d.DestroyDeposed
}
func (d *InstanceDiff) SetDestroyDeposed(b bool) {
d.mu.Lock()
defer d.mu.Unlock()
d.DestroyDeposed = b
}
// These methods are properly locked, for use outside other InstanceDiff // These methods are properly locked, for use outside other InstanceDiff
// methods but everywhere else within in the terraform package. // methods but everywhere else within in the terraform package.
// TODO refactor the locking scheme // TODO refactor the locking scheme

View File

@ -69,6 +69,7 @@ func (n *EvalCompareDiff) Eval(ctx EvalContext) (interface{}, error) {
// EvalDiff is an EvalNode implementation that does a refresh for // EvalDiff is an EvalNode implementation that does a refresh for
// a resource. // a resource.
type EvalDiff struct { type EvalDiff struct {
Name string
Info *InstanceInfo Info *InstanceInfo
Config **ResourceConfig Config **ResourceConfig
Provider *ResourceProvider Provider *ResourceProvider
@ -112,6 +113,18 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
diff = new(InstanceDiff) diff = new(InstanceDiff)
} }
// Set DestroyDeposed if we have deposed instances
_, err = readInstanceFromState(ctx, n.Name, nil, func(rs *ResourceState) (*InstanceState, error) {
if len(rs.Deposed) > 0 {
diff.DestroyDeposed = true
}
return nil, nil
})
if err != nil {
return nil, err
}
// Preserve the DestroyTainted flag // Preserve the DestroyTainted flag
if n.Diff != nil { if n.Diff != nil {
diff.SetTainted((*n.Diff).GetDestroyTainted()) diff.SetTainted((*n.Diff).GetDestroyTainted())

View File

@ -166,6 +166,7 @@ func (n *NodePlannableResourceInstance) evalTreeManagedResource(
Output: &state, Output: &state,
}, },
&EvalDiff{ &EvalDiff{
Name: stateId,
Info: info, Info: info,
Config: &resourceConfig, Config: &resourceConfig,
Resource: n.Config, Resource: n.Config,

View File

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

View File

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

View File

@ -0,0 +1,3 @@
resource "aws_instance" "foo" {
lifecycle { create_before_destroy = true }
}

View File

@ -58,7 +58,7 @@ func (t *DiffTransformer) Transform(g *Graph) error {
addr.Path = m.Path[1:] addr.Path = m.Path[1:]
// If we're destroying, add the destroy node // If we're destroying, add the destroy node
if inst.Destroy { if inst.Destroy || inst.GetDestroyDeposed() {
abstract := &NodeAbstractResource{Addr: addr} abstract := &NodeAbstractResource{Addr: addr}
g.Add(&NodeDestroyResource{NodeAbstractResource: abstract}) g.Add(&NodeDestroyResource{NodeAbstractResource: abstract})
} }

9
test/foo/main.tf Normal file
View File

@ -0,0 +1,9 @@
resource "null_resource" "foo" {
count = 2
provisioner "local-exec" { command = "sleep ${count.index*3}" }
//provisioner "local-exec" { command = "exit 1" }
lifecycle { create_before_destroy = true }
}

1
test/index.html Normal file
View File

@ -0,0 +1 @@
Hello, World

13
test/main.tf Normal file
View File

@ -0,0 +1,13 @@
variable "username" {
default = "bob"
}
data "template_file" "user" {
template = "$${USERNAME}"
vars {
USERNAME = "${var.username}"
}
provisioner "local-exec" {
command = "echo ${self.rendered} > user.txt"
}
}

45
test/real.tfstate Normal file
View File

@ -0,0 +1,45 @@
{
"version": 3,
"terraform_version": "0.7.2",
"serial": 393,
"lineage": "e8e8cc31-ebe6-4260-bb6a-eed53258cc08",
"modules": [
{
"path": [
"root"
],
"outputs": {},
"resources": {
"aws_route53_record.rslc-xxx-test-route53-record": {
"type": "aws_route53_record",
"depends_on": [
],
"primary": {
"id": "ZKBENUPLDUDMJ_xxx-test.rslcare.com.au_A",
"attributes": {
"alias.#": "1",
"alias.2624757427.evaluate_target_health": "false",
"alias.2624757427.name": "awseb-e-3-AWSEBLoa-1OO1X7V7IRMMF-999999999.ap-southeast-2.elb.amazonaws.com.",
"alias.2624757427.zone_id": "Z2999QAZ9SRTIC",
"fqdn": "hws-test.rslcare.com.au",
"health_check_id": "",
"id": "ZKBENUPLDUDMJ_hws-test.rslcare.com.au_A",
"name": "xxx-test.example.com",
"records.#": "0",
"set_identifier": "",
"ttl": "0",
"type": "A",
"zone_id": "ZKBENUPLDUDMJ"
},
"meta": {
"schema_version": "2"
},
"tainted": false
},
"deposed": [],
"provider": ""
}
}
}
]
}