Merge pull request #6894 from hashicorp/svanharmelen-f-taint-semantics

core: change taint semantics
This commit is contained in:
Paul Hinze 2016-05-26 20:20:49 -05:00
commit 24eaf9ac55
36 changed files with 332 additions and 1152 deletions

View File

@ -1361,16 +1361,20 @@ const applyVarFileJSON = `
const testApplyDisableBackupStr = ` const testApplyDisableBackupStr = `
ID = bar ID = bar
Tainted = false
` `
const testApplyDisableBackupStateStr = ` const testApplyDisableBackupStateStr = `
ID = bar ID = bar
Tainted = false
` `
const testApplyStateStr = ` const testApplyStateStr = `
ID = bar ID = bar
Tainted = false
` `
const testApplyStateDiffStr = ` const testApplyStateDiffStr = `
ID = bar ID = bar
Tainted = false
` `

View File

@ -112,9 +112,14 @@ func formatPlanModuleExpand(
symbol = "-" symbol = "-"
} }
taintStr := ""
if rdiff.DestroyTainted {
taintStr = " (tainted)"
}
buf.WriteString(opts.Color.Color(fmt.Sprintf( buf.WriteString(opts.Color.Color(fmt.Sprintf(
"[%s]%s %s\n", "[%s]%s %s%s\n",
color, symbol, name))) color, symbol, name, taintStr)))
// 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

@ -101,7 +101,7 @@ func formatStateModuleExpand(
} }
taintStr := "" taintStr := ""
if len(rs.Tainted) > 0 { if rs.Primary.Tainted {
taintStr = " (tainted)" taintStr = " (tainted)"
} }

View File

@ -603,8 +603,10 @@ const testPlanNoStateStr = `
const testPlanStateStr = ` const testPlanStateStr = `
ID = bar ID = bar
Tainted = false
` `
const testPlanStateDefaultStr = ` const testPlanStateDefaultStr = `
ID = bar ID = bar
Tainted = false
` `

View File

@ -347,9 +347,8 @@ func TestTaint_module(t *testing.T) {
} }
const testTaintStr = ` const testTaintStr = `
test_instance.foo: (1 tainted) test_instance.foo: (tainted)
ID = <not created> ID = bar
Tainted ID 1 = bar
` `
const testTaintDefaultStr = ` const testTaintDefaultStr = `
@ -362,7 +361,6 @@ test_instance.foo:
ID = bar ID = bar
module.child: module.child:
test_instance.blah: (1 tainted) test_instance.blah: (tainted)
ID = <not created> ID = blah
Tainted ID 1 = blah
` `

View File

@ -17,14 +17,12 @@ func (c *UntaintCommand) Run(args []string) int {
var allowMissing bool var allowMissing bool
var module string var module string
var index int
cmdFlags := c.Meta.flagSet("untaint") cmdFlags := c.Meta.flagSet("untaint")
cmdFlags.BoolVar(&allowMissing, "allow-missing", false, "module") cmdFlags.BoolVar(&allowMissing, "allow-missing", false, "module")
cmdFlags.StringVar(&module, "module", "", "module") cmdFlags.StringVar(&module, "module", "", "module")
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path") cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path") cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path")
cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path") cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path")
cmdFlags.IntVar(&index, "index", -1, "index")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil { if err := cmdFlags.Parse(args); err != nil {
return 1 return 1
@ -108,11 +106,7 @@ func (c *UntaintCommand) Run(args []string) int {
} }
// Untaint the resource // Untaint the resource
if err := rs.Untaint(index); err != nil { rs.Untaint()
c.Ui.Error(fmt.Sprintf("Error untainting %s: %s", name, err))
c.Ui.Error("You can use `terraform show` to inspect the current state.")
return 1
}
log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath()) log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath())
if err := c.Meta.PersistState(s); err != nil { if err := c.Meta.PersistState(s); err != nil {
@ -148,13 +142,6 @@ Options:
modifying. Defaults to the "-state-out" path with modifying. Defaults to the "-state-out" path with
".backup" extension. Set to "-" to disable backup. ".backup" extension. Set to "-" to disable backup.
-index=n Selects a single tainted instance when there are more
than one tainted instances present in the state for a
given resource. This flag is required when multiple
tainted instances are present. The vast majority of the
time, there is a maxiumum of one tainted instance per
resource, so this flag can be safely omitted.
-module=path The module path where the resource lives. By -module=path The module path where the resource lives. By
default this will be root. Child modules can be specified default this will be root. Child modules can be specified
by names. Ex. "consul" or "consul.vpc" (nested modules). by names. Ex. "consul" or "consul.vpc" (nested modules).

View File

@ -17,8 +17,9 @@ func TestUntaint(t *testing.T) {
Resources: map[string]*terraform.ResourceState{ Resources: map[string]*terraform.ResourceState{
"test_instance.foo": &terraform.ResourceState{ "test_instance.foo": &terraform.ResourceState{
Type: "test_instance", Type: "test_instance",
Tainted: []*terraform.InstanceState{ Primary: &terraform.InstanceState{
&terraform.InstanceState{ID: "bar"}, ID: "bar",
Tainted: true,
}, },
}, },
}, },
@ -49,101 +50,6 @@ test_instance.foo:
testStateOutput(t, statePath, expected) testStateOutput(t, statePath, expected)
} }
func TestUntaint_indexRequired(t *testing.T) {
state := &terraform.State{
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Resources: map[string]*terraform.ResourceState{
"test_instance.foo": &terraform.ResourceState{
Type: "test_instance",
Tainted: []*terraform.InstanceState{
&terraform.InstanceState{ID: "bar"},
&terraform.InstanceState{ID: "bar2"},
},
},
},
},
},
}
statePath := testStateFile(t, state)
ui := new(cli.MockUi)
c := &UntaintCommand{
Meta: Meta{
Ui: ui,
},
}
args := []string{
"-state", statePath,
"test_instance.foo",
}
if code := c.Run(args); code == 0 {
t.Fatalf("Expected non-zero exit. Output:\n\n%s", ui.OutputWriter.String())
}
// Nothing should have gotten untainted
expected := strings.TrimSpace(`
test_instance.foo: (2 tainted)
ID = <not created>
Tainted ID 1 = bar
Tainted ID 2 = bar2
`)
testStateOutput(t, statePath, expected)
// Should have gotten an error message mentioning index
errOut := ui.ErrorWriter.String()
errContains := "please specify an index"
if !strings.Contains(errOut, errContains) {
t.Fatalf("Expected err output: %s, to contain: %s", errOut, errContains)
}
}
func TestUntaint_indexSpecified(t *testing.T) {
state := &terraform.State{
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Resources: map[string]*terraform.ResourceState{
"test_instance.foo": &terraform.ResourceState{
Type: "test_instance",
Tainted: []*terraform.InstanceState{
&terraform.InstanceState{ID: "bar"},
&terraform.InstanceState{ID: "bar2"},
},
},
},
},
},
}
statePath := testStateFile(t, state)
ui := new(cli.MockUi)
c := &UntaintCommand{
Meta: Meta{
Ui: ui,
},
}
args := []string{
"-state", statePath,
"-index", "1",
"test_instance.foo",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
// Nothing should have gotten untainted
expected := strings.TrimSpace(`
test_instance.foo: (1 tainted)
ID = bar2
Tainted ID 1 = bar
`)
testStateOutput(t, statePath, expected)
}
func TestUntaint_backup(t *testing.T) { func TestUntaint_backup(t *testing.T) {
// Get a temp cwd // Get a temp cwd
tmp, cwd := testCwd(t) tmp, cwd := testCwd(t)
@ -157,8 +63,9 @@ func TestUntaint_backup(t *testing.T) {
Resources: map[string]*terraform.ResourceState{ Resources: map[string]*terraform.ResourceState{
"test_instance.foo": &terraform.ResourceState{ "test_instance.foo": &terraform.ResourceState{
Type: "test_instance", Type: "test_instance",
Tainted: []*terraform.InstanceState{ Primary: &terraform.InstanceState{
&terraform.InstanceState{ID: "bar"}, ID: "bar",
Tainted: true,
}, },
}, },
}, },
@ -183,9 +90,8 @@ func TestUntaint_backup(t *testing.T) {
// Backup is still tainted // Backup is still tainted
testStateOutput(t, path+".backup", strings.TrimSpace(` testStateOutput(t, path+".backup", strings.TrimSpace(`
test_instance.foo: (1 tainted) test_instance.foo: (tainted)
ID = <not created> ID = bar
Tainted ID 1 = bar
`)) `))
// State is untainted // State is untainted
@ -208,8 +114,9 @@ func TestUntaint_backupDisable(t *testing.T) {
Resources: map[string]*terraform.ResourceState{ Resources: map[string]*terraform.ResourceState{
"test_instance.foo": &terraform.ResourceState{ "test_instance.foo": &terraform.ResourceState{
Type: "test_instance", Type: "test_instance",
Tainted: []*terraform.InstanceState{ Primary: &terraform.InstanceState{
&terraform.InstanceState{ID: "bar"}, ID: "bar",
Tainted: true,
}, },
}, },
}, },
@ -273,8 +180,9 @@ func TestUntaint_defaultState(t *testing.T) {
Resources: map[string]*terraform.ResourceState{ Resources: map[string]*terraform.ResourceState{
"test_instance.foo": &terraform.ResourceState{ "test_instance.foo": &terraform.ResourceState{
Type: "test_instance", Type: "test_instance",
Tainted: []*terraform.InstanceState{ Primary: &terraform.InstanceState{
&terraform.InstanceState{ID: "bar"}, ID: "bar",
Tainted: true,
}, },
}, },
}, },
@ -311,8 +219,9 @@ func TestUntaint_missing(t *testing.T) {
Resources: map[string]*terraform.ResourceState{ Resources: map[string]*terraform.ResourceState{
"test_instance.foo": &terraform.ResourceState{ "test_instance.foo": &terraform.ResourceState{
Type: "test_instance", Type: "test_instance",
Tainted: []*terraform.InstanceState{ Primary: &terraform.InstanceState{
&terraform.InstanceState{ID: "bar"}, ID: "bar",
Tainted: true,
}, },
}, },
}, },
@ -345,8 +254,9 @@ func TestUntaint_missingAllow(t *testing.T) {
Resources: map[string]*terraform.ResourceState{ Resources: map[string]*terraform.ResourceState{
"test_instance.foo": &terraform.ResourceState{ "test_instance.foo": &terraform.ResourceState{
Type: "test_instance", Type: "test_instance",
Tainted: []*terraform.InstanceState{ Primary: &terraform.InstanceState{
&terraform.InstanceState{ID: "bar"}, ID: "bar",
Tainted: true,
}, },
}, },
}, },
@ -385,8 +295,9 @@ func TestUntaint_stateOut(t *testing.T) {
Resources: map[string]*terraform.ResourceState{ Resources: map[string]*terraform.ResourceState{
"test_instance.foo": &terraform.ResourceState{ "test_instance.foo": &terraform.ResourceState{
Type: "test_instance", Type: "test_instance",
Tainted: []*terraform.InstanceState{ Primary: &terraform.InstanceState{
&terraform.InstanceState{ID: "bar"}, ID: "bar",
Tainted: true,
}, },
}, },
}, },
@ -411,9 +322,8 @@ func TestUntaint_stateOut(t *testing.T) {
} }
testStateOutput(t, path, strings.TrimSpace(` testStateOutput(t, path, strings.TrimSpace(`
test_instance.foo: (1 tainted) test_instance.foo: (tainted)
ID = <not created> ID = bar
Tainted ID 1 = bar
`)) `))
testStateOutput(t, "foo", strings.TrimSpace(` testStateOutput(t, "foo", strings.TrimSpace(`
test_instance.foo: test_instance.foo:
@ -429,8 +339,9 @@ func TestUntaint_module(t *testing.T) {
Resources: map[string]*terraform.ResourceState{ Resources: map[string]*terraform.ResourceState{
"test_instance.foo": &terraform.ResourceState{ "test_instance.foo": &terraform.ResourceState{
Type: "test_instance", Type: "test_instance",
Tainted: []*terraform.InstanceState{ Primary: &terraform.InstanceState{
&terraform.InstanceState{ID: "bar"}, ID: "bar",
Tainted: true,
}, },
}, },
}, },
@ -440,8 +351,9 @@ func TestUntaint_module(t *testing.T) {
Resources: map[string]*terraform.ResourceState{ Resources: map[string]*terraform.ResourceState{
"test_instance.blah": &terraform.ResourceState{ "test_instance.blah": &terraform.ResourceState{
Type: "test_instance", Type: "test_instance",
Tainted: []*terraform.InstanceState{ Primary: &terraform.InstanceState{
&terraform.InstanceState{ID: "bar"}, ID: "bar",
Tainted: true,
}, },
}, },
}, },
@ -467,9 +379,8 @@ func TestUntaint_module(t *testing.T) {
} }
testStateOutput(t, statePath, strings.TrimSpace(` testStateOutput(t, statePath, strings.TrimSpace(`
test_instance.foo: (1 tainted) test_instance.foo: (tainted)
ID = <not created> ID = bar
Tainted ID 1 = bar
module.child: module.child:
test_instance.blah: test_instance.blah:

View File

@ -290,6 +290,10 @@ func (d *ResourceData) State() *terraform.InstanceState {
result.Attributes["id"] = d.Id() result.Attributes["id"] = d.Id()
} }
if d.state != nil {
result.Tainted = d.state.Tainted
}
return &result return &result
} }

View File

@ -308,6 +308,11 @@ func (m schemaMap) Diff(
result := new(terraform.InstanceDiff) result := new(terraform.InstanceDiff)
result.Attributes = make(map[string]*terraform.ResourceAttrDiff) result.Attributes = make(map[string]*terraform.ResourceAttrDiff)
// Make sure to mark if the resource is tainted
if s != nil {
result.DestroyTainted = s.Tainted
}
d := &ResourceData{ d := &ResourceData{
schema: m, schema: m,
state: s, state: s,
@ -330,6 +335,9 @@ func (m schemaMap) Diff(
result2 := new(terraform.InstanceDiff) result2 := new(terraform.InstanceDiff)
result2.Attributes = make(map[string]*terraform.ResourceAttrDiff) result2.Attributes = make(map[string]*terraform.ResourceAttrDiff)
// Preserve the DestroyTainted flag
result2.DestroyTainted = result.DestroyTainted
// Reset the data to not contain state. We have to call init() // Reset the data to not contain state. We have to call init()
// again in order to reset the FieldReaders. // again in order to reset the FieldReaders.
d.state = nil d.state = nil

View File

@ -884,14 +884,13 @@ func TestContext2Apply_countTainted(t *testing.T) {
Resources: map[string]*ResourceState{ Resources: map[string]*ResourceState{
"aws_instance.foo.0": &ResourceState{ "aws_instance.foo.0": &ResourceState{
Type: "aws_instance", Type: "aws_instance",
Tainted: []*InstanceState{ Primary: &InstanceState{
&InstanceState{ ID: "bar",
ID: "bar", Attributes: map[string]string{
Attributes: map[string]string{ "foo": "foo",
"foo": "foo", "type": "aws_instance",
"type": "aws_instance",
},
}, },
Tainted: true,
}, },
}, },
}, },
@ -2989,13 +2988,12 @@ func TestContext2Apply_destroyTaintedProvisioner(t *testing.T) {
Resources: map[string]*ResourceState{ Resources: map[string]*ResourceState{
"aws_instance.foo": &ResourceState{ "aws_instance.foo": &ResourceState{
Type: "aws_instance", Type: "aws_instance",
Tainted: []*InstanceState{ Primary: &InstanceState{
&InstanceState{ ID: "bar",
ID: "bar", Attributes: map[string]string{
Attributes: map[string]string{ "id": "bar",
"id": "bar",
},
}, },
Tainted: true,
}, },
}, },
}, },
@ -3505,14 +3503,13 @@ func TestContext2Apply_taint(t *testing.T) {
Resources: map[string]*ResourceState{ Resources: map[string]*ResourceState{
"aws_instance.bar": &ResourceState{ "aws_instance.bar": &ResourceState{
Type: "aws_instance", Type: "aws_instance",
Tainted: []*InstanceState{ Primary: &InstanceState{
&InstanceState{ ID: "baz",
ID: "baz", Attributes: map[string]string{
Attributes: map[string]string{ "num": "2",
"num": "2", "type": "aws_instance",
"type": "aws_instance",
},
}, },
Tainted: true,
}, },
}, },
}, },
@ -3559,14 +3556,13 @@ func TestContext2Apply_taintDep(t *testing.T) {
Resources: map[string]*ResourceState{ Resources: map[string]*ResourceState{
"aws_instance.foo": &ResourceState{ "aws_instance.foo": &ResourceState{
Type: "aws_instance", Type: "aws_instance",
Tainted: []*InstanceState{ Primary: &InstanceState{
&InstanceState{ ID: "baz",
ID: "baz", Attributes: map[string]string{
Attributes: map[string]string{ "num": "2",
"num": "2", "type": "aws_instance",
"type": "aws_instance",
},
}, },
Tainted: true,
}, },
}, },
"aws_instance.bar": &ResourceState{ "aws_instance.bar": &ResourceState{
@ -3622,14 +3618,13 @@ func TestContext2Apply_taintDepRequiresNew(t *testing.T) {
Resources: map[string]*ResourceState{ Resources: map[string]*ResourceState{
"aws_instance.foo": &ResourceState{ "aws_instance.foo": &ResourceState{
Type: "aws_instance", Type: "aws_instance",
Tainted: []*InstanceState{ Primary: &InstanceState{
&InstanceState{ ID: "baz",
ID: "baz", Attributes: map[string]string{
Attributes: map[string]string{ "num": "2",
"num": "2", "type": "aws_instance",
"type": "aws_instance",
},
}, },
Tainted: true,
}, },
}, },
"aws_instance.bar": &ResourceState{ "aws_instance.bar": &ResourceState{
@ -4438,10 +4433,9 @@ func TestContext2Apply_targetedWithTaintedInState(t *testing.T) {
Path: rootModulePath, Path: rootModulePath,
Resources: map[string]*ResourceState{ Resources: map[string]*ResourceState{
"aws_instance.ifailedprovisioners": &ResourceState{ "aws_instance.ifailedprovisioners": &ResourceState{
Tainted: []*InstanceState{ Primary: &InstanceState{
&InstanceState{ ID: "ifailedprovisioners",
ID: "ifailedprovisioners", Tainted: true,
},
}, },
}, },
}, },
@ -4485,9 +4479,8 @@ func TestContext2Apply_targetedWithTaintedInState(t *testing.T) {
expected := strings.TrimSpace(` expected := strings.TrimSpace(`
aws_instance.iambeingadded: aws_instance.iambeingadded:
ID = foo ID = foo
aws_instance.ifailedprovisioners: (1 tainted) aws_instance.ifailedprovisioners: (tainted)
ID = <not created> ID = ifailedprovisioners
Tainted ID 1 = ifailedprovisioners
`) `)
if actual != expected { if actual != expected {
t.Fatalf("expected state: \n%s\ngot: \n%s", expected, actual) t.Fatalf("expected state: \n%s\ngot: \n%s", expected, actual)

View File

@ -1718,10 +1718,9 @@ func TestContext2Plan_taint(t *testing.T) {
}, },
"aws_instance.bar": &ResourceState{ "aws_instance.bar": &ResourceState{
Type: "aws_instance", Type: "aws_instance",
Tainted: []*InstanceState{ Primary: &InstanceState{
&InstanceState{ ID: "baz",
ID: "baz", Tainted: true,
},
}, },
}, },
}, },
@ -1748,57 +1747,6 @@ func TestContext2Plan_taint(t *testing.T) {
} }
} }
func TestContext2Plan_multiple_taint(t *testing.T) {
m := testModule(t, "plan-taint")
p := testProvider("aws")
p.DiffFn = testDiffFn
s := &State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.foo": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "bar",
Attributes: map[string]string{"num": "2"},
},
},
"aws_instance.bar": &ResourceState{
Type: "aws_instance",
Tainted: []*InstanceState{
&InstanceState{
ID: "baz",
},
&InstanceState{
ID: "zip",
},
},
},
},
},
},
}
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(testTerraformPlanMultipleTaintStr)
if actual != expected {
t.Fatalf("bad:\n%s", actual)
}
}
// Fails about 50% of the time before the fix for GH-4982, covers the fix. // Fails about 50% of the time before the fix for GH-4982, covers the fix.
func TestContext2Plan_taintDestroyInterpolatedCountRace(t *testing.T) { func TestContext2Plan_taintDestroyInterpolatedCountRace(t *testing.T) {
m := testModule(t, "plan-taint-interpolated-count") m := testModule(t, "plan-taint-interpolated-count")
@ -1811,8 +1759,9 @@ func TestContext2Plan_taintDestroyInterpolatedCountRace(t *testing.T) {
Resources: map[string]*ResourceState{ Resources: map[string]*ResourceState{
"aws_instance.foo.0": &ResourceState{ "aws_instance.foo.0": &ResourceState{
Type: "aws_instance", Type: "aws_instance",
Tainted: []*InstanceState{ Primary: &InstanceState{
&InstanceState{ID: "bar"}, ID: "bar",
Tainted: true,
}, },
}, },
"aws_instance.foo.1": &ResourceState{ "aws_instance.foo.1": &ResourceState{
@ -1849,9 +1798,8 @@ DESTROY/CREATE: aws_instance.foo.0
STATE: STATE:
aws_instance.foo.0: (1 tainted) aws_instance.foo.0: (tainted)
ID = <not created> ID = bar
Tainted ID 1 = bar
aws_instance.foo.1: aws_instance.foo.1:
ID = bar ID = bar
aws_instance.foo.2: aws_instance.foo.2:

View File

@ -324,10 +324,9 @@ func TestContext2Refresh_modules(t *testing.T) {
Resources: map[string]*ResourceState{ Resources: map[string]*ResourceState{
"aws_instance.web": &ResourceState{ "aws_instance.web": &ResourceState{
Type: "aws_instance", Type: "aws_instance",
Tainted: []*InstanceState{ Primary: &InstanceState{
&InstanceState{ ID: "bar",
ID: "bar", Tainted: true,
},
}, },
}, },
}, },
@ -650,10 +649,9 @@ func TestContext2Refresh_tainted(t *testing.T) {
Resources: map[string]*ResourceState{ Resources: map[string]*ResourceState{
"aws_instance.web": &ResourceState{ "aws_instance.web": &ResourceState{
Type: "aws_instance", Type: "aws_instance",
Tainted: []*InstanceState{ Primary: &InstanceState{
&InstanceState{ ID: "bar",
ID: "bar", Tainted: true,
},
}, },
}, },
}, },
@ -670,7 +668,8 @@ func TestContext2Refresh_tainted(t *testing.T) {
p.RefreshFn = nil p.RefreshFn = nil
p.RefreshReturn = &InstanceState{ p.RefreshReturn = &InstanceState{
ID: "foo", ID: "foo",
Tainted: true,
} }
s, err := ctx.Refresh() s, err := ctx.Refresh()

View File

@ -107,9 +107,13 @@ func testDiffFn(
info *InstanceInfo, info *InstanceInfo,
s *InstanceState, s *InstanceState,
c *ResourceConfig) (*InstanceDiff, error) { c *ResourceConfig) (*InstanceDiff, error) {
var diff InstanceDiff diff := new(InstanceDiff)
diff.Attributes = make(map[string]*ResourceAttrDiff) diff.Attributes = make(map[string]*ResourceAttrDiff)
if s != nil {
diff.DestroyTainted = s.Tainted
}
for k, v := range c.Raw { for k, v := range c.Raw {
if _, ok := v.(string); !ok { if _, ok := v.(string); !ok {
continue continue
@ -181,17 +185,21 @@ func testDiffFn(
} }
} }
for k, v := range diff.Attributes { // If we recreate this resource because it's tainted, we keep all attrs
if v.NewComputed { if !diff.RequiresNew() {
continue for k, v := range diff.Attributes {
} if v.NewComputed {
continue
}
old, ok := s.Attributes[k] old, ok := s.Attributes[k]
if !ok { if !ok {
continue continue
} }
if old == v.New {
delete(diff.Attributes, k) if old == v.New {
delete(diff.Attributes, k)
}
} }
} }
@ -202,7 +210,7 @@ func testDiffFn(
} }
} }
return &diff, nil return diff, nil
} }
func testProvider(prefix string) *MockResourceProvider { func testProvider(prefix string) *MockResourceProvider {
@ -276,9 +284,8 @@ root
` `
const testContextRefreshModuleStr = ` const testContextRefreshModuleStr = `
aws_instance.web: (1 tainted) aws_instance.web: (tainted)
ID = <not created> ID = bar
Tainted ID 1 = bar
module.child: module.child:
aws_instance.web: aws_instance.web:
@ -300,7 +307,6 @@ const testContextRefreshOutputPartialStr = `
` `
const testContextRefreshTaintedStr = ` const testContextRefreshTaintedStr = `
aws_instance.web: (1 tainted) aws_instance.web: (tainted)
ID = <not created> ID = foo
Tainted ID 1 = foo
` `

View File

@ -651,10 +651,9 @@ func TestContext2Validate_tainted(t *testing.T) {
Resources: map[string]*ResourceState{ Resources: map[string]*ResourceState{
"aws_instance.foo": &ResourceState{ "aws_instance.foo": &ResourceState{
Type: "aws_instance", Type: "aws_instance",
Tainted: []*InstanceState{ Primary: &InstanceState{
&InstanceState{ ID: "bar",
ID: "bar", Tainted: true,
},
}, },
}, },
}, },

View File

@ -215,11 +215,12 @@ func (d *ModuleDiff) String() string {
rdiff := d.Resources[name] rdiff := d.Resources[name]
crud := "UPDATE" crud := "UPDATE"
if rdiff.RequiresNew() && (rdiff.Destroy || rdiff.DestroyTainted) { switch {
case rdiff.RequiresNew() && (rdiff.Destroy || rdiff.DestroyTainted):
crud = "DESTROY/CREATE" crud = "DESTROY/CREATE"
} else if rdiff.Destroy { case rdiff.Destroy:
crud = "DESTROY" crud = "DESTROY"
} else if rdiff.RequiresNew() { case rdiff.RequiresNew():
crud = "CREATE" crud = "CREATE"
} }
@ -356,6 +357,10 @@ func (d *InstanceDiff) RequiresNew() bool {
return false return false
} }
if d.DestroyTainted {
return true
}
for _, rd := range d.Attributes { for _, rd := range d.Attributes {
if rd != nil && rd.RequiresNew { if rd != nil && rd.RequiresNew {
return true return true

View File

@ -139,7 +139,6 @@ type EvalApplyProvisioners struct {
Resource *config.Resource Resource *config.Resource
InterpResource *Resource InterpResource *Resource
CreateNew *bool CreateNew *bool
Tainted *bool
Error *error Error *error
} }
@ -159,9 +158,7 @@ func (n *EvalApplyProvisioners) Eval(ctx EvalContext) (interface{}, error) {
if n.Error != nil && *n.Error != nil { if n.Error != nil && *n.Error != nil {
// We're already errored creating, so mark as tainted and continue // We're already errored creating, so mark as tainted and continue
if n.Tainted != nil { state.Tainted = true
*n.Tainted = true
}
// We're already tainted, so just return out // We're already tainted, so just return out
return nil, nil return nil, nil
@ -180,10 +177,10 @@ func (n *EvalApplyProvisioners) Eval(ctx EvalContext) (interface{}, error) {
// If there are no errors, then we append it to our output error // If there are no errors, then we append it to our output error
// if we have one, otherwise we just output it. // if we have one, otherwise we just output it.
err := n.apply(ctx) err := n.apply(ctx)
if n.Tainted != nil {
*n.Tainted = err != nil
}
if err != nil { if err != nil {
// Provisioning failed, so mark the resource as tainted
state.Tainted = true
if n.Error != nil { if n.Error != nil {
*n.Error = multierror.Append(*n.Error, err) *n.Error = multierror.Append(*n.Error, err)
} else { } else {

View File

@ -69,8 +69,9 @@ type EvalDiff struct {
Info *InstanceInfo Info *InstanceInfo
Config **ResourceConfig Config **ResourceConfig
Provider *ResourceProvider Provider *ResourceProvider
Diff **InstanceDiff
State **InstanceState State **InstanceState
Output **InstanceDiff OutputDiff **InstanceDiff
OutputState **InstanceState OutputState **InstanceState
} }
@ -104,7 +105,12 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
diff = new(InstanceDiff) diff = new(InstanceDiff)
} }
// Require a destroy if there is no ID and it requires new. // Preserve the DestroyTainted flag
if n.Diff != nil {
diff.DestroyTainted = (*n.Diff).DestroyTainted
}
// Require a destroy if there is an ID and it requires new.
if diff.RequiresNew() && state != nil && state.ID != "" { if diff.RequiresNew() && state != nil && state.ID != "" {
diff.Destroy = true diff.Destroy = true
} }
@ -135,7 +141,7 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
} }
// Update our output // Update our output
*n.Output = diff *n.OutputDiff = diff
// Update the state if we care // Update the state if we care
if n.OutputState != nil { if n.OutputState != nil {
@ -216,41 +222,6 @@ func (n *EvalDiffDestroyModule) Eval(ctx EvalContext) (interface{}, error) {
return nil, nil return nil, nil
} }
// EvalDiffTainted is an EvalNode implementation that writes the diff to
// the full diff.
type EvalDiffTainted struct {
Name string
Diff **InstanceDiff
}
// TODO: test
func (n *EvalDiffTainted) Eval(ctx EvalContext) (interface{}, error) {
state, lock := ctx.State()
// Get a read lock so we can access this instance
lock.RLock()
defer lock.RUnlock()
// Look for the module state. If we don't have one, then it doesn't matter.
mod := state.ModuleByPath(ctx.Path())
if mod == nil {
return nil, nil
}
// Look for the resource state. If we don't have one, then it is okay.
rs := mod.Resources[n.Name]
if rs == nil {
return nil, nil
}
// If we have tainted, then mark it on the diff
if len(rs.Tainted) > 0 {
(*n.Diff).DestroyTainted = true
}
return nil, nil
}
// EvalFilterDiff is an EvalNode implementation that filters the diff // EvalFilterDiff is an EvalNode implementation that filters the diff
// according to some filter. // according to some filter.
type EvalFilterDiff struct { type EvalFilterDiff struct {

View File

@ -1,8 +1,6 @@
package terraform package terraform
import ( import "fmt"
"fmt"
)
// EvalReadState is an EvalNode implementation that reads the // EvalReadState is an EvalNode implementation that reads the
// primary InstanceState for a specific resource out of the state. // primary InstanceState for a specific resource out of the state.
@ -17,31 +15,6 @@ func (n *EvalReadState) Eval(ctx EvalContext) (interface{}, error) {
}) })
} }
// EvalReadStateTainted is an EvalNode implementation that reads a
// tainted InstanceState for a specific resource out of the state
type EvalReadStateTainted struct {
Name string
Output **InstanceState
// Index indicates which instance in the Tainted list to target, or -1 for
// the last item.
Index int
}
func (n *EvalReadStateTainted) Eval(ctx EvalContext) (interface{}, error) {
return readInstanceFromState(ctx, n.Name, n.Output, func(rs *ResourceState) (*InstanceState, error) {
// Get the index. If it is negative, then we get the last one
idx := n.Index
if idx < 0 {
idx = len(rs.Tainted) - 1
}
if idx >= 0 && idx < len(rs.Tainted) {
return rs.Tainted[idx], nil
} else {
return nil, fmt.Errorf("bad tainted index: %d, for resource: %#v", idx, rs)
}
})
}
// EvalReadStateDeposed is an EvalNode implementation that reads the // EvalReadStateDeposed is an EvalNode implementation that reads the
// deposed InstanceState for a specific resource out of the state // deposed InstanceState for a specific resource out of the state
type EvalReadStateDeposed struct { type EvalReadStateDeposed struct {
@ -168,33 +141,6 @@ func (n *EvalWriteState) Eval(ctx EvalContext) (interface{}, error) {
) )
} }
// EvalWriteStateTainted is an EvalNode implementation that writes
// an InstanceState out to the Tainted list of a resource in the state.
type EvalWriteStateTainted struct {
Name string
ResourceType string
Provider string
Dependencies []string
State **InstanceState
// Index indicates which instance in the Tainted list to target, or -1 to append.
Index int
}
// EvalWriteStateTainted is an EvalNode implementation that writes the
// one of the tainted InstanceStates for a specific resource out of the state.
func (n *EvalWriteStateTainted) Eval(ctx EvalContext) (interface{}, error) {
return writeInstanceToState(ctx, n.Name, n.ResourceType, n.Provider, n.Dependencies,
func(rs *ResourceState) error {
if n.Index == -1 {
rs.Tainted = append(rs.Tainted, *n.State)
} else {
rs.Tainted[n.Index] = *n.State
}
return nil
},
)
}
// EvalWriteStateDeposed is an EvalNode implementation that writes // EvalWriteStateDeposed is an EvalNode implementation that writes
// an InstanceState out to the Deposed list of a resource in the state. // an InstanceState out to the Deposed list of a resource in the state.
type EvalWriteStateDeposed struct { type EvalWriteStateDeposed struct {
@ -339,7 +285,8 @@ func (n *EvalDeposeState) Eval(ctx EvalContext) (interface{}, error) {
// EvalUndeposeState is an EvalNode implementation that reads the // EvalUndeposeState is an EvalNode implementation that reads the
// InstanceState for a specific resource out of the state. // InstanceState for a specific resource out of the state.
type EvalUndeposeState struct { type EvalUndeposeState struct {
Name string Name string
State **InstanceState
} }
// TODO: test // TODO: test
@ -370,7 +317,7 @@ func (n *EvalUndeposeState) Eval(ctx EvalContext) (interface{}, error) {
// Undepose // Undepose
idx := len(rs.Deposed) - 1 idx := len(rs.Deposed) - 1
rs.Primary = rs.Deposed[idx] rs.Primary = rs.Deposed[idx]
rs.Deposed[idx] = nil rs.Deposed[idx] = *n.State
return nil, nil return nil, nil
} }

View File

@ -88,21 +88,6 @@ func TestEvalReadState(t *testing.T) {
}, },
ExpectedInstanceId: "i-abc123", ExpectedInstanceId: "i-abc123",
}, },
"ReadStateTainted gets tainted instance": {
Resources: map[string]*ResourceState{
"aws_instance.bar": &ResourceState{
Tainted: []*InstanceState{
&InstanceState{ID: "i-abc123"},
},
},
},
Node: &EvalReadStateTainted{
Name: "aws_instance.bar",
Output: &output,
Index: 0,
},
ExpectedInstanceId: "i-abc123",
},
"ReadStateDeposed gets deposed instance": { "ReadStateDeposed gets deposed instance": {
Resources: map[string]*ResourceState{ Resources: map[string]*ResourceState{
"aws_instance.bar": &ResourceState{ "aws_instance.bar": &ResourceState{
@ -175,32 +160,6 @@ restype.resname:
`) `)
} }
func TestEvalWriteStateTainted(t *testing.T) {
state := &State{}
ctx := new(MockEvalContext)
ctx.StateState = state
ctx.StateLock = new(sync.RWMutex)
ctx.PathPath = rootModulePath
is := &InstanceState{ID: "i-abc123"}
node := &EvalWriteStateTainted{
Name: "restype.resname",
ResourceType: "restype",
State: &is,
Index: -1,
}
_, err := node.Eval(ctx)
if err != nil {
t.Fatalf("Got err: %#v", err)
}
checkStateString(t, state, `
restype.resname: (1 tainted)
ID = <not created>
Tainted ID 1 = i-abc123
`)
}
func TestEvalWriteStateDeposed(t *testing.T) { func TestEvalWriteStateDeposed(t *testing.T) {
state := &State{} state := &State{}
ctx := new(MockEvalContext) ctx := new(MockEvalContext)

View File

@ -295,16 +295,11 @@ provider.aws (close)
const testBuiltinGraphBuilderVerboseStr = ` const testBuiltinGraphBuilderVerboseStr = `
aws_instance.db aws_instance.db
aws_instance.db (destroy tainted)
aws_instance.db (destroy) aws_instance.db (destroy)
aws_instance.db (destroy tainted)
aws_instance.web (destroy tainted)
aws_instance.db (destroy) aws_instance.db (destroy)
aws_instance.web (destroy) aws_instance.web (destroy)
aws_instance.web aws_instance.web
aws_instance.db aws_instance.db
aws_instance.web (destroy tainted)
provider.aws
aws_instance.web (destroy) aws_instance.web (destroy)
provider.aws provider.aws
provider.aws provider.aws

View File

@ -20,9 +20,9 @@ type GraphNodeCountDependent interface {
type GraphNodeConfigResource struct { type GraphNodeConfigResource struct {
Resource *config.Resource Resource *config.Resource
// If this is set to anything other than destroyModeNone, then this // If set to true, this resource represents a resource
// resource represents a resource that will be destroyed in some way. // that will be destroyed in some way.
DestroyMode GraphNodeDestroyMode Destroy bool
// Used during DynamicExpand to target indexes // Used during DynamicExpand to target indexes
Targets []ResourceAddress Targets []ResourceAddress
@ -32,10 +32,10 @@ type GraphNodeConfigResource struct {
func (n *GraphNodeConfigResource) Copy() *GraphNodeConfigResource { func (n *GraphNodeConfigResource) Copy() *GraphNodeConfigResource {
ncr := &GraphNodeConfigResource{ ncr := &GraphNodeConfigResource{
Resource: n.Resource.Copy(), Resource: n.Resource.Copy(),
DestroyMode: n.DestroyMode, Destroy: n.Destroy,
Targets: make([]ResourceAddress, 0, len(n.Targets)), Targets: make([]ResourceAddress, 0, len(n.Targets)),
Path: make([]string, 0, len(n.Path)), Path: make([]string, 0, len(n.Path)),
} }
for _, t := range n.Targets { for _, t := range n.Targets {
ncr.Targets = append(ncr.Targets, *t.Copy()) ncr.Targets = append(ncr.Targets, *t.Copy())
@ -121,22 +121,15 @@ func (n *GraphNodeConfigResource) VarWalk(fn func(config.InterpolatedVariable))
func (n *GraphNodeConfigResource) Name() string { func (n *GraphNodeConfigResource) Name() string {
result := n.Resource.Id() result := n.Resource.Id()
switch n.DestroyMode { if n.Destroy {
case DestroyNone:
case DestroyPrimary:
result += " (destroy)" result += " (destroy)"
case DestroyTainted:
result += " (destroy tainted)"
default:
result += " (unknown destroy type)"
} }
return result return result
} }
// GraphNodeDotter impl. // GraphNodeDotter impl.
func (n *GraphNodeConfigResource) DotNode(name string, opts *GraphDotOpts) *dot.Node { func (n *GraphNodeConfigResource) DotNode(name string, opts *GraphDotOpts) *dot.Node {
if n.DestroyMode != DestroyNone && !opts.Verbose { if n.Destroy && !opts.Verbose {
return nil return nil
} }
return dot.NewNode(name, map[string]string{ return dot.NewNode(name, map[string]string{
@ -162,23 +155,18 @@ func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error)
// Start creating the steps // Start creating the steps
steps := make([]GraphTransformer, 0, 5) steps := make([]GraphTransformer, 0, 5)
// Primary and non-destroy modes are responsible for creating/destroying // Expand counts.
// all the nodes, expanding counts. steps = append(steps, &ResourceCountTransformer{
switch n.DestroyMode { Resource: n.Resource,
case DestroyNone, DestroyPrimary: Destroy: n.Destroy,
steps = append(steps, &ResourceCountTransformer{ Targets: n.Targets,
Resource: n.Resource, })
Destroy: n.DestroyMode != DestroyNone,
Targets: n.Targets,
})
}
// Additional destroy modifications. // Additional destroy modifications.
switch n.DestroyMode { if n.Destroy {
case DestroyPrimary: // If we're destroying a primary or tainted resource, we want to
// If we're destroying the primary instance, then we want to
// expand orphans, which have all the same semantics in a destroy // expand orphans, which have all the same semantics in a destroy
// as a primary. // as a primary or tainted resource.
steps = append(steps, &OrphanTransformer{ steps = append(steps, &OrphanTransformer{
State: state, State: state,
View: n.Resource.Id(), View: n.Resource.Id(),
@ -188,19 +176,12 @@ func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error)
State: state, State: state,
View: n.Resource.Id(), View: n.Resource.Id(),
}) })
case DestroyTainted:
// If we're only destroying tainted resources, then we only
// want to find tainted resources and destroy them here.
steps = append(steps, &TaintedTransformer{
State: state,
View: n.Resource.Id(),
})
} }
// We always want to apply targeting // We always want to apply targeting
steps = append(steps, &TargetsTransformer{ steps = append(steps, &TargetsTransformer{
ParsedTargets: n.Targets, ParsedTargets: n.Targets,
Destroy: n.DestroyMode != DestroyNone, Destroy: n.Destroy,
}) })
// Always end with the root being added // Always end with the root being added
@ -258,9 +239,9 @@ func (n *GraphNodeConfigResource) ProvisionedBy() []string {
} }
// GraphNodeDestroyable // GraphNodeDestroyable
func (n *GraphNodeConfigResource) DestroyNode(mode GraphNodeDestroyMode) GraphNodeDestroy { func (n *GraphNodeConfigResource) DestroyNode() GraphNodeDestroy {
// If we're already a destroy node, then don't do anything // If we're already a destroy node, then don't do anything
if n.DestroyMode != DestroyNone { if n.Destroy {
return nil return nil
} }
@ -268,7 +249,8 @@ func (n *GraphNodeConfigResource) DestroyNode(mode GraphNodeDestroyMode) GraphNo
GraphNodeConfigResource: *n.Copy(), GraphNodeConfigResource: *n.Copy(),
Original: n, Original: n,
} }
result.DestroyMode = mode result.Destroy = true
return result return result
} }
@ -276,7 +258,7 @@ func (n *GraphNodeConfigResource) DestroyNode(mode GraphNodeDestroyMode) GraphNo
func (n *GraphNodeConfigResource) Noop(opts *NoopOpts) bool { func (n *GraphNodeConfigResource) Noop(opts *NoopOpts) bool {
log.Printf("[DEBUG] Checking resource noop: %s", n.Name()) log.Printf("[DEBUG] Checking resource noop: %s", n.Name())
// We don't have any noop optimizations for destroy nodes yet // We don't have any noop optimizations for destroy nodes yet
if n.DestroyMode != DestroyNone { if n.Destroy {
log.Printf("[DEBUG] Destroy node, not a noop") log.Printf("[DEBUG] Destroy node, not a noop")
return false return false
} }
@ -365,9 +347,9 @@ func (n *GraphNodeConfigResourceFlat) ProvisionedBy() []string {
} }
// GraphNodeDestroyable impl. // GraphNodeDestroyable impl.
func (n *GraphNodeConfigResourceFlat) DestroyNode(mode GraphNodeDestroyMode) GraphNodeDestroy { func (n *GraphNodeConfigResourceFlat) DestroyNode() GraphNodeDestroy {
// Get our parent destroy node. If we don't have any, just return // Get our parent destroy node. If we don't have any, just return
raw := n.GraphNodeConfigResource.DestroyNode(mode) raw := n.GraphNodeConfigResource.DestroyNode()
if raw == nil { if raw == nil {
return nil return nil
} }
@ -423,13 +405,8 @@ type graphNodeResourceDestroy struct {
} }
func (n *graphNodeResourceDestroy) CreateBeforeDestroy() bool { func (n *graphNodeResourceDestroy) CreateBeforeDestroy() bool {
// CBD is enabled if the resource enables it in addition to us // CBD is enabled if the resource enables it
// being responsible for destroying the primary state. The primary return n.Original.Resource.Lifecycle.CreateBeforeDestroy && n.Destroy
// state destroy node is the only destroy node that needs to be
// "shuffled" according to the CBD rules, since tainted resources
// don't have the same inverse dependencies.
return n.Original.Resource.Lifecycle.CreateBeforeDestroy &&
n.DestroyMode == DestroyPrimary
} }
func (n *graphNodeResourceDestroy) CreateNode() dag.Vertex { func (n *graphNodeResourceDestroy) CreateNode() dag.Vertex {
@ -437,43 +414,14 @@ func (n *graphNodeResourceDestroy) CreateNode() dag.Vertex {
} }
func (n *graphNodeResourceDestroy) DestroyInclude(d *ModuleDiff, s *ModuleState) bool { func (n *graphNodeResourceDestroy) DestroyInclude(d *ModuleDiff, s *ModuleState) bool {
switch n.DestroyMode { if n.Destroy {
case DestroyPrimary: return n.destroyInclude(d, s)
return n.destroyIncludePrimary(d, s)
case DestroyTainted:
return n.destroyIncludeTainted(d, s)
default:
return true
} }
return true
} }
func (n *graphNodeResourceDestroy) destroyIncludeTainted( func (n *graphNodeResourceDestroy) destroyInclude(
d *ModuleDiff, s *ModuleState) bool {
// If there is no state, there can't by any tainted.
if s == nil {
return false
}
// Grab the ID which is the prefix (in the case count > 0 at some point)
prefix := n.Original.Resource.Id()
// Go through the resources and find any with our prefix. If there
// are any tainted, we need to keep it.
for k, v := range s.Resources {
if !strings.HasPrefix(k, prefix) {
continue
}
if len(v.Tainted) > 0 {
return true
}
}
// We didn't find any tainted nodes, return
return false
}
func (n *graphNodeResourceDestroy) destroyIncludePrimary(
d *ModuleDiff, s *ModuleState) bool { d *ModuleDiff, s *ModuleState) bool {
// Get the count, and specifically the raw value of the count // Get the count, and specifically the raw value of the count
// (with interpolations and all). If the count is NOT a static "1", // (with interpolations and all). If the count is NOT a static "1",
@ -516,7 +464,7 @@ func (n *graphNodeResourceDestroy) destroyIncludePrimary(
// If the count is set to ANYTHING other than a static "1" (variable, // If the count is set to ANYTHING other than a static "1" (variable,
// computed attribute, static number greater than 1), then we keep the // computed attribute, static number greater than 1), then we keep the
// destroy, since it is required for dynamic graph expansion to find // destroy, since it is required for dynamic graph expansion to find
// orphan/tainted count objects. // orphan count objects.
// //
// This isn't ideal logic, but its strictly better without introducing // This isn't ideal logic, but its strictly better without introducing
// new impossibilities. It breaks the cycle in practical cases, and the // new impossibilities. It breaks the cycle in practical cases, and the
@ -535,9 +483,9 @@ func (n *graphNodeResourceDestroy) destroyIncludePrimary(
// only for resources in the diff that match our resource or a count-index // only for resources in the diff that match our resource or a count-index
// of our resource that are marked for destroy. // of our resource that are marked for destroy.
if d != nil { if d != nil {
for k, d := range d.Resources { for k, v := range d.Resources {
match := k == prefix || strings.HasPrefix(k, prefix+".") match := k == prefix || strings.HasPrefix(k, prefix+".")
if match && d.Destroy { if match && v.Destroy {
return true return true
} }
} }

View File

@ -42,7 +42,6 @@ type Resource struct {
State *InstanceState State *InstanceState
Provisioners []*ResourceProvisionerConfig Provisioners []*ResourceProvisionerConfig
Flags ResourceFlag Flags ResourceFlag
TaintedIndex int
} }
// ResourceKind specifies what kind of instance we're working with, whether // ResourceKind specifies what kind of instance we're working with, whether
@ -53,7 +52,6 @@ const (
FlagPrimary ResourceFlag = 1 << iota FlagPrimary ResourceFlag = 1 << iota
FlagTainted FlagTainted
FlagOrphan FlagOrphan
FlagHasTainted
FlagReplacePrimary FlagReplacePrimary
FlagDeposed FlagDeposed
) )

View File

@ -327,7 +327,7 @@ func (s *State) removeInstance(path []string, r *ResourceState, v *InstanceState
} }
// Check lists // Check lists
lists := [][]*InstanceState{r.Tainted, r.Deposed} lists := [][]*InstanceState{r.Deposed}
for _, is := range lists { for _, is := range lists {
for i, instance := range is { for i, instance := range is {
if instance == v { if instance == v {
@ -780,7 +780,7 @@ func (m *ModuleState) prune() {
for k, v := range m.Resources { for k, v := range m.Resources {
v.prune() v.prune()
if (v.Primary == nil || v.Primary.ID == "") && len(v.Tainted) == 0 && len(v.Deposed) == 0 { if (v.Primary == nil || v.Primary.ID == "") && len(v.Deposed) == 0 {
delete(m.Resources, k) delete(m.Resources, k)
} }
} }
@ -826,8 +826,8 @@ func (m *ModuleState) String() string {
} }
taintStr := "" taintStr := ""
if len(rs.Tainted) > 0 { if rs.Primary.Tainted {
taintStr = fmt.Sprintf(" (%d tainted)", len(rs.Tainted)) taintStr = " (tainted)"
} }
deposedStr := "" deposedStr := ""
@ -860,12 +860,12 @@ func (m *ModuleState) String() string {
buf.WriteString(fmt.Sprintf(" %s = %s\n", ak, av)) buf.WriteString(fmt.Sprintf(" %s = %s\n", ak, av))
} }
for idx, t := range rs.Tainted {
buf.WriteString(fmt.Sprintf(" Tainted ID %d = %s\n", idx+1, t.ID))
}
for idx, t := range rs.Deposed { for idx, t := range rs.Deposed {
buf.WriteString(fmt.Sprintf(" Deposed ID %d = %s\n", idx+1, t.ID)) taintStr := ""
if t.Tainted {
taintStr = " (tainted)"
}
buf.WriteString(fmt.Sprintf(" Deposed ID %d = %s%s\n", idx+1, t.ID, taintStr))
} }
if len(rs.Dependencies) > 0 { if len(rs.Dependencies) > 0 {
@ -1031,22 +1031,17 @@ type ResourceState struct {
// This is the instances on which providers will act. // This is the instances on which providers will act.
Primary *InstanceState `json:"primary"` Primary *InstanceState `json:"primary"`
// Tainted is used to track any underlying instances that
// have been created but are in a bad or unknown state and
// need to be cleaned up subsequently. In the
// standard case, there is only at most a single instance.
// However, in pathological cases, it is possible for the number
// of instances to accumulate.
Tainted []*InstanceState `json:"tainted,omitempty"`
// Deposed is used in the mechanics of CreateBeforeDestroy: the existing // Deposed is used in the mechanics of CreateBeforeDestroy: the existing
// Primary is Deposed to get it out of the way for the replacement Primary to // Primary is Deposed to get it out of the way for the replacement Primary to
// be created by Apply. If the replacement Primary creates successfully, the // be created by Apply. If the replacement Primary creates successfully, the
// Deposed instance is cleaned up. If there were problems creating the // Deposed instance is cleaned up.
// replacement, the instance remains in the Deposed list so it can be //
// destroyed in a future run. Functionally, Deposed instances are very // If there were problems creating the replacement Primary, the Deposed
// similar to Tainted instances in that Terraform is only tracking them in // instance and the (now tainted) replacement Primary will be swapped so the
// order to remember to destroy them. // tainted replacement will be cleaned up instead.
//
// An instance will remain in the Deposed list until it is successfully
// destroyed and purged.
Deposed []*InstanceState `json:"deposed,omitempty"` Deposed []*InstanceState `json:"deposed,omitempty"`
// Provider is used when a resource is connected to a provider with an alias. // Provider is used when a resource is connected to a provider with an alias.
@ -1083,81 +1078,21 @@ func (s *ResourceState) Equal(other *ResourceState) bool {
return false return false
} }
// Tainted
taints := make(map[string]*InstanceState)
for _, t := range other.Tainted {
if t == nil {
continue
}
taints[t.ID] = t
}
for _, t := range s.Tainted {
if t == nil {
continue
}
otherT, ok := taints[t.ID]
if !ok {
return false
}
delete(taints, t.ID)
if !t.Equal(otherT) {
return false
}
}
// This means that we have stuff in other tainted that we don't
// have, so it is not equal.
if len(taints) > 0 {
return false
}
return true return true
} }
// Taint takes the primary state and marks it as tainted. If there is no // Taint marks a resource as tainted.
// primary state, this does nothing.
func (r *ResourceState) Taint() { func (r *ResourceState) Taint() {
// If there is no primary, nothing to do if r.Primary != nil {
if r.Primary == nil { r.Primary.Tainted = true
return
} }
// Shuffle to the end of the taint list and set primary to nil
r.Tainted = append(r.Tainted, r.Primary)
r.Primary = nil
} }
// Untaint takes a tainted InstanceState and marks it as primary. // Untaint unmarks a resource as tainted.
// The index argument is used to select a single InstanceState from the func (r *ResourceState) Untaint() {
// array of Tainted when there are more than one. If index is -1, the
// first Tainted InstanceState will be untainted iff there is only one
// Tainted InstanceState. Index must be >= 0 to specify an InstanceState
// when Tainted has more than one member.
func (r *ResourceState) Untaint(index int) error {
if len(r.Tainted) == 0 {
return fmt.Errorf("Nothing to untaint.")
}
if r.Primary != nil { if r.Primary != nil {
return fmt.Errorf("Resource has a primary instance in the state that would be overwritten by untainting. If you want to restore a tainted resource to primary, taint the existing primary instance first.") r.Primary.Tainted = false
} }
if index == -1 && len(r.Tainted) > 1 {
return fmt.Errorf("There are %d tainted instances for this resource, please specify an index to select which one to untaint.", len(r.Tainted))
}
if index == -1 {
index = 0
}
if index >= len(r.Tainted) {
return fmt.Errorf("There are %d tainted instances for this resource, the index specified (%d) is out of range.", len(r.Tainted), index)
}
// Perform the untaint
r.Primary = r.Tainted[index]
r.Tainted = append(r.Tainted[:index], r.Tainted[index+1:]...)
return nil
} }
func (r *ResourceState) init() { func (r *ResourceState) init() {
@ -1176,19 +1111,12 @@ func (r *ResourceState) deepcopy() *ResourceState {
Type: r.Type, Type: r.Type,
Dependencies: nil, Dependencies: nil,
Primary: r.Primary.DeepCopy(), Primary: r.Primary.DeepCopy(),
Tainted: nil,
Provider: r.Provider, Provider: r.Provider,
} }
if r.Dependencies != nil { if r.Dependencies != nil {
n.Dependencies = make([]string, len(r.Dependencies)) n.Dependencies = make([]string, len(r.Dependencies))
copy(n.Dependencies, r.Dependencies) copy(n.Dependencies, r.Dependencies)
} }
if r.Tainted != nil {
n.Tainted = make([]*InstanceState, 0, len(r.Tainted))
for _, inst := range r.Tainted {
n.Tainted = append(n.Tainted, inst.DeepCopy())
}
}
if r.Deposed != nil { if r.Deposed != nil {
n.Deposed = make([]*InstanceState, 0, len(r.Deposed)) n.Deposed = make([]*InstanceState, 0, len(r.Deposed))
for _, inst := range r.Deposed { for _, inst := range r.Deposed {
@ -1201,20 +1129,7 @@ func (r *ResourceState) deepcopy() *ResourceState {
// prune is used to remove any instances that are no longer required // prune is used to remove any instances that are no longer required
func (r *ResourceState) prune() { func (r *ResourceState) prune() {
n := len(r.Tainted) n := len(r.Deposed)
for i := 0; i < n; i++ {
inst := r.Tainted[i]
if inst == nil || inst.ID == "" {
copy(r.Tainted[i:], r.Tainted[i+1:])
r.Tainted[n-1] = nil
n--
i--
}
}
r.Tainted = r.Tainted[:n]
n = len(r.Deposed)
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
inst := r.Deposed[i] inst := r.Deposed[i]
if inst == nil || inst.ID == "" { if inst == nil || inst.ID == "" {
@ -1263,6 +1178,9 @@ type InstanceState struct {
// ignored by Terraform core. It's meant to be used for accounting by // ignored by Terraform core. It's meant to be used for accounting by
// external client code. // external client code.
Meta map[string]string `json:"meta,omitempty"` Meta map[string]string `json:"meta,omitempty"`
// Tainted is used to mark a resource for recreation.
Tainted bool `json:"tainted,omitempty"`
} }
func (i *InstanceState) init() { func (i *InstanceState) init() {
@ -1282,6 +1200,7 @@ func (i *InstanceState) DeepCopy() *InstanceState {
n := &InstanceState{ n := &InstanceState{
ID: i.ID, ID: i.ID,
Ephemeral: *i.Ephemeral.DeepCopy(), Ephemeral: *i.Ephemeral.DeepCopy(),
Tainted: i.Tainted,
} }
if i.Attributes != nil { if i.Attributes != nil {
n.Attributes = make(map[string]string, len(i.Attributes)) n.Attributes = make(map[string]string, len(i.Attributes))
@ -1343,6 +1262,10 @@ func (s *InstanceState) Equal(other *InstanceState) bool {
} }
} }
if s.Tainted != other.Tainted {
return false
}
return true return true
} }
@ -1413,6 +1336,8 @@ func (i *InstanceState) String() string {
buf.WriteString(fmt.Sprintf("%s = %s\n", ak, av)) buf.WriteString(fmt.Sprintf("%s = %s\n", ak, av))
} }
buf.WriteString(fmt.Sprintf("Tainted = %t\n", i.Tainted))
return buf.String() return buf.String()
} }

View File

@ -133,11 +133,6 @@ func stateAddFunc_Resource_Resource(s *State, fromAddr, addr *ResourceAddress, r
} }
} }
// Move all tainted
if len(src.Tainted) > 0 {
resource.Tainted = src.Tainted
}
// Move all deposed // Move all deposed
if len(src.Deposed) > 0 { if len(src.Deposed) > 0 {
resource.Deposed = src.Deposed resource.Deposed = src.Deposed
@ -281,23 +276,12 @@ func stateAddInitAddr(s *State, addr *ResourceAddress) (interface{}, bool) {
exists = true exists = true
instance := &InstanceState{} instance := &InstanceState{}
switch addr.InstanceType { switch addr.InstanceType {
case TypePrimary: case TypePrimary, TypeTainted:
if v := resource.Primary; v != nil { if v := resource.Primary; v != nil {
instance = resource.Primary instance = resource.Primary
} else { } else {
exists = false exists = false
} }
case TypeTainted:
idx := addr.Index
if addr.Index < 0 {
idx = 0
}
if len(resource.Tainted) > idx {
instance = resource.Tainted[idx]
} else {
resource.Tainted = append(resource.Tainted, instance)
exists = false
}
case TypeDeposed: case TypeDeposed:
idx := addr.Index idx := addr.Index
if addr.Index < 0 { if addr.Index < 0 {

View File

@ -256,16 +256,15 @@ func TestStateAdd(t *testing.T) {
}, },
}, },
"ResourceState w/ tainted => Resource Addr (new)": { "ResourceState tainted => Resource Addr (new)": {
false, false,
"aws_instance.bar", "aws_instance.bar",
"aws_instance.foo", "aws_instance.foo",
&ResourceState{ &ResourceState{
Type: "test_instance", Type: "test_instance",
Tainted: []*InstanceState{ Primary: &InstanceState{
&InstanceState{ ID: "foo",
ID: "foo", Tainted: true,
},
}, },
}, },
@ -276,12 +275,10 @@ func TestStateAdd(t *testing.T) {
Path: []string{"root"}, Path: []string{"root"},
Resources: map[string]*ResourceState{ Resources: map[string]*ResourceState{
"aws_instance.foo": &ResourceState{ "aws_instance.foo": &ResourceState{
Type: "test_instance", Type: "test_instance",
Primary: &InstanceState{}, Primary: &InstanceState{
Tainted: []*InstanceState{ ID: "foo",
&InstanceState{ Tainted: true,
ID: "foo",
},
}, },
}, },
}, },

View File

@ -133,19 +133,6 @@ func (f *StateFilter) filterSingle(a *ResourceAddress) []*StateFilterResult {
}) })
} }
for _, instance := range r.Tainted {
if f.relevant(a, instance) {
addr.InstanceType = TypeTainted
addr.InstanceTypeSet = true
results = append(results, &StateFilterResult{
Path: addr.Path,
Address: addr.String(),
Parent: resourceResult,
Value: instance,
})
}
}
for _, instance := range r.Deposed { for _, instance := range r.Deposed {
if f.relevant(a, instance) { if f.relevant(a, instance) {
addr.InstanceType = TypeDeposed addr.InstanceType = TypeDeposed

View File

@ -742,11 +742,14 @@ func TestResourceStateEqual(t *testing.T) {
{ {
false, false,
&ResourceState{ &ResourceState{
Tainted: nil, Primary: &InstanceState{
ID: "foo",
},
}, },
&ResourceState{ &ResourceState{
Tainted: []*InstanceState{ Primary: &InstanceState{
&InstanceState{ID: "foo"}, ID: "foo",
Tainted: true,
}, },
}, },
}, },
@ -754,28 +757,15 @@ func TestResourceStateEqual(t *testing.T) {
{ {
true, true,
&ResourceState{ &ResourceState{
Tainted: []*InstanceState{ Primary: &InstanceState{
&InstanceState{ID: "foo"}, ID: "foo",
Tainted: true,
}, },
}, },
&ResourceState{ &ResourceState{
Tainted: []*InstanceState{ Primary: &InstanceState{
&InstanceState{ID: "foo"}, ID: "foo",
}, Tainted: true,
},
},
{
true,
&ResourceState{
Tainted: []*InstanceState{
&InstanceState{ID: "foo"},
nil,
},
},
&ResourceState{
Tainted: []*InstanceState{
&InstanceState{ID: "foo"},
}, },
}, },
}, },
@ -801,28 +791,29 @@ func TestResourceStateTaint(t *testing.T) {
&ResourceState{}, &ResourceState{},
}, },
"primary, no tainted": { "primary, not tainted": {
&ResourceState{ &ResourceState{
Primary: &InstanceState{ID: "foo"}, Primary: &InstanceState{ID: "foo"},
}, },
&ResourceState{ &ResourceState{
Tainted: []*InstanceState{ Primary: &InstanceState{
&InstanceState{ID: "foo"}, ID: "foo",
Tainted: true,
}, },
}, },
}, },
"primary, with tainted": { "primary, tainted": {
&ResourceState{ &ResourceState{
Primary: &InstanceState{ID: "foo"}, Primary: &InstanceState{
Tainted: []*InstanceState{ ID: "foo",
&InstanceState{ID: "bar"}, Tainted: true,
}, },
}, },
&ResourceState{ &ResourceState{
Tainted: []*InstanceState{ Primary: &InstanceState{
&InstanceState{ID: "bar"}, ID: "foo",
&InstanceState{ID: "foo"}, Tainted: true,
}, },
}, },
}, },
@ -841,92 +832,36 @@ func TestResourceStateTaint(t *testing.T) {
func TestResourceStateUntaint(t *testing.T) { func TestResourceStateUntaint(t *testing.T) {
cases := map[string]struct { cases := map[string]struct {
Input *ResourceState Input *ResourceState
Index func() int
ExpectedOutput *ResourceState ExpectedOutput *ResourceState
ExpectedErrMsg string
}{ }{
"no primary, no tainted, err": { "no primary, err": {
Input: &ResourceState{}, Input: &ResourceState{},
ExpectedOutput: &ResourceState{}, ExpectedOutput: &ResourceState{},
ExpectedErrMsg: "Nothing to untaint",
}, },
"one tainted, no primary": { "primary, not tainted": {
Input: &ResourceState{ Input: &ResourceState{
Tainted: []*InstanceState{ Primary: &InstanceState{ID: "foo"},
&InstanceState{ID: "foo"}, },
ExpectedOutput: &ResourceState{
Primary: &InstanceState{ID: "foo"},
},
},
"primary, tainted": {
Input: &ResourceState{
Primary: &InstanceState{
ID: "foo",
Tainted: true,
}, },
}, },
ExpectedOutput: &ResourceState{ ExpectedOutput: &ResourceState{
Primary: &InstanceState{ID: "foo"}, Primary: &InstanceState{ID: "foo"},
Tainted: []*InstanceState{},
}, },
}, },
"one tainted, existing primary error": {
Input: &ResourceState{
Primary: &InstanceState{ID: "foo"},
Tainted: []*InstanceState{
&InstanceState{ID: "foo"},
},
},
ExpectedErrMsg: "Resource has a primary",
},
"multiple tainted, no index": {
Input: &ResourceState{
Tainted: []*InstanceState{
&InstanceState{ID: "bar"},
&InstanceState{ID: "foo"},
},
},
ExpectedErrMsg: "please specify an index",
},
"multiple tainted, with index": {
Input: &ResourceState{
Tainted: []*InstanceState{
&InstanceState{ID: "bar"},
&InstanceState{ID: "foo"},
},
},
Index: func() int { return 1 },
ExpectedOutput: &ResourceState{
Primary: &InstanceState{ID: "foo"},
Tainted: []*InstanceState{
&InstanceState{ID: "bar"},
},
},
},
"index out of bounds error": {
Input: &ResourceState{
Tainted: []*InstanceState{
&InstanceState{ID: "bar"},
&InstanceState{ID: "foo"},
},
},
Index: func() int { return 2 },
ExpectedErrMsg: "out of range",
},
} }
for k, tc := range cases { for k, tc := range cases {
index := -1 tc.Input.Untaint()
if tc.Index != nil {
index = tc.Index()
}
err := tc.Input.Untaint(index)
if tc.ExpectedErrMsg == "" && err != nil {
t.Fatalf("[%s] unexpected err: %s", k, err)
}
if tc.ExpectedErrMsg != "" {
if strings.Contains(err.Error(), tc.ExpectedErrMsg) {
continue
}
t.Fatalf("[%s] expected err: %s to contain: %s",
k, err, tc.ExpectedErrMsg)
}
if !reflect.DeepEqual(tc.Input, tc.ExpectedOutput) { if !reflect.DeepEqual(tc.Input, tc.ExpectedOutput) {
t.Fatalf( t.Fatalf(
"Failure: %s\n\nExpected: %#v\n\nGot: %#v", "Failure: %s\n\nExpected: %#v\n\nGot: %#v",
@ -1152,7 +1087,7 @@ func TestInstanceState_MergeDiff(t *testing.T) {
} }
func TestInstanceState_MergeDiff_nil(t *testing.T) { func TestInstanceState_MergeDiff_nil(t *testing.T) {
var is *InstanceState = nil var is *InstanceState
diff := &InstanceDiff{ diff := &InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{ Attributes: map[string]*ResourceAttrDiff{
@ -1504,7 +1439,7 @@ func TestUpgradeV0State(t *testing.T) {
foo.Primary.Attributes["key"] != "val" { foo.Primary.Attributes["key"] != "val" {
t.Fatalf("bad: %#v", foo) t.Fatalf("bad: %#v", foo)
} }
if len(foo.Tainted) > 0 { if foo.Primary.Tainted {
t.Fatalf("bad: %#v", foo) t.Fatalf("bad: %#v", foo)
} }
@ -1512,16 +1447,9 @@ func TestUpgradeV0State(t *testing.T) {
if bar.Type != "test_resource" { if bar.Type != "test_resource" {
t.Fatalf("bad: %#v", bar) t.Fatalf("bad: %#v", bar)
} }
if bar.Primary != nil { if !bar.Primary.Tainted {
t.Fatalf("bad: %#v", bar) t.Fatalf("bad: %#v", bar)
} }
if len(bar.Tainted) != 1 {
t.Fatalf("bad: %#v", bar)
}
bt := bar.Tainted[0]
if bt.ID != "1234" || bt.Attributes["a"] != "b" {
t.Fatalf("bad: %#v", bt)
}
} }
func TestParseResourceStateKey(t *testing.T) { func TestParseResourceStateKey(t *testing.T) {

View File

@ -10,8 +10,9 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/hashicorp/terraform/config"
"log" "log"
"github.com/hashicorp/terraform/config"
) )
// The format byte is prefixed into the state file format so that we have // The format byte is prefixed into the state file format so that we have
@ -342,16 +343,14 @@ func (old *StateV0) upgrade() (*State, error) {
root.Resources[id] = newRs root.Resources[id] = newRs
// Migrate to an instance state // Migrate to an instance state
instance := &InstanceState{ newRs.Primary = &InstanceState{
ID: rs.ID, ID: rs.ID,
Attributes: rs.Attributes, Attributes: rs.Attributes,
} }
// Check if this is the primary or tainted instance // Check if old resource was tainted
if _, ok := old.Tainted[id]; ok { if _, ok := old.Tainted[id]; ok {
newRs.Tainted = append(newRs.Tainted, instance) newRs.Primary.Tainted = true
} else {
newRs.Primary = instance
} }
// Warn if the resource uses Extra, as there is // Warn if the resource uses Extra, as there is

View File

@ -235,18 +235,6 @@ func (old *resourceStateV1) upgrade() (*ResourceState, error) {
return nil, fmt.Errorf("Error upgrading ResourceState V1: %v", err) return nil, fmt.Errorf("Error upgrading ResourceState V1: %v", err)
} }
tainted := make([]*InstanceState, len(old.Tainted))
for i, v := range old.Tainted {
upgraded, err := v.upgrade()
if err != nil {
return nil, fmt.Errorf("Error upgrading ResourceState V1: %v", err)
}
tainted[i] = upgraded
}
if len(tainted) == 0 {
tainted = nil
}
deposed := make([]*InstanceState, len(old.Deposed)) deposed := make([]*InstanceState, len(old.Deposed))
for i, v := range old.Deposed { for i, v := range old.Deposed {
upgraded, err := v.upgrade() upgraded, err := v.upgrade()
@ -263,7 +251,6 @@ func (old *resourceStateV1) upgrade() (*ResourceState, error) {
Type: old.Type, Type: old.Type,
Dependencies: dependencies.([]string), Dependencies: dependencies.([]string),
Primary: primary, Primary: primary,
Tainted: tainted,
Deposed: deposed, Deposed: deposed,
Provider: old.Provider, Provider: old.Provider,
}, nil }, nil

View File

@ -399,9 +399,8 @@ aws_instance.foo:
` `
const testTerraformApplyProvisionerFailStr = ` const testTerraformApplyProvisionerFailStr = `
aws_instance.bar: (1 tainted) aws_instance.bar: (tainted)
ID = <not created> ID = foo
Tainted ID 1 = foo
aws_instance.foo: aws_instance.foo:
ID = foo ID = foo
num = 2 num = 2
@ -409,9 +408,8 @@ aws_instance.foo:
` `
const testTerraformApplyProvisionerFailCreateStr = ` const testTerraformApplyProvisionerFailCreateStr = `
aws_instance.bar: (1 tainted) aws_instance.bar: (tainted)
ID = <not created> ID = foo
Tainted ID 1 = foo
` `
const testTerraformApplyProvisionerFailCreateNoIdStr = ` const testTerraformApplyProvisionerFailCreateNoIdStr = `
@ -419,10 +417,10 @@ const testTerraformApplyProvisionerFailCreateNoIdStr = `
` `
const testTerraformApplyProvisionerFailCreateBeforeDestroyStr = ` const testTerraformApplyProvisionerFailCreateBeforeDestroyStr = `
aws_instance.bar: (1 tainted) aws_instance.bar: (1 deposed)
ID = bar ID = bar
require_new = abc require_new = abc
Tainted ID 1 = foo Deposed ID 1 = foo (tainted)
` `
const testTerraformApplyProvisionerResourceRefStr = ` const testTerraformApplyProvisionerResourceRefStr = `
@ -1242,9 +1240,8 @@ DESTROY/CREATE: aws_instance.bar
STATE: STATE:
aws_instance.bar: (1 tainted) aws_instance.bar: (tainted)
ID = <not created> ID = baz
Tainted ID 1 = baz
aws_instance.foo: aws_instance.foo:
ID = bar ID = bar
num = 2 num = 2

View File

@ -4,14 +4,6 @@ import (
"github.com/hashicorp/terraform/dag" "github.com/hashicorp/terraform/dag"
) )
type GraphNodeDestroyMode byte
const (
DestroyNone GraphNodeDestroyMode = 0
DestroyPrimary GraphNodeDestroyMode = 1 << iota
DestroyTainted
)
// GraphNodeDestroyable is the interface that nodes that can be destroyed // GraphNodeDestroyable is the interface that nodes that can be destroyed
// must implement. This is used to automatically handle the creation of // must implement. This is used to automatically handle the creation of
// destroy nodes in the graph and the dependency ordering of those destroys. // destroy nodes in the graph and the dependency ordering of those destroys.
@ -19,7 +11,7 @@ type GraphNodeDestroyable interface {
// DestroyNode returns the node used for the destroy with the given // DestroyNode returns the node used for the destroy with the given
// mode. If this returns nil, then a destroy node for that mode // mode. If this returns nil, then a destroy node for that mode
// will not be added. // will not be added.
DestroyNode(GraphNodeDestroyMode) GraphNodeDestroy DestroyNode() GraphNodeDestroy
} }
// GraphNodeDestroy is the interface that must implemented by // GraphNodeDestroy is the interface that must implemented by
@ -60,32 +52,6 @@ type DestroyTransformer struct {
func (t *DestroyTransformer) Transform(g *Graph) error { func (t *DestroyTransformer) Transform(g *Graph) error {
var connect, remove []dag.Edge var connect, remove []dag.Edge
modes := []GraphNodeDestroyMode{DestroyPrimary, DestroyTainted}
for _, m := range modes {
connectMode, removeMode, err := t.transform(g, m)
if err != nil {
return err
}
connect = append(connect, connectMode...)
remove = append(remove, removeMode...)
}
// Atomatically add/remove the edges
for _, e := range connect {
g.Connect(e)
}
for _, e := range remove {
g.RemoveEdge(e)
}
return nil
}
func (t *DestroyTransformer) transform(
g *Graph, mode GraphNodeDestroyMode) ([]dag.Edge, []dag.Edge, error) {
var connect, remove []dag.Edge
nodeToCn := make(map[dag.Vertex]dag.Vertex, len(g.Vertices())) nodeToCn := make(map[dag.Vertex]dag.Vertex, len(g.Vertices()))
nodeToDn := make(map[dag.Vertex]dag.Vertex, len(g.Vertices())) nodeToDn := make(map[dag.Vertex]dag.Vertex, len(g.Vertices()))
for _, v := range g.Vertices() { for _, v := range g.Vertices() {
@ -96,7 +62,7 @@ func (t *DestroyTransformer) transform(
} }
// Grab the destroy side of the node and connect it through // Grab the destroy side of the node and connect it through
n := cn.DestroyNode(mode) n := cn.DestroyNode()
if n == nil { if n == nil {
continue continue
} }
@ -155,7 +121,15 @@ func (t *DestroyTransformer) transform(
} }
} }
return connect, remove, nil // Atomatically add/remove the edges
for _, e := range connect {
g.Connect(e)
}
for _, e := range remove {
g.RemoveEdge(e)
}
return nil
} }
// CreateBeforeDestroyTransformer is a GraphTransformer that modifies // CreateBeforeDestroyTransformer is a GraphTransformer that modifies

View File

@ -359,8 +359,9 @@ func TestPruneDestroyTransformer_tainted(t *testing.T) {
Path: RootModulePath, Path: RootModulePath,
Resources: map[string]*ResourceState{ Resources: map[string]*ResourceState{
"aws_instance.bar": &ResourceState{ "aws_instance.bar": &ResourceState{
Tainted: []*InstanceState{ Primary: &InstanceState{
&InstanceState{ID: "foo"}, ID: "foo",
Tainted: true,
}, },
}, },
}, },
@ -399,16 +400,11 @@ func TestPruneDestroyTransformer_tainted(t *testing.T) {
const testTransformDestroyBasicStr = ` const testTransformDestroyBasicStr = `
aws_instance.bar aws_instance.bar
aws_instance.bar (destroy tainted)
aws_instance.bar (destroy) aws_instance.bar (destroy)
aws_instance.foo aws_instance.foo
aws_instance.bar (destroy tainted)
aws_instance.bar (destroy) aws_instance.bar (destroy)
aws_instance.foo aws_instance.foo
aws_instance.foo (destroy tainted)
aws_instance.foo (destroy) aws_instance.foo (destroy)
aws_instance.foo (destroy tainted)
aws_instance.bar (destroy tainted)
aws_instance.foo (destroy) aws_instance.foo (destroy)
aws_instance.bar (destroy) aws_instance.bar (destroy)
` `
@ -456,40 +452,28 @@ aws_instance.foo-bar (destroy)
const testTransformPruneDestroyTaintedStr = ` const testTransformPruneDestroyTaintedStr = `
aws_instance.bar aws_instance.bar
aws_instance.bar (destroy tainted)
aws_instance.foo aws_instance.foo
aws_instance.bar (destroy tainted)
aws_instance.foo aws_instance.foo
` `
const testTransformCreateBeforeDestroyBasicStr = ` const testTransformCreateBeforeDestroyBasicStr = `
aws_instance.web aws_instance.web
aws_instance.web (destroy tainted)
aws_instance.web (destroy tainted)
aws_load_balancer.lb (destroy tainted)
aws_instance.web (destroy) aws_instance.web (destroy)
aws_instance.web aws_instance.web
aws_load_balancer.lb aws_load_balancer.lb
aws_load_balancer.lb (destroy) aws_load_balancer.lb (destroy)
aws_load_balancer.lb aws_load_balancer.lb
aws_instance.web aws_instance.web
aws_load_balancer.lb (destroy tainted)
aws_load_balancer.lb (destroy) aws_load_balancer.lb (destroy)
aws_load_balancer.lb (destroy tainted)
aws_load_balancer.lb (destroy) aws_load_balancer.lb (destroy)
` `
const testTransformCreateBeforeDestroyTwiceStr = ` const testTransformCreateBeforeDestroyTwiceStr = `
aws_autoscale.bar aws_autoscale.bar
aws_autoscale.bar (destroy tainted)
aws_lc.foo aws_lc.foo
aws_autoscale.bar (destroy tainted)
aws_autoscale.bar (destroy) aws_autoscale.bar (destroy)
aws_autoscale.bar aws_autoscale.bar
aws_lc.foo aws_lc.foo
aws_lc.foo (destroy tainted)
aws_lc.foo (destroy tainted)
aws_autoscale.bar (destroy tainted)
aws_lc.foo (destroy) aws_lc.foo (destroy)
aws_autoscale.bar aws_autoscale.bar
aws_autoscale.bar (destroy) aws_autoscale.bar (destroy)

View File

@ -368,11 +368,7 @@ func (n *graphNodeOrphanResource) dependableName() string {
} }
// GraphNodeDestroyable impl. // GraphNodeDestroyable impl.
func (n *graphNodeOrphanResource) DestroyNode(mode GraphNodeDestroyMode) GraphNodeDestroy { func (n *graphNodeOrphanResource) DestroyNode() GraphNodeDestroy {
if mode != DestroyPrimary {
return nil
}
return n return n
} }
@ -402,11 +398,7 @@ func (n *graphNodeOrphanResourceFlat) Path() []string {
} }
// GraphNodeDestroyable impl. // GraphNodeDestroyable impl.
func (n *graphNodeOrphanResourceFlat) DestroyNode(mode GraphNodeDestroyMode) GraphNodeDestroy { func (n *graphNodeOrphanResourceFlat) DestroyNode() GraphNodeDestroy {
if mode != DestroyPrimary {
return nil
}
return n return n
} }

View File

@ -337,7 +337,7 @@ func (n *graphNodeExpandedResource) managedResourceEvalNodes(resource *Resource,
Config: &resourceConfig, Config: &resourceConfig,
Provider: &provider, Provider: &provider,
State: &state, State: &state,
Output: &diff, OutputDiff: &diff,
OutputState: &state, OutputState: &state,
}, },
&EvalCheckPreventDestroy{ &EvalCheckPreventDestroy{
@ -355,10 +355,6 @@ func (n *graphNodeExpandedResource) managedResourceEvalNodes(resource *Resource,
Dependencies: n.StateDependencies(), Dependencies: n.StateDependencies(),
State: &state, State: &state,
}, },
&EvalDiffTainted{
Diff: &diff,
Name: n.stateId(),
},
&EvalWriteDiff{ &EvalWriteDiff{
Name: n.stateId(), Name: n.stateId(),
Diff: &diff, Diff: &diff,
@ -396,7 +392,7 @@ func (n *graphNodeExpandedResource) managedResourceEvalNodes(resource *Resource,
// Apply // Apply
var diffApply *InstanceDiff var diffApply *InstanceDiff
var err error var err error
var createNew, tainted bool var createNew bool
var createBeforeDestroyEnabled bool var createBeforeDestroyEnabled bool
var wasChangeType DiffChangeType var wasChangeType DiffChangeType
nodes = append(nodes, &EvalOpFilter{ nodes = append(nodes, &EvalOpFilter{
@ -460,11 +456,12 @@ func (n *graphNodeExpandedResource) managedResourceEvalNodes(resource *Resource,
}, },
&EvalDiff{ &EvalDiff{
Info: info, Info: info,
Config: &resourceConfig, Config: &resourceConfig,
Provider: &provider, Provider: &provider,
State: &state, Diff: &diffApply,
Output: &diffApply, State: &state,
OutputDiff: &diffApply,
}, },
&EvalIgnoreChanges{ &EvalIgnoreChanges{
Resource: n.Resource, Resource: n.Resource,
@ -515,20 +512,22 @@ func (n *graphNodeExpandedResource) managedResourceEvalNodes(resource *Resource,
Resource: n.Resource, Resource: n.Resource,
InterpResource: resource, InterpResource: resource,
CreateNew: &createNew, CreateNew: &createNew,
Tainted: &tainted,
Error: &err, Error: &err,
}, },
&EvalIf{ &EvalIf{
If: func(ctx EvalContext) (bool, error) { If: func(ctx EvalContext) (bool, error) {
if createBeforeDestroyEnabled { return createBeforeDestroyEnabled && err != nil, nil
tainted = err != nil
}
failure := tainted || err != nil
return createBeforeDestroyEnabled && failure, nil
}, },
Then: &EvalUndeposeState{ Then: &EvalUndeposeState{
Name: n.stateId(), Name: n.stateId(),
State: &state,
},
Else: &EvalWriteState{
Name: n.stateId(),
ResourceType: n.Resource.Type,
Provider: n.Resource.Provider,
Dependencies: n.StateDependencies(),
State: &state,
}, },
}, },
@ -540,38 +539,6 @@ func (n *graphNodeExpandedResource) managedResourceEvalNodes(resource *Resource,
Diff: nil, Diff: nil,
}, },
&EvalIf{
If: func(ctx EvalContext) (bool, error) {
return tainted, nil
},
Then: &EvalSequence{
Nodes: []EvalNode{
&EvalWriteStateTainted{
Name: n.stateId(),
ResourceType: n.Resource.Type,
Provider: n.Resource.Provider,
Dependencies: n.StateDependencies(),
State: &state,
Index: -1,
},
&EvalIf{
If: func(ctx EvalContext) (bool, error) {
return !n.Resource.Lifecycle.CreateBeforeDestroy, nil
},
Then: &EvalClearPrimaryState{
Name: n.stateId(),
},
},
},
},
Else: &EvalWriteState{
Name: n.stateId(),
ResourceType: n.Resource.Type,
Provider: n.Resource.Provider,
Dependencies: n.StateDependencies(),
State: &state,
},
},
&EvalApplyPost{ &EvalApplyPost{
Info: info, Info: info,
State: &state, State: &state,

View File

@ -1,154 +0,0 @@
package terraform
import (
"fmt"
)
// TaintedTransformer is a GraphTransformer that adds tainted resources
// to the graph.
type TaintedTransformer struct {
// State is the global state. We'll automatically find the correct
// ModuleState based on the Graph.Path that is being transformed.
State *State
// View, if non-empty, is the ModuleState.View used around the state
// to find tainted resources.
View string
}
func (t *TaintedTransformer) Transform(g *Graph) error {
state := t.State.ModuleByPath(g.Path)
if state == nil {
// If there is no state for our module there can't be any tainted
// resources, since they live in the state.
return nil
}
// If we have a view, apply it now
if t.View != "" {
state = state.View(t.View)
}
// Go through all the resources in our state to look for tainted resources
for k, rs := range state.Resources {
// If we have no tainted resources, then move on
if len(rs.Tainted) == 0 {
continue
}
tainted := rs.Tainted
for i, _ := range tainted {
// Add the graph node and make the connection from any untainted
// resources with this name to the tainted resource, so that
// the tainted resource gets destroyed first.
g.Add(&graphNodeTaintedResource{
Index: i,
ResourceName: k,
ResourceType: rs.Type,
Provider: rs.Provider,
})
}
}
return nil
}
// graphNodeTaintedResource is the graph vertex representing a tainted resource.
type graphNodeTaintedResource struct {
Index int
ResourceName string
ResourceType string
Provider string
}
func (n *graphNodeTaintedResource) Name() string {
return fmt.Sprintf("%s (tainted #%d)", n.ResourceName, n.Index+1)
}
func (n *graphNodeTaintedResource) ProvidedBy() []string {
return []string{resourceProvider(n.ResourceName, n.Provider)}
}
// GraphNodeEvalable impl.
func (n *graphNodeTaintedResource) EvalTree() EvalNode {
var provider ResourceProvider
var state *InstanceState
seq := &EvalSequence{Nodes: make([]EvalNode, 0, 5)}
// Build instance info
info := &InstanceInfo{Id: n.ResourceName, Type: n.ResourceType}
seq.Nodes = append(seq.Nodes, &EvalInstanceInfo{Info: info})
// Refresh the resource
seq.Nodes = append(seq.Nodes, &EvalOpFilter{
Ops: []walkOperation{walkRefresh},
Node: &EvalSequence{
Nodes: []EvalNode{
&EvalGetProvider{
Name: n.ProvidedBy()[0],
Output: &provider,
},
&EvalReadStateTainted{
Name: n.ResourceName,
Index: n.Index,
Output: &state,
},
&EvalRefresh{
Info: info,
Provider: &provider,
State: &state,
Output: &state,
},
&EvalWriteStateTainted{
Name: n.ResourceName,
ResourceType: n.ResourceType,
Provider: n.Provider,
State: &state,
Index: n.Index,
},
},
},
})
// Apply
var diff *InstanceDiff
seq.Nodes = append(seq.Nodes, &EvalOpFilter{
Ops: []walkOperation{walkApply, walkDestroy},
Node: &EvalSequence{
Nodes: []EvalNode{
&EvalGetProvider{
Name: n.ProvidedBy()[0],
Output: &provider,
},
&EvalReadStateTainted{
Name: n.ResourceName,
Index: n.Index,
Output: &state,
},
&EvalDiffDestroy{
Info: info,
State: &state,
Output: &diff,
},
&EvalApply{
Info: info,
State: &state,
Diff: &diff,
Provider: &provider,
Output: &state,
},
&EvalWriteStateTainted{
Name: n.ResourceName,
ResourceType: n.ResourceType,
Provider: n.Provider,
State: &state,
Index: n.Index,
},
&EvalUpdateStateHook{},
},
},
})
return seq
}

View File

@ -1,71 +0,0 @@
package terraform
import (
"strings"
"testing"
"github.com/hashicorp/terraform/dag"
)
func TestTaintedTransformer(t *testing.T) {
mod := testModule(t, "transform-tainted-basic")
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: RootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.web": &ResourceState{
Type: "aws_instance",
Tainted: []*InstanceState{
&InstanceState{ID: "foo"},
},
},
},
},
},
}
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
transform := &TaintedTransformer{State: state}
if err := transform.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformTaintedBasicStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestGraphNodeTaintedResource_impl(t *testing.T) {
var _ dag.Vertex = new(graphNodeTaintedResource)
var _ dag.NamedVertex = new(graphNodeTaintedResource)
var _ GraphNodeProviderConsumer = new(graphNodeTaintedResource)
}
func TestGraphNodeTaintedResource_ProvidedBy(t *testing.T) {
n := &graphNodeTaintedResource{ResourceName: "aws_instance.foo"}
if v := n.ProvidedBy(); v[0] != "aws" {
t.Fatalf("bad: %#v", v)
}
}
func TestGraphNodeTaintedResource_ProvidedBy_alias(t *testing.T) {
n := &graphNodeTaintedResource{ResourceName: "aws_instance.foo", Provider: "aws.bar"}
if v := n.ProvidedBy(); v[0] != "aws.bar" {
t.Fatalf("bad: %#v", v)
}
}
const testTransformTaintedBasicStr = `
aws_instance.web
aws_instance.web (tainted #1)
`