helper/resource: Add ability to pre-taint resources

This adds the Taint field to the acceptance testing framework, allowing
the ability to pre-taint resources at the beginning of a particular
TestStep. This can be useful for when an explicit ForceNew is required
for a specific resource for troubleshooting things like diff mismatches,
etc.

The field accepts resource addresses as a list of strings. To keep
things simple for the time being, only addresses in the root module are
accepted. If we ever want to expand this past that, I'd be almost
inclined to add some facilities to the core terraform package to help
translate actual module resource addresses (ie:
module.foo.module.bar.some_resource.baz) into the correct state, versus
the current convention in some acceptance testing facilities that take
the module address as a list of strings (ie: []string{"root", "foo",
"bar"}).
This commit is contained in:
Chris Marchesi 2018-05-25 07:50:30 -07:00
parent 170a1530d1
commit 3505769600
No known key found for this signature in database
GPG Key ID: 8D6F1589D9834498
3 changed files with 113 additions and 0 deletions

View File

@ -266,6 +266,15 @@ type TestStep struct {
// below.
PreConfig func()
// Taint is a list of resource addresses to taint prior to the execution of
// the step. Be sure to only include this at a step where the referenced
// address will be present in state, as it will fail the test if the resource
// is missing.
//
// This option is ignored on ImportState tests, and currently only works for
// resources in the root module path.
Taint []string
//---------------------------------------------------------------
// Test modes. One of the following groups of settings must be
// set to determine what the test step will do. Ideally we would've

View File

@ -1,6 +1,7 @@
package resource
import (
"errors"
"fmt"
"log"
"strings"
@ -21,6 +22,14 @@ func testStep(
opts terraform.ContextOpts,
state *terraform.State,
step TestStep) (*terraform.State, error) {
// Pre-taint any resources that have been defined in Taint, as long as this
// is not a destroy step.
if !step.Destroy {
if err := testStepTaint(state, step); err != nil {
return state, err
}
}
mod, err := testModule(opts, step)
if err != nil {
return state, err
@ -154,3 +163,19 @@ func testStep(
// Made it here? Good job test step!
return state, nil
}
func testStepTaint(state *terraform.State, step TestStep) error {
for _, p := range step.Taint {
m := state.RootModule()
if m == nil {
return errors.New("no state")
}
rs, ok := m.Resources[p]
if !ok {
return fmt.Errorf("resource %q not found in state", p)
}
log.Printf("[WARN] Test: Explicitly tainting resource %q", p)
rs.Taint()
}
return nil
}

View File

@ -911,6 +911,85 @@ func mockSweeperFunc(s string) error {
return nil
}
func TestTest_Taint(t *testing.T) {
mp := testProvider()
mp.DiffFn = func(
_ *terraform.InstanceInfo,
state *terraform.InstanceState,
_ *terraform.ResourceConfig,
) (*terraform.InstanceDiff, error) {
return &terraform.InstanceDiff{
DestroyTainted: state.Tainted,
}, nil
}
mp.ApplyFn = func(
info *terraform.InstanceInfo,
state *terraform.InstanceState,
diff *terraform.InstanceDiff,
) (*terraform.InstanceState, error) {
var id string
switch {
case diff.Destroy && !diff.DestroyTainted:
return nil, nil
case diff.DestroyTainted:
id = "tainted"
default:
id = "not_tainted"
}
return &terraform.InstanceState{
ID: id,
}, nil
}
mp.RefreshFn = func(
_ *terraform.InstanceInfo,
state *terraform.InstanceState,
) (*terraform.InstanceState, error) {
return state, nil
}
mt := new(mockT)
Test(mt, TestCase{
Providers: map[string]terraform.ResourceProvider{
"test": mp,
},
Steps: []TestStep{
TestStep{
Config: testConfigStr,
Check: func(s *terraform.State) error {
rs := s.RootModule().Resources["test_instance.foo"]
if rs.Primary.ID != "not_tainted" {
return fmt.Errorf("expected not_tainted, got %s", rs.Primary.ID)
}
return nil
},
},
TestStep{
Taint: []string{"test_instance.foo"},
Config: testConfigStr,
Check: func(s *terraform.State) error {
rs := s.RootModule().Resources["test_instance.foo"]
if rs.Primary.ID != "tainted" {
return fmt.Errorf("expected tainted, got %s", rs.Primary.ID)
}
return nil
},
},
TestStep{
Taint: []string{"test_instance.fooo"},
Config: testConfigStr,
ExpectError: regexp.MustCompile("resource \"test_instance.fooo\" not found in state"),
},
},
})
if mt.failed() {
t.Fatalf("test failure: %s", mt.failMessage())
}
}
const testConfigStr = `
resource "test_instance" "foo" {}
`