From 3505769600eec4a85a3be203a9c7eba7f97c5ade Mon Sep 17 00:00:00 2001 From: Chris Marchesi Date: Fri, 25 May 2018 07:50:30 -0700 Subject: [PATCH] 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"}). --- helper/resource/testing.go | 9 ++++ helper/resource/testing_config.go | 25 ++++++++++ helper/resource/testing_test.go | 79 +++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+) 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" {} `