Merge pull request #1078 from hashicorp/b-tainted-double-destroy
core: [refactor] pull Deposed out of Tainted list
This commit is contained in:
commit
b7462a8f6a
|
@ -3845,6 +3845,136 @@ func TestContext2Apply_errorDestroy_createBeforeDestroy(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestContext2Apply_multiDepose_createBeforeDestroy(t *testing.T) {
|
||||
m := testModule(t, "apply-multi-depose-create-before-destroy")
|
||||
p := testProvider("aws")
|
||||
p.DiffFn = testDiffFn
|
||||
ps := map[string]ResourceProviderFactory{"aws": testProviderFuncFixed(p)}
|
||||
state := &State{
|
||||
Modules: []*ModuleState{
|
||||
&ModuleState{
|
||||
Path: rootModulePath,
|
||||
Resources: map[string]*ResourceState{
|
||||
"aws_instance.web": &ResourceState{
|
||||
Type: "aws_instance",
|
||||
Primary: &InstanceState{ID: "foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Module: m,
|
||||
Providers: ps,
|
||||
State: state,
|
||||
})
|
||||
createdInstanceId := "bar"
|
||||
// Create works
|
||||
createFunc := func(is *InstanceState) (*InstanceState, error) {
|
||||
return &InstanceState{ID: createdInstanceId}, nil
|
||||
}
|
||||
// Destroy starts broken
|
||||
destroyFunc := func(is *InstanceState) (*InstanceState, error) {
|
||||
return is, fmt.Errorf("destroy failed")
|
||||
}
|
||||
p.ApplyFn = func(info *InstanceInfo, is *InstanceState, id *InstanceDiff) (*InstanceState, error) {
|
||||
if id.Destroy {
|
||||
return destroyFunc(is)
|
||||
} else {
|
||||
return createFunc(is)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := ctx.Plan(nil); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
// Destroy is broken, so even though CBD successfully replaces the instance,
|
||||
// we'll have to save the Deposed instance to destroy later
|
||||
state, err := ctx.Apply()
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
checkStateString(t, state, `
|
||||
aws_instance.web: (1 deposed)
|
||||
ID = bar
|
||||
Deposed ID 1 = foo
|
||||
`)
|
||||
|
||||
createdInstanceId = "baz"
|
||||
ctx = testContext2(t, &ContextOpts{
|
||||
Module: m,
|
||||
Providers: ps,
|
||||
State: state,
|
||||
})
|
||||
|
||||
if _, err := ctx.Plan(nil); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
// We're replacing the primary instance once again. Destroy is _still_
|
||||
// broken, so the Deposed list gets longer
|
||||
state, err = ctx.Apply()
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
checkStateString(t, state, `
|
||||
aws_instance.web: (2 deposed)
|
||||
ID = baz
|
||||
Deposed ID 1 = foo
|
||||
Deposed ID 2 = bar
|
||||
`)
|
||||
|
||||
// Destroy partially fixed!
|
||||
destroyFunc = func(is *InstanceState) (*InstanceState, error) {
|
||||
if is.ID == "foo" || is.ID == "baz" {
|
||||
return nil, nil
|
||||
} else {
|
||||
return is, fmt.Errorf("destroy partially failed")
|
||||
}
|
||||
}
|
||||
|
||||
createdInstanceId = "qux"
|
||||
if _, err := ctx.Plan(nil); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
state, err = ctx.Apply()
|
||||
// Expect error because 1/2 of Deposed destroys failed
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// foo and baz are now gone, bar sticks around
|
||||
checkStateString(t, state, `
|
||||
aws_instance.web: (1 deposed)
|
||||
ID = qux
|
||||
Deposed ID 1 = bar
|
||||
`)
|
||||
|
||||
// Destroy working fully!
|
||||
destroyFunc = func(is *InstanceState) (*InstanceState, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
createdInstanceId = "quux"
|
||||
if _, err := ctx.Plan(nil); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
state, err = ctx.Apply()
|
||||
if err != nil {
|
||||
t.Fatal("should not have error:", err)
|
||||
}
|
||||
|
||||
// And finally the state is clean
|
||||
checkStateString(t, state, `
|
||||
aws_instance.web:
|
||||
ID = quux
|
||||
`)
|
||||
}
|
||||
|
||||
func TestContext2Apply_provisionerResourceRef(t *testing.T) {
|
||||
m := testModule(t, "apply-provisioner-resource-ref")
|
||||
p := testProvider("aws")
|
||||
|
@ -5343,6 +5473,15 @@ func testProvisioner() *MockResourceProvisioner {
|
|||
return p
|
||||
}
|
||||
|
||||
func checkStateString(t *testing.T, state *State, expected string) {
|
||||
actual := strings.TrimSpace(state.String())
|
||||
expected = strings.TrimSpace(expected)
|
||||
|
||||
if actual != expected {
|
||||
t.Fatalf("state does not match! actual:\n%s\n\nexpected:\n%s", actual, expected)
|
||||
}
|
||||
}
|
||||
|
||||
const testContextGraph = `
|
||||
root: root
|
||||
aws_instance.bar
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package terraform
|
||||
|
||||
// EvalReturnError is an EvalNode implementation that returns an
|
||||
// error if it is present.
|
||||
//
|
||||
// This is useful for scenarios where an error has been captured by
|
||||
// another EvalNode (like EvalApply) for special EvalTree-based error
|
||||
// handling, and that handling has completed, so the error should be
|
||||
// returned normally.
|
||||
type EvalReturnError struct {
|
||||
Error *error
|
||||
}
|
||||
|
||||
func (n *EvalReturnError) Eval(ctx EvalContext) (interface{}, error) {
|
||||
if n.Error == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, *n.Error
|
||||
}
|
|
@ -3,7 +3,8 @@ package terraform
|
|||
// EvalIf is an EvalNode that is a conditional.
|
||||
type EvalIf struct {
|
||||
If func(EvalContext) (bool, error)
|
||||
Node EvalNode
|
||||
Then EvalNode
|
||||
Else EvalNode
|
||||
}
|
||||
|
||||
// TODO: test
|
||||
|
@ -14,7 +15,11 @@ func (n *EvalIf) Eval(ctx EvalContext) (interface{}, error) {
|
|||
}
|
||||
|
||||
if yes {
|
||||
return EvalRaw(n.Node, ctx)
|
||||
return EvalRaw(n.Then, ctx)
|
||||
} else {
|
||||
if n.Else != nil {
|
||||
return EvalRaw(n.Else, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
|
|
|
@ -5,16 +5,77 @@ import (
|
|||
)
|
||||
|
||||
// EvalReadState is an EvalNode implementation that reads the
|
||||
// InstanceState for a specific resource out of the state.
|
||||
// primary InstanceState for a specific resource out of the state.
|
||||
type EvalReadState struct {
|
||||
Name string
|
||||
Tainted bool
|
||||
TaintedIndex int
|
||||
Output **InstanceState
|
||||
Name string
|
||||
Output **InstanceState
|
||||
}
|
||||
|
||||
// TODO: test
|
||||
func (n *EvalReadState) Eval(ctx EvalContext) (interface{}, error) {
|
||||
return readInstanceFromState(ctx, n.Name, n.Output, func(rs *ResourceState) (*InstanceState, error) {
|
||||
return rs.Primary, nil
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
// deposed InstanceState for a specific resource out of the state
|
||||
type EvalReadStateDeposed struct {
|
||||
Name string
|
||||
Output **InstanceState
|
||||
// Index indicates which instance in the Deposed list to target, or -1 for
|
||||
// the last item.
|
||||
Index int
|
||||
}
|
||||
|
||||
func (n *EvalReadStateDeposed) 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.Deposed) - 1
|
||||
}
|
||||
if idx >= 0 && idx < len(rs.Deposed) {
|
||||
return rs.Deposed[idx], nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("bad deposed index: %d, for resource: %#v", idx, rs)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Does the bulk of the work for the various flavors of ReadState eval nodes.
|
||||
// Each node just provides a reader function to get from the ResourceState to the
|
||||
// InstanceState, and this takes care of all the plumbing.
|
||||
func readInstanceFromState(
|
||||
ctx EvalContext,
|
||||
resourceName string,
|
||||
output **InstanceState,
|
||||
readerFn func(*ResourceState) (*InstanceState, error),
|
||||
) (*InstanceState, error) {
|
||||
state, lock := ctx.State()
|
||||
|
||||
// Get a read lock so we can access this instance
|
||||
|
@ -28,33 +89,23 @@ func (n *EvalReadState) Eval(ctx EvalContext) (interface{}, error) {
|
|||
}
|
||||
|
||||
// Look for the resource state. If we don't have one, then it is okay.
|
||||
rs := mod.Resources[n.Name]
|
||||
rs := mod.Resources[resourceName]
|
||||
if rs == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var result *InstanceState
|
||||
if !n.Tainted {
|
||||
// Return the primary
|
||||
result = rs.Primary
|
||||
} else {
|
||||
// Get the index. If it is negative, then we get the last one
|
||||
idx := n.TaintedIndex
|
||||
if idx < 0 {
|
||||
idx = len(rs.Tainted) - 1
|
||||
}
|
||||
if idx >= 0 && idx < len(rs.Tainted) {
|
||||
// Return the proper tainted resource
|
||||
result = rs.Tainted[idx]
|
||||
}
|
||||
// Use the delegate function to get the instance state from the resource state
|
||||
is, err := readerFn(rs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Write the result to the output pointer
|
||||
if n.Output != nil {
|
||||
*n.Output = result
|
||||
if output != nil {
|
||||
*output = is
|
||||
}
|
||||
|
||||
return result, nil
|
||||
return is, nil
|
||||
}
|
||||
|
||||
// EvalRequireState is an EvalNode implementation that early exits
|
||||
|
@ -98,20 +149,85 @@ func (n *EvalUpdateStateHook) Eval(ctx EvalContext) (interface{}, error) {
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
// EvalWriteState is an EvalNode implementation that reads the
|
||||
// InstanceState for a specific resource out of the state.
|
||||
// EvalWriteState is an EvalNode implementation that writes the
|
||||
// primary InstanceState for a specific resource into the state.
|
||||
type EvalWriteState struct {
|
||||
Name string
|
||||
ResourceType string
|
||||
Dependencies []string
|
||||
State **InstanceState
|
||||
Tainted *bool
|
||||
TaintedIndex int
|
||||
TaintedClearPrimary bool
|
||||
Name string
|
||||
ResourceType string
|
||||
Dependencies []string
|
||||
State **InstanceState
|
||||
}
|
||||
|
||||
// TODO: test
|
||||
func (n *EvalWriteState) Eval(ctx EvalContext) (interface{}, error) {
|
||||
return writeInstanceToState(ctx, n.Name, n.ResourceType, n.Dependencies,
|
||||
func(rs *ResourceState) error {
|
||||
rs.Primary = *n.State
|
||||
return nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// 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
|
||||
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.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
|
||||
// an InstanceState out to the Deposed list of a resource in the state.
|
||||
type EvalWriteStateDeposed struct {
|
||||
Name string
|
||||
ResourceType string
|
||||
Dependencies []string
|
||||
State **InstanceState
|
||||
// Index indicates which instance in the Deposed list to target, or -1 to append.
|
||||
Index int
|
||||
}
|
||||
|
||||
func (n *EvalWriteStateDeposed) Eval(ctx EvalContext) (interface{}, error) {
|
||||
return writeInstanceToState(ctx, n.Name, n.ResourceType, n.Dependencies,
|
||||
func(rs *ResourceState) error {
|
||||
if n.Index == -1 {
|
||||
rs.Deposed = append(rs.Deposed, *n.State)
|
||||
} else {
|
||||
rs.Deposed[n.Index] = *n.State
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Pulls together the common tasks of the EvalWriteState nodes. All the args
|
||||
// are passed directly down from the EvalNode along with a `writer` function
|
||||
// which is yielded the *ResourceState and is responsible for writing an
|
||||
// InstanceState to the proper field in the ResourceState.
|
||||
func writeInstanceToState(
|
||||
ctx EvalContext,
|
||||
resourceName string,
|
||||
resourceType string,
|
||||
dependencies []string,
|
||||
writerFn func(*ResourceState) error,
|
||||
) (*InstanceState, error) {
|
||||
state, lock := ctx.State()
|
||||
if state == nil {
|
||||
return nil, fmt.Errorf("cannot write state to nil state")
|
||||
|
@ -128,35 +244,55 @@ func (n *EvalWriteState) Eval(ctx EvalContext) (interface{}, error) {
|
|||
}
|
||||
|
||||
// Look for the resource state.
|
||||
rs := mod.Resources[n.Name]
|
||||
rs := mod.Resources[resourceName]
|
||||
if rs == nil {
|
||||
rs = &ResourceState{}
|
||||
rs.init()
|
||||
mod.Resources[n.Name] = rs
|
||||
mod.Resources[resourceName] = rs
|
||||
}
|
||||
rs.Type = n.ResourceType
|
||||
rs.Dependencies = n.Dependencies
|
||||
rs.Type = resourceType
|
||||
rs.Dependencies = dependencies
|
||||
|
||||
if n.Tainted != nil && *n.Tainted {
|
||||
if n.TaintedIndex != -1 {
|
||||
rs.Tainted[n.TaintedIndex] = *n.State
|
||||
} else {
|
||||
rs.Tainted = append(rs.Tainted, *n.State)
|
||||
}
|
||||
|
||||
if n.TaintedClearPrimary {
|
||||
rs.Primary = nil
|
||||
}
|
||||
} else {
|
||||
// Set the primary state
|
||||
rs.Primary = *n.State
|
||||
if err := writerFn(rs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// EvalClearPrimaryState is an EvalNode implementation that clears the primary
|
||||
// instance from a resource state.
|
||||
type EvalClearPrimaryState struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (n *EvalClearPrimaryState) 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
|
||||
}
|
||||
|
||||
// Clear primary from the resource state
|
||||
rs.Primary = nil
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// EvalDeposeState is an EvalNode implementation that takes the primary
|
||||
// out of a state and makes it tainted. This is done at the beggining of
|
||||
// out of a state and makes it Deposed. This is done at the beginning of
|
||||
// create-before-destroy calls so that the create can create while preserving
|
||||
// the old state of the to-be-destroyed resource.
|
||||
type EvalDeposeState struct {
|
||||
|
@ -188,8 +324,8 @@ func (n *EvalDeposeState) Eval(ctx EvalContext) (interface{}, error) {
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
// Depose to the tainted
|
||||
rs.Tainted = append(rs.Tainted, rs.Primary)
|
||||
// Depose
|
||||
rs.Deposed = append(rs.Deposed, rs.Primary)
|
||||
rs.Primary = nil
|
||||
|
||||
return nil, nil
|
||||
|
@ -221,15 +357,15 @@ func (n *EvalUndeposeState) Eval(ctx EvalContext) (interface{}, error) {
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
// If we don't have any tainted, then we don't have anything to do
|
||||
if len(rs.Tainted) == 0 {
|
||||
// If we don't have any desposed resource, then we don't have anything to do
|
||||
if len(rs.Deposed) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Undepose to the tainted
|
||||
idx := len(rs.Tainted) - 1
|
||||
rs.Primary = rs.Tainted[idx]
|
||||
rs.Tainted[idx] = nil
|
||||
// Undepose
|
||||
idx := len(rs.Deposed) - 1
|
||||
rs.Primary = rs.Deposed[idx]
|
||||
rs.Deposed[idx] = nil
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
@ -66,3 +66,163 @@ func TestEvalUpdateStateHook(t *testing.T) {
|
|||
t.Fatalf("bad: %#v", mockHook.PostStateUpdateState)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvalReadState(t *testing.T) {
|
||||
var output *InstanceState
|
||||
cases := map[string]struct {
|
||||
Resources map[string]*ResourceState
|
||||
Node EvalNode
|
||||
ExpectedInstanceId string
|
||||
}{
|
||||
"ReadState gets primary instance state": {
|
||||
Resources: map[string]*ResourceState{
|
||||
"aws_instance.bar": &ResourceState{
|
||||
Primary: &InstanceState{
|
||||
ID: "i-abc123",
|
||||
},
|
||||
},
|
||||
},
|
||||
Node: &EvalReadState{
|
||||
Name: "aws_instance.bar",
|
||||
Output: &output,
|
||||
},
|
||||
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": {
|
||||
Resources: map[string]*ResourceState{
|
||||
"aws_instance.bar": &ResourceState{
|
||||
Deposed: []*InstanceState{
|
||||
&InstanceState{ID: "i-abc123"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Node: &EvalReadStateDeposed{
|
||||
Name: "aws_instance.bar",
|
||||
Output: &output,
|
||||
Index: 0,
|
||||
},
|
||||
ExpectedInstanceId: "i-abc123",
|
||||
},
|
||||
}
|
||||
|
||||
for k, c := range cases {
|
||||
ctx := new(MockEvalContext)
|
||||
ctx.StateState = &State{
|
||||
Modules: []*ModuleState{
|
||||
&ModuleState{
|
||||
Path: rootModulePath,
|
||||
Resources: c.Resources,
|
||||
},
|
||||
},
|
||||
}
|
||||
ctx.StateLock = new(sync.RWMutex)
|
||||
ctx.PathPath = rootModulePath
|
||||
|
||||
result, err := c.Node.Eval(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("[%s] Got err: %#v", k, err)
|
||||
}
|
||||
|
||||
expected := c.ExpectedInstanceId
|
||||
if !(result != nil && result.(*InstanceState).ID == expected) {
|
||||
t.Fatalf("[%s] Expected return with ID %#v, got: %#v", k, expected, result)
|
||||
}
|
||||
|
||||
if !(output != nil && output.ID == expected) {
|
||||
t.Fatalf("[%s] Expected output with ID %#v, got: %#v", k, expected, output)
|
||||
}
|
||||
|
||||
output = nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvalWriteState(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 := &EvalWriteState{
|
||||
Name: "restype.resname",
|
||||
ResourceType: "restype",
|
||||
State: &is,
|
||||
}
|
||||
_, err := node.Eval(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Got err: %#v", err)
|
||||
}
|
||||
|
||||
checkStateString(t, state, `
|
||||
restype.resname:
|
||||
ID = i-abc123
|
||||
`)
|
||||
}
|
||||
|
||||
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) {
|
||||
state := &State{}
|
||||
ctx := new(MockEvalContext)
|
||||
ctx.StateState = state
|
||||
ctx.StateLock = new(sync.RWMutex)
|
||||
ctx.PathPath = rootModulePath
|
||||
|
||||
is := &InstanceState{ID: "i-abc123"}
|
||||
node := &EvalWriteStateDeposed{
|
||||
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 deposed)
|
||||
ID = <not created>
|
||||
Deposed ID 1 = i-abc123
|
||||
`)
|
||||
}
|
||||
|
|
|
@ -293,24 +293,16 @@ func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error)
|
|||
View: n.Resource.Id(),
|
||||
})
|
||||
|
||||
if n.Resource.Lifecycle.CreateBeforeDestroy {
|
||||
// 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(),
|
||||
Deposed: n.Resource.Lifecycle.CreateBeforeDestroy,
|
||||
DeposedInclude: true,
|
||||
})
|
||||
}
|
||||
steps = append(steps, &DeposedTransformer{
|
||||
State: state,
|
||||
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(),
|
||||
Deposed: n.Resource.Lifecycle.CreateBeforeDestroy,
|
||||
DeposedInclude: false,
|
||||
State: state,
|
||||
View: n.Resource.Id(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -498,7 +498,7 @@ func (m *ModuleState) prune() {
|
|||
for k, v := range m.Resources {
|
||||
v.prune()
|
||||
|
||||
if (v.Primary == nil || v.Primary.ID == "") && len(v.Tainted) == 0 {
|
||||
if (v.Primary == nil || v.Primary.ID == "") && len(v.Tainted) == 0 && len(v.Deposed) == 0 {
|
||||
delete(m.Resources, k)
|
||||
}
|
||||
}
|
||||
|
@ -548,7 +548,12 @@ func (m *ModuleState) String() string {
|
|||
taintStr = fmt.Sprintf(" (%d tainted)", len(rs.Tainted))
|
||||
}
|
||||
|
||||
buf.WriteString(fmt.Sprintf("%s:%s\n", k, taintStr))
|
||||
deposedStr := ""
|
||||
if len(rs.Deposed) > 0 {
|
||||
deposedStr = fmt.Sprintf(" (%d deposed)", len(rs.Deposed))
|
||||
}
|
||||
|
||||
buf.WriteString(fmt.Sprintf("%s:%s%s\n", k, taintStr, deposedStr))
|
||||
buf.WriteString(fmt.Sprintf(" ID = %s\n", id))
|
||||
|
||||
var attributes map[string]string
|
||||
|
@ -574,6 +579,10 @@ func (m *ModuleState) String() string {
|
|||
buf.WriteString(fmt.Sprintf(" Tainted ID %d = %s\n", idx+1, t.ID))
|
||||
}
|
||||
|
||||
for idx, t := range rs.Deposed {
|
||||
buf.WriteString(fmt.Sprintf(" Deposed ID %d = %s\n", idx+1, t.ID))
|
||||
}
|
||||
|
||||
if len(rs.Dependencies) > 0 {
|
||||
buf.WriteString(fmt.Sprintf("\n Dependencies:\n"))
|
||||
for _, dep := range rs.Dependencies {
|
||||
|
@ -644,6 +653,16 @@ type ResourceState struct {
|
|||
// 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
|
||||
// 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
|
||||
// Deposed instance is cleaned up. If there were problems creating the
|
||||
// replacement, the instance remains in the Deposed list so it can be
|
||||
// destroyed in a future run. Functionally, Deposed instances are very
|
||||
// similar to Tainted instances in that Terraform is only tracking them in
|
||||
// order to remember to destroy them.
|
||||
Deposed []*InstanceState `json:"deposed,omitempty"`
|
||||
}
|
||||
|
||||
// Equal tests whether two ResourceStates are equal.
|
||||
|
@ -744,6 +763,12 @@ func (r *ResourceState) deepcopy() *ResourceState {
|
|||
n.Tainted = append(n.Tainted, inst.deepcopy())
|
||||
}
|
||||
}
|
||||
if r.Deposed != nil {
|
||||
n.Deposed = make([]*InstanceState, 0, len(r.Deposed))
|
||||
for _, inst := range r.Deposed {
|
||||
n.Deposed = append(n.Deposed, inst.deepcopy())
|
||||
}
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
@ -762,6 +787,19 @@ func (r *ResourceState) prune() {
|
|||
}
|
||||
|
||||
r.Tainted = r.Tainted[:n]
|
||||
|
||||
n = len(r.Deposed)
|
||||
for i := 0; i < n; i++ {
|
||||
inst := r.Deposed[i]
|
||||
if inst == nil || inst.ID == "" {
|
||||
copy(r.Deposed[i:], r.Deposed[i+1:])
|
||||
r.Deposed[n-1] = nil
|
||||
n--
|
||||
i--
|
||||
}
|
||||
}
|
||||
|
||||
r.Deposed = r.Deposed[:n]
|
||||
}
|
||||
|
||||
func (r *ResourceState) sort() {
|
||||
|
|
|
@ -443,9 +443,9 @@ aws_instance.bar:
|
|||
`
|
||||
|
||||
const testTerraformApplyErrorDestroyCreateBeforeDestroyStr = `
|
||||
aws_instance.bar: (1 tainted)
|
||||
aws_instance.bar: (1 deposed)
|
||||
ID = foo
|
||||
Tainted ID 1 = bar
|
||||
Deposed ID 1 = bar
|
||||
`
|
||||
|
||||
const testTerraformApplyErrorPartialStr = `
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
resource "aws_instance" "web" {
|
||||
// require_new is a special attribute recognized by testDiffFn that forces
|
||||
// a new resource on every apply
|
||||
require_new = "yes"
|
||||
lifecycle {
|
||||
create_before_destroy = true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
package terraform
|
||||
|
||||
import "fmt"
|
||||
|
||||
// DeposedTransformer is a GraphTransformer that adds deposed resources
|
||||
// to the graph.
|
||||
type DeposedTransformer 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 deposed resources.
|
||||
View string
|
||||
}
|
||||
|
||||
func (t *DeposedTransformer) 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 deposed
|
||||
// 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 deposed resources
|
||||
for k, rs := range state.Resources {
|
||||
// If we have no deposed resources, then move on
|
||||
if len(rs.Deposed) == 0 {
|
||||
continue
|
||||
}
|
||||
deposed := rs.Deposed
|
||||
|
||||
for i, _ := range deposed {
|
||||
g.Add(&graphNodeDeposedResource{
|
||||
Index: i,
|
||||
ResourceName: k,
|
||||
ResourceType: rs.Type,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// graphNodeDeposedResource is the graph vertex representing a deposed resource.
|
||||
type graphNodeDeposedResource struct {
|
||||
Index int
|
||||
ResourceName string
|
||||
ResourceType string
|
||||
}
|
||||
|
||||
func (n *graphNodeDeposedResource) Name() string {
|
||||
return fmt.Sprintf("%s (deposed #%d)", n.ResourceName, n.Index)
|
||||
}
|
||||
|
||||
func (n *graphNodeDeposedResource) ProvidedBy() []string {
|
||||
return []string{resourceProvider(n.ResourceName)}
|
||||
}
|
||||
|
||||
// GraphNodeEvalable impl.
|
||||
func (n *graphNodeDeposedResource) 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,
|
||||
},
|
||||
&EvalReadStateDeposed{
|
||||
Name: n.ResourceName,
|
||||
Output: &state,
|
||||
Index: n.Index,
|
||||
},
|
||||
&EvalRefresh{
|
||||
Info: info,
|
||||
Provider: &provider,
|
||||
State: &state,
|
||||
Output: &state,
|
||||
},
|
||||
&EvalWriteStateDeposed{
|
||||
Name: n.ResourceName,
|
||||
ResourceType: n.ResourceType,
|
||||
State: &state,
|
||||
Index: n.Index,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Apply
|
||||
var diff *InstanceDiff
|
||||
var err error
|
||||
seq.Nodes = append(seq.Nodes, &EvalOpFilter{
|
||||
Ops: []walkOperation{walkApply},
|
||||
Node: &EvalSequence{
|
||||
Nodes: []EvalNode{
|
||||
&EvalGetProvider{
|
||||
Name: n.ProvidedBy()[0],
|
||||
Output: &provider,
|
||||
},
|
||||
&EvalReadStateDeposed{
|
||||
Name: n.ResourceName,
|
||||
Output: &state,
|
||||
Index: n.Index,
|
||||
},
|
||||
&EvalDiffDestroy{
|
||||
Info: info,
|
||||
State: &state,
|
||||
Output: &diff,
|
||||
},
|
||||
&EvalApply{
|
||||
Info: info,
|
||||
State: &state,
|
||||
Diff: &diff,
|
||||
Provider: &provider,
|
||||
Output: &state,
|
||||
Error: &err,
|
||||
},
|
||||
// Always write the resource back to the state deposed... if it
|
||||
// was successfully destroyed it will be pruned. If it was not, it will
|
||||
// be caught on the next run.
|
||||
&EvalWriteStateDeposed{
|
||||
Name: n.ResourceName,
|
||||
ResourceType: n.ResourceType,
|
||||
State: &state,
|
||||
Index: n.Index,
|
||||
},
|
||||
&EvalReturnError{
|
||||
Error: &err,
|
||||
},
|
||||
&EvalUpdateStateHook{},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return seq
|
||||
}
|
|
@ -285,7 +285,7 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode {
|
|||
diffApply.Destroy = false
|
||||
return true, nil
|
||||
},
|
||||
Node: EvalNoop{},
|
||||
Then: EvalNoop{},
|
||||
},
|
||||
|
||||
&EvalIf{
|
||||
|
@ -301,7 +301,7 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode {
|
|||
|
||||
return createBeforeDestroyEnabled, nil
|
||||
},
|
||||
Node: &EvalDeposeState{
|
||||
Then: &EvalDeposeState{
|
||||
Name: n.stateId(),
|
||||
},
|
||||
},
|
||||
|
@ -382,7 +382,7 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode {
|
|||
failure := tainted || err != nil
|
||||
return createBeforeDestroyEnabled && failure, nil
|
||||
},
|
||||
Node: &EvalUndeposeState{
|
||||
Then: &EvalUndeposeState{
|
||||
Name: n.stateId(),
|
||||
},
|
||||
},
|
||||
|
@ -395,14 +395,35 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode {
|
|||
Diff: nil,
|
||||
},
|
||||
|
||||
&EvalWriteState{
|
||||
Name: n.stateId(),
|
||||
ResourceType: n.Resource.Type,
|
||||
Dependencies: n.DependentOn(),
|
||||
State: &state,
|
||||
Tainted: &tainted,
|
||||
TaintedIndex: -1,
|
||||
TaintedClearPrimary: !n.Resource.Lifecycle.CreateBeforeDestroy,
|
||||
&EvalIf{
|
||||
If: func(ctx EvalContext) (bool, error) {
|
||||
return tainted, nil
|
||||
},
|
||||
Then: &EvalSequence{
|
||||
Nodes: []EvalNode{
|
||||
&EvalWriteStateTainted{
|
||||
Name: n.stateId(),
|
||||
ResourceType: n.Resource.Type,
|
||||
Dependencies: n.DependentOn(),
|
||||
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,
|
||||
Dependencies: n.DependentOn(),
|
||||
State: &state,
|
||||
},
|
||||
},
|
||||
&EvalApplyPost{
|
||||
Info: info,
|
||||
|
@ -480,18 +501,26 @@ func (n *graphNodeExpandedResourceDestroy) EvalTree() EvalNode {
|
|||
|
||||
return true, EvalEarlyExitError{}
|
||||
},
|
||||
Node: EvalNoop{},
|
||||
Then: EvalNoop{},
|
||||
},
|
||||
|
||||
&EvalGetProvider{
|
||||
Name: n.ProvidedBy()[0],
|
||||
Output: &provider,
|
||||
},
|
||||
&EvalReadState{
|
||||
Name: n.stateId(),
|
||||
Output: &state,
|
||||
Tainted: n.Resource.Lifecycle.CreateBeforeDestroy,
|
||||
TaintedIndex: -1,
|
||||
&EvalIf{
|
||||
If: func(ctx EvalContext) (bool, error) {
|
||||
return n.Resource.Lifecycle.CreateBeforeDestroy, nil
|
||||
},
|
||||
Then: &EvalReadStateTainted{
|
||||
Name: n.stateId(),
|
||||
Output: &state,
|
||||
Index: -1,
|
||||
},
|
||||
Else: &EvalReadState{
|
||||
Name: n.stateId(),
|
||||
Output: &state,
|
||||
},
|
||||
},
|
||||
&EvalRequireState{
|
||||
State: &state,
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"fmt"
|
||||
)
|
||||
|
||||
// TraintedTransformer is a GraphTransformer that adds tainted resources
|
||||
// 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
|
||||
|
@ -14,12 +14,6 @@ type TaintedTransformer struct {
|
|||
// View, if non-empty, is the ModuleState.View used around the state
|
||||
// to find tainted resources.
|
||||
View string
|
||||
|
||||
// Deposed, if set to true, assumes that the last tainted index
|
||||
// represents a "deposed" resource, or a resource that was previously
|
||||
// a primary but is now tainted since it is demoted.
|
||||
Deposed bool
|
||||
DeposedInclude bool
|
||||
}
|
||||
|
||||
func (t *TaintedTransformer) Transform(g *Graph) error {
|
||||
|
@ -43,17 +37,6 @@ func (t *TaintedTransformer) Transform(g *Graph) error {
|
|||
}
|
||||
tainted := rs.Tainted
|
||||
|
||||
// If we expect a deposed resource, then shuffle a bit
|
||||
if t.Deposed {
|
||||
if t.DeposedInclude {
|
||||
// Only include the deposed resource
|
||||
tainted = rs.Tainted[len(rs.Tainted)-1:]
|
||||
} else {
|
||||
// Exclude the deposed resource
|
||||
tainted = rs.Tainted[:len(rs.Tainted)-1]
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -88,7 +71,6 @@ func (n *graphNodeTaintedResource) ProvidedBy() []string {
|
|||
func (n *graphNodeTaintedResource) EvalTree() EvalNode {
|
||||
var provider ResourceProvider
|
||||
var state *InstanceState
|
||||
tainted := true
|
||||
|
||||
seq := &EvalSequence{Nodes: make([]EvalNode, 0, 5)}
|
||||
|
||||
|
@ -105,11 +87,10 @@ func (n *graphNodeTaintedResource) EvalTree() EvalNode {
|
|||
Name: n.ProvidedBy()[0],
|
||||
Output: &provider,
|
||||
},
|
||||
&EvalReadState{
|
||||
Name: n.ResourceName,
|
||||
Tainted: true,
|
||||
TaintedIndex: n.Index,
|
||||
Output: &state,
|
||||
&EvalReadStateTainted{
|
||||
Name: n.ResourceName,
|
||||
Index: n.Index,
|
||||
Output: &state,
|
||||
},
|
||||
&EvalRefresh{
|
||||
Info: info,
|
||||
|
@ -117,12 +98,11 @@ func (n *graphNodeTaintedResource) EvalTree() EvalNode {
|
|||
State: &state,
|
||||
Output: &state,
|
||||
},
|
||||
&EvalWriteState{
|
||||
&EvalWriteStateTainted{
|
||||
Name: n.ResourceName,
|
||||
ResourceType: n.ResourceType,
|
||||
State: &state,
|
||||
Tainted: &tainted,
|
||||
TaintedIndex: n.Index,
|
||||
Index: n.Index,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -138,11 +118,10 @@ func (n *graphNodeTaintedResource) EvalTree() EvalNode {
|
|||
Name: n.ProvidedBy()[0],
|
||||
Output: &provider,
|
||||
},
|
||||
&EvalReadState{
|
||||
Name: n.ResourceName,
|
||||
Tainted: true,
|
||||
TaintedIndex: n.Index,
|
||||
Output: &state,
|
||||
&EvalReadStateTainted{
|
||||
Name: n.ResourceName,
|
||||
Index: n.Index,
|
||||
Output: &state,
|
||||
},
|
||||
&EvalDiffDestroy{
|
||||
Info: info,
|
||||
|
@ -156,12 +135,11 @@ func (n *graphNodeTaintedResource) EvalTree() EvalNode {
|
|||
Provider: &provider,
|
||||
Output: &state,
|
||||
},
|
||||
&EvalWriteState{
|
||||
&EvalWriteStateTainted{
|
||||
Name: n.ResourceName,
|
||||
ResourceType: n.ResourceType,
|
||||
State: &state,
|
||||
Tainted: &tainted,
|
||||
TaintedIndex: n.Index,
|
||||
Index: n.Index,
|
||||
},
|
||||
&EvalUpdateStateHook{},
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue