diff --git a/terraform/eval_diff.go b/terraform/eval_diff.go index 89a583918..4a06f4c43 100644 --- a/terraform/eval_diff.go +++ b/terraform/eval_diff.go @@ -238,6 +238,38 @@ func (n *EvalDiffTainted) Eval(ctx EvalContext) (interface{}, error) { return nil, nil } +// EvalFilterDiff is an EvalNode implementation that filters the diff +// according to some filter. +type EvalFilterDiff struct { + // Input and output + Diff **InstanceDiff + Output **InstanceDiff + + // Destroy, if true, will only include a destroy diff if it is set. + Destroy bool +} + +func (n *EvalFilterDiff) Eval(ctx EvalContext) (interface{}, error) { + if *n.Diff == nil { + return nil, nil + } + + input := *n.Diff + result := new(InstanceDiff) + + if n.Destroy { + if input.Destroy || input.RequiresNew() { + result.Destroy = true + } + } + + if n.Output != nil { + *n.Output = result + } + + return nil, nil +} + // EvalReadDiff is an EvalNode implementation that writes the diff to // the full diff. type EvalReadDiff struct { @@ -275,7 +307,10 @@ func (n *EvalWriteDiff) Eval(ctx EvalContext) (interface{}, error) { diff, lock := ctx.Diff() // The diff to write, if its empty it should write nil - diffVal := *n.Diff + var diffVal *InstanceDiff + if n.Diff != nil { + diffVal = *n.Diff + } if diffVal.Empty() { diffVal = nil } diff --git a/terraform/eval_diff_test.go b/terraform/eval_diff_test.go new file mode 100644 index 000000000..1291e6993 --- /dev/null +++ b/terraform/eval_diff_test.go @@ -0,0 +1,78 @@ +package terraform + +import ( + "reflect" + "testing" +) + +func TestEvalFilterDiff(t *testing.T) { + ctx := new(MockEvalContext) + + cases := []struct { + Node *EvalFilterDiff + Input *InstanceDiff + Output *InstanceDiff + }{ + // With no settings, it returns an empty diff + { + &EvalFilterDiff{}, + &InstanceDiff{Destroy: true}, + &InstanceDiff{}, + }, + + // Destroy + { + &EvalFilterDiff{Destroy: true}, + &InstanceDiff{Destroy: false}, + &InstanceDiff{Destroy: false}, + }, + { + &EvalFilterDiff{Destroy: true}, + &InstanceDiff{Destroy: true}, + &InstanceDiff{Destroy: true}, + }, + { + &EvalFilterDiff{Destroy: true}, + &InstanceDiff{ + Destroy: true, + Attributes: map[string]*ResourceAttrDiff{ + "foo": &ResourceAttrDiff{}, + }, + }, + &InstanceDiff{Destroy: true}, + }, + { + &EvalFilterDiff{Destroy: true}, + &InstanceDiff{ + Attributes: map[string]*ResourceAttrDiff{ + "foo": &ResourceAttrDiff{ + RequiresNew: true, + }, + }, + }, + &InstanceDiff{Destroy: true}, + }, + { + &EvalFilterDiff{Destroy: true}, + &InstanceDiff{ + Attributes: map[string]*ResourceAttrDiff{ + "foo": &ResourceAttrDiff{}, + }, + }, + &InstanceDiff{Destroy: false}, + }, + } + + for i, tc := range cases { + var output *InstanceDiff + tc.Node.Diff = &tc.Input + tc.Node.Output = &output + if _, err := tc.Node.Eval(ctx); err != nil { + t.Fatalf("err: %s", err) + } + + if !reflect.DeepEqual(output, tc.Output) { + t.Fatalf("bad: %d\n\n%#v", i, output) + } + } +} diff --git a/terraform/eval_state.go b/terraform/eval_state.go index d584c0363..161752eb4 100644 --- a/terraform/eval_state.go +++ b/terraform/eval_state.go @@ -43,10 +43,9 @@ func (n *EvalReadState) Eval(ctx EvalContext) (interface{}, error) { if idx < 0 { idx = len(rs.Tainted) - 1 } - - if idx < len(rs.Tainted) { + if idx >= 0 && idx < len(rs.Tainted) { // Return the proper tainted resource - result = rs.Tainted[n.TaintedIndex] + result = rs.Tainted[idx] } } @@ -58,6 +57,25 @@ func (n *EvalReadState) Eval(ctx EvalContext) (interface{}, error) { return result, nil } +// EvalRequireState is an EvalNode implementation that early exits +// if the state doesn't have an ID. +type EvalRequireState struct { + State **InstanceState +} + +func (n *EvalRequireState) Eval(ctx EvalContext) (interface{}, error) { + if n.State == nil { + return nil, EvalEarlyExitError{} + } + + state := *n.State + if state == nil || state.ID == "" { + return nil, EvalEarlyExitError{} + } + + return nil, nil +} + // EvalUpdateStateHook is an EvalNode implementation that calls the // PostStateUpdate hook with the current state. type EvalUpdateStateHook struct{} diff --git a/terraform/eval_state_test.go b/terraform/eval_state_test.go index 0e9e98808..b2783da9a 100644 --- a/terraform/eval_state_test.go +++ b/terraform/eval_state_test.go @@ -5,6 +5,47 @@ import ( "testing" ) +func TestEvalRequireState(t *testing.T) { + ctx := new(MockEvalContext) + + cases := []struct { + State *InstanceState + Exit bool + }{ + { + nil, + true, + }, + { + &InstanceState{}, + true, + }, + { + &InstanceState{ID: "foo"}, + false, + }, + } + + var exitVal EvalEarlyExitError + for _, tc := range cases { + node := &EvalRequireState{State: &tc.State} + _, err := node.Eval(ctx) + if tc.Exit { + if err != exitVal { + t.Fatalf("should've exited: %#v", tc.State) + } + + continue + } + if !tc.Exit && err != nil { + t.Fatalf("shouldn't exit: %#v", tc.State) + } + if err != nil { + t.Fatalf("err: %s", err) + } + } +} + func TestEvalUpdateStateHook(t *testing.T) { mockHook := new(MockHook) diff --git a/terraform/transform_resource.go b/terraform/transform_resource.go index 19c9c418a..4b1985450 100644 --- a/terraform/transform_resource.go +++ b/terraform/transform_resource.go @@ -386,6 +386,15 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode { Name: n.stateId(), }, }, + + // We clear the diff out here so that future nodes + // don't see a diff that is already complete. There + // is no longer a diff! + &EvalWriteDiff{ + Name: n.stateId(), + Diff: nil, + }, + &EvalWriteState{ Name: n.stateId(), ResourceType: n.Resource.Type, @@ -455,6 +464,13 @@ func (n *graphNodeExpandedResourceDestroy) EvalTree() EvalNode { Diff: &diffApply, }, + // Filter the diff so we only get the destroy + &EvalFilterDiff{ + Diff: &diffApply, + Output: &diffApply, + Destroy: true, + }, + // If we're not destroying, then compare diffs &EvalIf{ If: func(ctx EvalContext) (bool, error) { @@ -477,6 +493,9 @@ func (n *graphNodeExpandedResourceDestroy) EvalTree() EvalNode { Tainted: n.Resource.Lifecycle.CreateBeforeDestroy, TaintedIndex: -1, }, + &EvalRequireState{ + State: &state, + }, &EvalApply{ Info: info, State: &state,