helper/resource: basic ImportState acceptance testing
Still some TODOs, and more test cases to write, but the basics are all here.
This commit is contained in:
parent
9bd1c9e7ca
commit
2d99c451fb
|
@ -31,6 +31,9 @@ const UnitTestOverride = "UnitTestOverride"
|
||||||
// it was created.
|
// it was created.
|
||||||
type TestCheckFunc func(*terraform.State) error
|
type TestCheckFunc func(*terraform.State) error
|
||||||
|
|
||||||
|
// ImportStateCheckFunc is the check function for ImportState tests
|
||||||
|
type ImportStateCheckFunc func([]*terraform.InstanceState) error
|
||||||
|
|
||||||
// TestCase is a single acceptance test case used to test the apply/destroy
|
// TestCase is a single acceptance test case used to test the apply/destroy
|
||||||
// lifecycle of a resource in a specific configuration.
|
// lifecycle of a resource in a specific configuration.
|
||||||
//
|
//
|
||||||
|
@ -81,6 +84,14 @@ type TestCase struct {
|
||||||
// potentially complex update logic. In general, simply create/destroy
|
// potentially complex update logic. In general, simply create/destroy
|
||||||
// tests will only need one step.
|
// tests will only need one step.
|
||||||
type TestStep struct {
|
type TestStep struct {
|
||||||
|
// ResourceName should be set to the name of the resource
|
||||||
|
// that is being tested. Example: "aws_instance.foo". Various test
|
||||||
|
// modes use this to auto-detect state information.
|
||||||
|
//
|
||||||
|
// This is only required if the test mode settings below say it is
|
||||||
|
// for the mode you're using.
|
||||||
|
ResourceName string
|
||||||
|
|
||||||
// PreConfig is called before the Config is applied to perform any per-step
|
// PreConfig is called before the Config is applied to perform any per-step
|
||||||
// setup that needs to happen. This is called regardless of "test mode"
|
// setup that needs to happen. This is called regardless of "test mode"
|
||||||
// below.
|
// below.
|
||||||
|
@ -119,6 +130,25 @@ type TestStep struct {
|
||||||
// ExpectNonEmptyPlan can be set to true for specific types of tests that are
|
// ExpectNonEmptyPlan can be set to true for specific types of tests that are
|
||||||
// looking to verify that a diff occurs
|
// looking to verify that a diff occurs
|
||||||
ExpectNonEmptyPlan bool
|
ExpectNonEmptyPlan bool
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// ImportState testing
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
|
||||||
|
// ImportState, if true, will test the functionality of ImportState
|
||||||
|
// by importing the resource with ResourceName (must be set) and the
|
||||||
|
// ID of that resource.
|
||||||
|
ImportState bool
|
||||||
|
|
||||||
|
// ImportStateId is the ID to perform an ImportState operation with.
|
||||||
|
// This is optional. If it isn't set, then the resource ID is automatically
|
||||||
|
// determined by inspecting the state for ResourceName's ID.
|
||||||
|
ImportStateId string
|
||||||
|
|
||||||
|
// ImportStateCheck checks the results of ImportState. It should be
|
||||||
|
// used to verify that the resulting value of ImportState has the
|
||||||
|
// proper resources, IDs, and attributes.
|
||||||
|
ImportStateCheck ImportStateCheckFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test performs an acceptance test on a resource.
|
// Test performs an acceptance test on a resource.
|
||||||
|
@ -180,7 +210,19 @@ func Test(t TestT, c TestCase) {
|
||||||
for i, step := range c.Steps {
|
for i, step := range c.Steps {
|
||||||
var err error
|
var err error
|
||||||
log.Printf("[WARN] Test: Executing step %d", i)
|
log.Printf("[WARN] Test: Executing step %d", i)
|
||||||
state, err = testStep(opts, state, step)
|
|
||||||
|
// Determine the test mode to execute
|
||||||
|
if step.Config != "" {
|
||||||
|
state, err = testStepConfig(opts, state, step)
|
||||||
|
} else if step.ImportState {
|
||||||
|
state, err = testStepImportState(opts, state, step)
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf(
|
||||||
|
"unknown test mode for step. Please see TestStep docs\n\n%#v",
|
||||||
|
step)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there was an error, exit
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errored = true
|
errored = true
|
||||||
t.Error(fmt.Sprintf(
|
t.Error(fmt.Sprintf(
|
||||||
|
@ -215,7 +257,7 @@ func Test(t TestT, c TestCase) {
|
||||||
if err := testIDOnlyRefresh(c, opts, step, idRefreshCheck); err != nil {
|
if err := testIDOnlyRefresh(c, opts, step, idRefreshCheck); err != nil {
|
||||||
log.Printf("[ERROR] Test: ID-only test failed: %s", err)
|
log.Printf("[ERROR] Test: ID-only test failed: %s", err)
|
||||||
t.Error(fmt.Sprintf(
|
t.Error(fmt.Sprintf(
|
||||||
"ID-Only refresh test failure: %s", err))
|
"[ERROR] Test: ID-only test failed: %s", err))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -367,118 +409,6 @@ func testIDOnlyRefresh(c TestCase, opts terraform.ContextOpts, step TestStep, r
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func testStep(
|
|
||||||
opts terraform.ContextOpts,
|
|
||||||
state *terraform.State,
|
|
||||||
step TestStep) (*terraform.State, error) {
|
|
||||||
mod, err := testModule(opts, step)
|
|
||||||
if err != nil {
|
|
||||||
return state, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the context
|
|
||||||
opts.Module = mod
|
|
||||||
opts.State = state
|
|
||||||
opts.Destroy = step.Destroy
|
|
||||||
ctx, err := terraform.NewContext(&opts)
|
|
||||||
if err != nil {
|
|
||||||
return state, fmt.Errorf("Error initializing context: %s", err)
|
|
||||||
}
|
|
||||||
if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 {
|
|
||||||
if len(es) > 0 {
|
|
||||||
estrs := make([]string, len(es))
|
|
||||||
for i, e := range es {
|
|
||||||
estrs[i] = e.Error()
|
|
||||||
}
|
|
||||||
return state, fmt.Errorf(
|
|
||||||
"Configuration is invalid.\n\nWarnings: %#v\n\nErrors: %#v",
|
|
||||||
ws, estrs)
|
|
||||||
}
|
|
||||||
log.Printf("[WARN] Config warnings: %#v", ws)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh!
|
|
||||||
state, err = ctx.Refresh()
|
|
||||||
if err != nil {
|
|
||||||
return state, fmt.Errorf(
|
|
||||||
"Error refreshing: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Plan!
|
|
||||||
if p, err := ctx.Plan(); err != nil {
|
|
||||||
return state, fmt.Errorf(
|
|
||||||
"Error planning: %s", err)
|
|
||||||
} else {
|
|
||||||
log.Printf("[WARN] Test: Step plan: %s", p)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need to keep a copy of the state prior to destroying
|
|
||||||
// such that destroy steps can verify their behaviour in the check
|
|
||||||
// function
|
|
||||||
stateBeforeApplication := state.DeepCopy()
|
|
||||||
|
|
||||||
// Apply!
|
|
||||||
state, err = ctx.Apply()
|
|
||||||
if err != nil {
|
|
||||||
return state, fmt.Errorf("Error applying: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check! Excitement!
|
|
||||||
if step.Check != nil {
|
|
||||||
if step.Destroy {
|
|
||||||
if err := step.Check(stateBeforeApplication); err != nil {
|
|
||||||
return state, fmt.Errorf("Check failed: %s", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := step.Check(state); err != nil {
|
|
||||||
return state, fmt.Errorf("Check failed: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now, verify that Plan is now empty and we don't have a perpetual diff issue
|
|
||||||
// We do this with TWO plans. One without a refresh.
|
|
||||||
var p *terraform.Plan
|
|
||||||
if p, err = ctx.Plan(); err != nil {
|
|
||||||
return state, fmt.Errorf("Error on follow-up plan: %s", err)
|
|
||||||
}
|
|
||||||
if p.Diff != nil && !p.Diff.Empty() {
|
|
||||||
if step.ExpectNonEmptyPlan {
|
|
||||||
log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", p)
|
|
||||||
} else {
|
|
||||||
return state, fmt.Errorf(
|
|
||||||
"After applying this step, the plan was not empty:\n\n%s", p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// And another after a Refresh.
|
|
||||||
state, err = ctx.Refresh()
|
|
||||||
if err != nil {
|
|
||||||
return state, fmt.Errorf(
|
|
||||||
"Error on follow-up refresh: %s", err)
|
|
||||||
}
|
|
||||||
if p, err = ctx.Plan(); err != nil {
|
|
||||||
return state, fmt.Errorf("Error on second follow-up plan: %s", err)
|
|
||||||
}
|
|
||||||
if p.Diff != nil && !p.Diff.Empty() {
|
|
||||||
if step.ExpectNonEmptyPlan {
|
|
||||||
log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", p)
|
|
||||||
} else {
|
|
||||||
return state, fmt.Errorf(
|
|
||||||
"After applying this step and refreshing, "+
|
|
||||||
"the plan was not empty:\n\n%s", p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Made it here, but expected a non-empty plan, fail!
|
|
||||||
if step.ExpectNonEmptyPlan && (p.Diff == nil || p.Diff.Empty()) {
|
|
||||||
return state, fmt.Errorf("Expected a non-empty plan, but got an empty plan!")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Made it here? Good job test step!
|
|
||||||
return state, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func testModule(
|
func testModule(
|
||||||
opts terraform.ContextOpts,
|
opts terraform.ContextOpts,
|
||||||
step TestStep) (*module.Tree, error) {
|
step TestStep) (*module.Tree, error) {
|
||||||
|
@ -526,6 +456,23 @@ func testModule(
|
||||||
return mod, nil
|
return mod, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testResource(c TestStep, state *terraform.State) (*terraform.ResourceState, error) {
|
||||||
|
if c.ResourceName == "" {
|
||||||
|
return nil, fmt.Errorf("ResourceName must be set in TestStep")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range state.Modules {
|
||||||
|
if len(m.Resources) > 0 {
|
||||||
|
if v, ok := m.Resources[c.ResourceName]; ok {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"Resource specified by ResourceName couldn't be found: %s", c.ResourceName)
|
||||||
|
}
|
||||||
|
|
||||||
// ComposeTestCheckFunc lets you compose multiple TestCheckFuncs into
|
// ComposeTestCheckFunc lets you compose multiple TestCheckFuncs into
|
||||||
// a single TestCheckFunc.
|
// a single TestCheckFunc.
|
||||||
//
|
//
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
// testStepConfig runs a config-mode test step
|
||||||
|
func testStepConfig(
|
||||||
|
opts terraform.ContextOpts,
|
||||||
|
state *terraform.State,
|
||||||
|
step TestStep) (*terraform.State, error) {
|
||||||
|
return testStep(opts, state, step)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStep(
|
||||||
|
opts terraform.ContextOpts,
|
||||||
|
state *terraform.State,
|
||||||
|
step TestStep) (*terraform.State, error) {
|
||||||
|
mod, err := testModule(opts, step)
|
||||||
|
if err != nil {
|
||||||
|
return state, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the context
|
||||||
|
opts.Module = mod
|
||||||
|
opts.State = state
|
||||||
|
opts.Destroy = step.Destroy
|
||||||
|
ctx, err := terraform.NewContext(&opts)
|
||||||
|
if err != nil {
|
||||||
|
return state, fmt.Errorf("Error initializing context: %s", err)
|
||||||
|
}
|
||||||
|
if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 {
|
||||||
|
if len(es) > 0 {
|
||||||
|
estrs := make([]string, len(es))
|
||||||
|
for i, e := range es {
|
||||||
|
estrs[i] = e.Error()
|
||||||
|
}
|
||||||
|
return state, fmt.Errorf(
|
||||||
|
"Configuration is invalid.\n\nWarnings: %#v\n\nErrors: %#v",
|
||||||
|
ws, estrs)
|
||||||
|
}
|
||||||
|
log.Printf("[WARN] Config warnings: %#v", ws)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh!
|
||||||
|
state, err = ctx.Refresh()
|
||||||
|
if err != nil {
|
||||||
|
return state, fmt.Errorf(
|
||||||
|
"Error refreshing: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plan!
|
||||||
|
if p, err := ctx.Plan(); err != nil {
|
||||||
|
return state, fmt.Errorf(
|
||||||
|
"Error planning: %s", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("[WARN] Test: Step plan: %s", p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to keep a copy of the state prior to destroying
|
||||||
|
// such that destroy steps can verify their behaviour in the check
|
||||||
|
// function
|
||||||
|
stateBeforeApplication := state.DeepCopy()
|
||||||
|
|
||||||
|
// Apply!
|
||||||
|
state, err = ctx.Apply()
|
||||||
|
if err != nil {
|
||||||
|
return state, fmt.Errorf("Error applying: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check! Excitement!
|
||||||
|
if step.Check != nil {
|
||||||
|
if step.Destroy {
|
||||||
|
if err := step.Check(stateBeforeApplication); err != nil {
|
||||||
|
return state, fmt.Errorf("Check failed: %s", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := step.Check(state); err != nil {
|
||||||
|
return state, fmt.Errorf("Check failed: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, verify that Plan is now empty and we don't have a perpetual diff issue
|
||||||
|
// We do this with TWO plans. One without a refresh.
|
||||||
|
var p *terraform.Plan
|
||||||
|
if p, err = ctx.Plan(); err != nil {
|
||||||
|
return state, fmt.Errorf("Error on follow-up plan: %s", err)
|
||||||
|
}
|
||||||
|
if p.Diff != nil && !p.Diff.Empty() {
|
||||||
|
if step.ExpectNonEmptyPlan {
|
||||||
|
log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", p)
|
||||||
|
} else {
|
||||||
|
return state, fmt.Errorf(
|
||||||
|
"After applying this step, the plan was not empty:\n\n%s", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// And another after a Refresh.
|
||||||
|
state, err = ctx.Refresh()
|
||||||
|
if err != nil {
|
||||||
|
return state, fmt.Errorf(
|
||||||
|
"Error on follow-up refresh: %s", err)
|
||||||
|
}
|
||||||
|
if p, err = ctx.Plan(); err != nil {
|
||||||
|
return state, fmt.Errorf("Error on second follow-up plan: %s", err)
|
||||||
|
}
|
||||||
|
if p.Diff != nil && !p.Diff.Empty() {
|
||||||
|
if step.ExpectNonEmptyPlan {
|
||||||
|
log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", p)
|
||||||
|
} else {
|
||||||
|
return state, fmt.Errorf(
|
||||||
|
"After applying this step and refreshing, "+
|
||||||
|
"the plan was not empty:\n\n%s", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Made it here, but expected a non-empty plan, fail!
|
||||||
|
if step.ExpectNonEmptyPlan && (p.Diff == nil || p.Diff.Empty()) {
|
||||||
|
return state, fmt.Errorf("Expected a non-empty plan, but got an empty plan!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Made it here? Good job test step!
|
||||||
|
return state, nil
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
// testStepImportState runs an imort state test step
|
||||||
|
func testStepImportState(
|
||||||
|
opts terraform.ContextOpts,
|
||||||
|
state *terraform.State,
|
||||||
|
step TestStep) (*terraform.State, error) {
|
||||||
|
// Determine the ID to import
|
||||||
|
importId := step.ImportStateId
|
||||||
|
if importId == "" {
|
||||||
|
resource, err := testResource(step, state)
|
||||||
|
if err != nil {
|
||||||
|
return state, err
|
||||||
|
}
|
||||||
|
|
||||||
|
importId = resource.Primary.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup the context. We initialize with an empty state. We use the
|
||||||
|
// full config for provider configurations.
|
||||||
|
mod, err := testModule(opts, step)
|
||||||
|
if err != nil {
|
||||||
|
return state, err
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.Module = mod
|
||||||
|
opts.State = terraform.NewState()
|
||||||
|
ctx, err := terraform.NewContext(&opts)
|
||||||
|
if err != nil {
|
||||||
|
return state, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: ImportOpts needs a flag to read a config module so it
|
||||||
|
// can load our provider config without env vars.
|
||||||
|
|
||||||
|
// Do the import!
|
||||||
|
newState, err := ctx.Import(&terraform.ImportOpts{
|
||||||
|
Targets: []*terraform.ImportTarget{
|
||||||
|
&terraform.ImportTarget{
|
||||||
|
Addr: step.ResourceName,
|
||||||
|
ID: importId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] Test: ImportState failure: %s", err)
|
||||||
|
return state, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go through the new state and verify
|
||||||
|
if step.ImportStateCheck != nil {
|
||||||
|
var states []*terraform.InstanceState
|
||||||
|
for _, r := range newState.RootModule().Resources {
|
||||||
|
if r.Primary != nil {
|
||||||
|
states = append(states, r.Primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := step.ImportStateCheck(states); err != nil {
|
||||||
|
return state, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the old state (non-imported) so we don't change anything.
|
||||||
|
return state, nil
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTest_importState(t *testing.T) {
|
||||||
|
mp := testProvider()
|
||||||
|
mp.ImportStateReturn = []*terraform.InstanceState{
|
||||||
|
&terraform.InstanceState{
|
||||||
|
ID: "foo",
|
||||||
|
Ephemeral: terraform.EphemeralState{Type: "test_instance"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mp.RefreshFn = func(
|
||||||
|
i *terraform.InstanceInfo,
|
||||||
|
s *terraform.InstanceState) (*terraform.InstanceState, error) {
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
checked := false
|
||||||
|
checkFn := func(s []*terraform.InstanceState) error {
|
||||||
|
checked = true
|
||||||
|
|
||||||
|
if s[0].ID != "foo" {
|
||||||
|
return fmt.Errorf("bad: %#v", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mt := new(mockT)
|
||||||
|
Test(mt, TestCase{
|
||||||
|
Providers: map[string]terraform.ResourceProvider{
|
||||||
|
"test": mp,
|
||||||
|
},
|
||||||
|
|
||||||
|
Steps: []TestStep{
|
||||||
|
TestStep{
|
||||||
|
ResourceName: "test_instance.foo",
|
||||||
|
ImportState: true,
|
||||||
|
ImportStateId: "foo",
|
||||||
|
ImportStateCheck: checkFn,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if mt.failed() {
|
||||||
|
t.Fatalf("test failed: %s", mt.failMessage())
|
||||||
|
}
|
||||||
|
if !checked {
|
||||||
|
t.Fatal("didn't call check")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue