diff --git a/helper/resource/testing.go b/helper/resource/testing.go index 27bfc9b5a..af3d2dda4 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -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 diff --git a/helper/resource/testing_config.go b/helper/resource/testing_config.go index 300a9ea6e..033f1266d 100644 --- a/helper/resource/testing_config.go +++ b/helper/resource/testing_config.go @@ -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 +} diff --git a/helper/resource/testing_test.go b/helper/resource/testing_test.go index 7002ea6f8..e9c55fb57 100644 --- a/helper/resource/testing_test.go +++ b/helper/resource/testing_test.go @@ -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" {} `