Change taint behaviour to act as a normal resource

This means it’s shown correctly in a plan and takes into account any
actions that are dependant on the tainted resource and, vice verse, any
actions that the tainted resource depends on.

So this changes the behaviour from saying this resource is tainted so
just forget about it and make sure it gets deleted in the background,
to saying I want that resource to be recreated (taking into account the
existing resource and it’s place in the graph).
This commit is contained in:
Sander van Harmelen 2016-04-21 21:55:29 +02:00 committed by Paul Hinze
parent 3355c1528d
commit 8560f50cbc
No known key found for this signature in database
GPG Key ID: B69DEDF2D55501C0
15 changed files with 106 additions and 611 deletions

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

@ -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

@ -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

@ -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

@ -104,7 +104,7 @@ 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. // 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
} }
@ -216,41 +216,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 {

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())
@ -120,23 +120,12 @@ func (n *GraphNodeConfigResource) VarWalk(fn func(config.InterpolatedVariable))
} }
func (n *GraphNodeConfigResource) Name() string { func (n *GraphNodeConfigResource) Name() string {
result := n.Resource.Id() return n.Resource.Id() + " (destroy)"
switch n.DestroyMode {
case DestroyNone:
case DestroyPrimary:
result += " (destroy)"
case DestroyTainted:
result += " (destroy tainted)"
default:
result += " (unknown destroy type)"
}
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 +151,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 +172,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 +235,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 +245,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 +254,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 +343,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 +401,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 +410,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 +460,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 +479,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

@ -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,10 +860,6 @@ 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)) buf.WriteString(fmt.Sprintf(" Deposed ID %d = %s\n", idx+1, t.ID))
} }
@ -1031,14 +1027,6 @@ 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
@ -1084,80 +1072,21 @@ func (s *ResourceState) Equal(other *ResourceState) bool {
} }
// Tainted // Tainted
taints := make(map[string]*InstanceState) if s.Primary.Tainted != other.Primary.Tainted {
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 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 r.Primary.Tainted = true
if r.Primary == nil {
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 r.Primary.Tainted = false
// 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 {
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.")
}
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 +1105,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 +1123,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 +1172,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 +1194,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 +1256,10 @@ func (s *InstanceState) Equal(other *InstanceState) bool {
} }
} }
if s.Tainted != other.Tainted {
return false
}
return true return true
} }
@ -1413,6 +1330,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

@ -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

@ -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

@ -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,
@ -540,37 +536,12 @@ func (n *graphNodeExpandedResource) managedResourceEvalNodes(resource *Resource,
Diff: nil, Diff: nil,
}, },
&EvalIf{ &EvalWriteState{
If: func(ctx EvalContext) (bool, error) { Name: n.stateId(),
return tainted, nil ResourceType: n.Resource.Type,
}, Provider: n.Resource.Provider,
Then: &EvalSequence{ Dependencies: n.StateDependencies(),
Nodes: []EvalNode{ State: &state,
&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,

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)
`