github.com/andresvia/terraform@v0.6.15-0.20160412045437-d51c75946785/helper/resource/testing.go (about)

     1  package resource
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"io/ioutil"
     7  	"log"
     8  	"os"
     9  	"path/filepath"
    10  	"regexp"
    11  	"strings"
    12  	"testing"
    13  
    14  	"github.com/hashicorp/go-getter"
    15  	"github.com/hashicorp/terraform/config/module"
    16  	"github.com/hashicorp/terraform/helper/logging"
    17  	"github.com/hashicorp/terraform/terraform"
    18  )
    19  
    20  const TestEnvVar = "TF_ACC"
    21  
    22  // UnitTestOverride is a value that when set in TestEnvVar indicates that this
    23  // is a unit test borrowing the acceptance testing framework.
    24  const UnitTestOverride = "UnitTestOverride"
    25  
    26  // TestCheckFunc is the callback type used with acceptance tests to check
    27  // the state of a resource. The state passed in is the latest state known,
    28  // or in the case of being after a destroy, it is the last known state when
    29  // it was created.
    30  type TestCheckFunc func(*terraform.State) error
    31  
    32  // TestCase is a single acceptance test case used to test the apply/destroy
    33  // lifecycle of a resource in a specific configuration.
    34  //
    35  // When the destroy plan is executed, the config from the last TestStep
    36  // is used to plan it.
    37  type TestCase struct {
    38  	// PreCheck, if non-nil, will be called before any test steps are
    39  	// executed. It will only be executed in the case that the steps
    40  	// would run, so it can be used for some validation before running
    41  	// acceptance tests, such as verifying that keys are setup.
    42  	PreCheck func()
    43  
    44  	// Providers is the ResourceProvider that will be under test.
    45  	//
    46  	// Alternately, ProviderFactories can be specified for the providers
    47  	// that are valid. This takes priority over Providers.
    48  	//
    49  	// The end effect of each is the same: specifying the providers that
    50  	// are used within the tests.
    51  	Providers         map[string]terraform.ResourceProvider
    52  	ProviderFactories map[string]terraform.ResourceProviderFactory
    53  
    54  	// CheckDestroy is called after the resource is finally destroyed
    55  	// to allow the tester to test that the resource is truly gone.
    56  	CheckDestroy TestCheckFunc
    57  
    58  	// Steps are the apply sequences done within the context of the
    59  	// same state. Each step can have its own check to verify correctness.
    60  	Steps []TestStep
    61  }
    62  
    63  // TestStep is a single apply sequence of a test, done within the
    64  // context of a state.
    65  //
    66  // Multiple TestSteps can be sequenced in a Test to allow testing
    67  // potentially complex update logic. In general, simply create/destroy
    68  // tests will only need one step.
    69  type TestStep struct {
    70  	// PreConfig is called before the Config is applied to perform any per-step
    71  	// setup that needs to happen
    72  	PreConfig func()
    73  
    74  	// Config a string of the configuration to give to Terraform.
    75  	Config string
    76  
    77  	// Check is called after the Config is applied. Use this step to
    78  	// make your own API calls to check the status of things, and to
    79  	// inspect the format of the ResourceState itself.
    80  	//
    81  	// If an error is returned, the test will fail. In this case, a
    82  	// destroy plan will still be attempted.
    83  	//
    84  	// If this is nil, no check is done on this step.
    85  	Check TestCheckFunc
    86  
    87  	// Destroy will create a destroy plan if set to true.
    88  	Destroy bool
    89  
    90  	// ExpectNonEmptyPlan can be set to true for specific types of tests that are
    91  	// looking to verify that a diff occurs
    92  	ExpectNonEmptyPlan bool
    93  }
    94  
    95  // Test performs an acceptance test on a resource.
    96  //
    97  // Tests are not run unless an environmental variable "TF_ACC" is
    98  // set to some non-empty value. This is to avoid test cases surprising
    99  // a user by creating real resources.
   100  //
   101  // Tests will fail unless the verbose flag (`go test -v`, or explicitly
   102  // the "-test.v" flag) is set. Because some acceptance tests take quite
   103  // long, we require the verbose flag so users are able to see progress
   104  // output.
   105  func Test(t TestT, c TestCase) {
   106  	// We only run acceptance tests if an env var is set because they're
   107  	// slow and generally require some outside configuration.
   108  	if os.Getenv(TestEnvVar) == "" {
   109  		t.Skip(fmt.Sprintf(
   110  			"Acceptance tests skipped unless env '%s' set",
   111  			TestEnvVar))
   112  		return
   113  	}
   114  
   115  	isUnitTest := (os.Getenv(TestEnvVar) == UnitTestOverride)
   116  
   117  	logWriter, err := logging.LogOutput()
   118  	if err != nil {
   119  		t.Error(fmt.Errorf("error setting up logging: %s", err))
   120  	}
   121  	log.SetOutput(logWriter)
   122  
   123  	// We require verbose mode so that the user knows what is going on.
   124  	if !testTesting && !testing.Verbose() && !isUnitTest {
   125  		t.Fatal("Acceptance tests must be run with the -v flag on tests")
   126  		return
   127  	}
   128  
   129  	// Run the PreCheck if we have it
   130  	if c.PreCheck != nil {
   131  		c.PreCheck()
   132  	}
   133  
   134  	// Build our context options that we can
   135  	ctxProviders := c.ProviderFactories
   136  	if ctxProviders == nil {
   137  		ctxProviders = make(map[string]terraform.ResourceProviderFactory)
   138  		for k, p := range c.Providers {
   139  			ctxProviders[k] = terraform.ResourceProviderFactoryFixed(p)
   140  		}
   141  	}
   142  	opts := terraform.ContextOpts{Providers: ctxProviders}
   143  
   144  	// A single state variable to track the lifecycle, starting with no state
   145  	var state *terraform.State
   146  
   147  	// Go through each step and run it
   148  	for i, step := range c.Steps {
   149  		var err error
   150  		log.Printf("[WARN] Test: Executing step %d", i)
   151  		state, err = testStep(opts, state, step)
   152  		if err != nil {
   153  			t.Error(fmt.Sprintf(
   154  				"Step %d error: %s", i, err))
   155  			break
   156  		}
   157  	}
   158  
   159  	// If we have a state, then run the destroy
   160  	if state != nil {
   161  		destroyStep := TestStep{
   162  			Config:  c.Steps[len(c.Steps)-1].Config,
   163  			Check:   c.CheckDestroy,
   164  			Destroy: true,
   165  		}
   166  
   167  		log.Printf("[WARN] Test: Executing destroy step")
   168  		state, err := testStep(opts, state, destroyStep)
   169  		if err != nil {
   170  			t.Error(fmt.Sprintf(
   171  				"Error destroying resource! WARNING: Dangling resources\n"+
   172  					"may exist. The full state and error is shown below.\n\n"+
   173  					"Error: %s\n\nState: %s",
   174  				err,
   175  				state))
   176  		}
   177  	} else {
   178  		log.Printf("[WARN] Skipping destroy test since there is no state.")
   179  	}
   180  }
   181  
   182  // UnitTest is a helper to force the acceptance testing harness to run in the
   183  // normal unit test suite. This should only be used for resource that don't
   184  // have any external dependencies.
   185  func UnitTest(t TestT, c TestCase) {
   186  	oldEnv := os.Getenv(TestEnvVar)
   187  	if err := os.Setenv(TestEnvVar, UnitTestOverride); err != nil {
   188  		t.Fatal(err)
   189  	}
   190  	defer func() {
   191  		if err := os.Setenv(TestEnvVar, oldEnv); err != nil {
   192  			t.Fatal(err)
   193  		}
   194  	}()
   195  	Test(t, c)
   196  }
   197  
   198  func testStep(
   199  	opts terraform.ContextOpts,
   200  	state *terraform.State,
   201  	step TestStep) (*terraform.State, error) {
   202  	if step.PreConfig != nil {
   203  		step.PreConfig()
   204  	}
   205  
   206  	cfgPath, err := ioutil.TempDir("", "tf-test")
   207  	if err != nil {
   208  		return state, fmt.Errorf(
   209  			"Error creating temporary directory for config: %s", err)
   210  	}
   211  	defer os.RemoveAll(cfgPath)
   212  
   213  	// Write the configuration
   214  	cfgF, err := os.Create(filepath.Join(cfgPath, "main.tf"))
   215  	if err != nil {
   216  		return state, fmt.Errorf(
   217  			"Error creating temporary file for config: %s", err)
   218  	}
   219  
   220  	_, err = io.Copy(cfgF, strings.NewReader(step.Config))
   221  	cfgF.Close()
   222  	if err != nil {
   223  		return state, fmt.Errorf(
   224  			"Error creating temporary file for config: %s", err)
   225  	}
   226  
   227  	// Parse the configuration
   228  	mod, err := module.NewTreeModule("", cfgPath)
   229  	if err != nil {
   230  		return state, fmt.Errorf(
   231  			"Error loading configuration: %s", err)
   232  	}
   233  
   234  	// Load the modules
   235  	modStorage := &getter.FolderStorage{
   236  		StorageDir: filepath.Join(cfgPath, ".tfmodules"),
   237  	}
   238  	err = mod.Load(modStorage, module.GetModeGet)
   239  	if err != nil {
   240  		return state, fmt.Errorf("Error downloading modules: %s", err)
   241  	}
   242  
   243  	// Build the context
   244  	opts.Module = mod
   245  	opts.State = state
   246  	opts.Destroy = step.Destroy
   247  	ctx := terraform.NewContext(&opts)
   248  	if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 {
   249  		if len(es) > 0 {
   250  			estrs := make([]string, len(es))
   251  			for i, e := range es {
   252  				estrs[i] = e.Error()
   253  			}
   254  			return state, fmt.Errorf(
   255  				"Configuration is invalid.\n\nWarnings: %#v\n\nErrors: %#v",
   256  				ws, estrs)
   257  		}
   258  		log.Printf("[WARN] Config warnings: %#v", ws)
   259  	}
   260  
   261  	// Refresh!
   262  	state, err = ctx.Refresh()
   263  	if err != nil {
   264  		return state, fmt.Errorf(
   265  			"Error refreshing: %s", err)
   266  	}
   267  
   268  	// Plan!
   269  	if p, err := ctx.Plan(); err != nil {
   270  		return state, fmt.Errorf(
   271  			"Error planning: %s", err)
   272  	} else {
   273  		log.Printf("[WARN] Test: Step plan: %s", p)
   274  	}
   275  
   276  	// We need to keep a copy of the state prior to destroying
   277  	// such that destroy steps can verify their behaviour in the check
   278  	// function
   279  	stateBeforeApplication := state.DeepCopy()
   280  
   281  	// Apply!
   282  	state, err = ctx.Apply()
   283  	if err != nil {
   284  		return state, fmt.Errorf("Error applying: %s", err)
   285  	}
   286  
   287  	// Check! Excitement!
   288  	if step.Check != nil {
   289  		if step.Destroy {
   290  			if err := step.Check(stateBeforeApplication); err != nil {
   291  				return state, fmt.Errorf("Check failed: %s", err)
   292  			}
   293  		} else {
   294  			if err := step.Check(state); err != nil {
   295  				return state, fmt.Errorf("Check failed: %s", err)
   296  			}
   297  		}
   298  	}
   299  
   300  	// Now, verify that Plan is now empty and we don't have a perpetual diff issue
   301  	// We do this with TWO plans. One without a refresh.
   302  	var p *terraform.Plan
   303  	if p, err = ctx.Plan(); err != nil {
   304  		return state, fmt.Errorf("Error on follow-up plan: %s", err)
   305  	}
   306  	if p.Diff != nil && !p.Diff.Empty() {
   307  		if step.ExpectNonEmptyPlan {
   308  			log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", p)
   309  		} else {
   310  			return state, fmt.Errorf(
   311  				"After applying this step, the plan was not empty:\n\n%s", p)
   312  		}
   313  	}
   314  
   315  	// And another after a Refresh.
   316  	state, err = ctx.Refresh()
   317  	if err != nil {
   318  		return state, fmt.Errorf(
   319  			"Error on follow-up refresh: %s", err)
   320  	}
   321  	if p, err = ctx.Plan(); err != nil {
   322  		return state, fmt.Errorf("Error on second follow-up plan: %s", err)
   323  	}
   324  	if p.Diff != nil && !p.Diff.Empty() {
   325  		if step.ExpectNonEmptyPlan {
   326  			log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", p)
   327  		} else {
   328  			return state, fmt.Errorf(
   329  				"After applying this step and refreshing, "+
   330  					"the plan was not empty:\n\n%s", p)
   331  		}
   332  	}
   333  
   334  	// Made it here, but expected a non-empty plan, fail!
   335  	if step.ExpectNonEmptyPlan && (p.Diff == nil || p.Diff.Empty()) {
   336  		return state, fmt.Errorf("Expected a non-empty plan, but got an empty plan!")
   337  	}
   338  
   339  	// Made it here? Good job test step!
   340  	return state, nil
   341  }
   342  
   343  // ComposeTestCheckFunc lets you compose multiple TestCheckFuncs into
   344  // a single TestCheckFunc.
   345  //
   346  // As a user testing their provider, this lets you decompose your checks
   347  // into smaller pieces more easily.
   348  func ComposeTestCheckFunc(fs ...TestCheckFunc) TestCheckFunc {
   349  	return func(s *terraform.State) error {
   350  		for i, f := range fs {
   351  			if err := f(s); err != nil {
   352  				return fmt.Errorf("Check %d/%d error: %s", i+1, len(fs), err)
   353  			}
   354  		}
   355  
   356  		return nil
   357  	}
   358  }
   359  
   360  func TestCheckResourceAttr(name, key, value string) TestCheckFunc {
   361  	return func(s *terraform.State) error {
   362  		ms := s.RootModule()
   363  		rs, ok := ms.Resources[name]
   364  		if !ok {
   365  			return fmt.Errorf("Not found: %s", name)
   366  		}
   367  
   368  		is := rs.Primary
   369  		if is == nil {
   370  			return fmt.Errorf("No primary instance: %s", name)
   371  		}
   372  
   373  		if is.Attributes[key] != value {
   374  			return fmt.Errorf(
   375  				"%s: Attribute '%s' expected %#v, got %#v",
   376  				name,
   377  				key,
   378  				value,
   379  				is.Attributes[key])
   380  		}
   381  
   382  		return nil
   383  	}
   384  }
   385  
   386  func TestMatchResourceAttr(name, key string, r *regexp.Regexp) TestCheckFunc {
   387  	return func(s *terraform.State) error {
   388  		ms := s.RootModule()
   389  		rs, ok := ms.Resources[name]
   390  		if !ok {
   391  			return fmt.Errorf("Not found: %s", name)
   392  		}
   393  
   394  		is := rs.Primary
   395  		if is == nil {
   396  			return fmt.Errorf("No primary instance: %s", name)
   397  		}
   398  
   399  		if !r.MatchString(is.Attributes[key]) {
   400  			return fmt.Errorf(
   401  				"%s: Attribute '%s' didn't match %q, got %#v",
   402  				name,
   403  				key,
   404  				r.String(),
   405  				is.Attributes[key])
   406  		}
   407  
   408  		return nil
   409  	}
   410  }
   411  
   412  // TestCheckResourceAttrPtr is like TestCheckResourceAttr except the
   413  // value is a pointer so that it can be updated while the test is running.
   414  // It will only be dereferenced at the point this step is run.
   415  func TestCheckResourceAttrPtr(name string, key string, value *string) TestCheckFunc {
   416  	return func(s *terraform.State) error {
   417  		return TestCheckResourceAttr(name, key, *value)(s)
   418  	}
   419  }
   420  
   421  // TestCheckOutput checks an output in the Terraform configuration
   422  func TestCheckOutput(name, value string) TestCheckFunc {
   423  	return func(s *terraform.State) error {
   424  		ms := s.RootModule()
   425  		rs, ok := ms.Outputs[name]
   426  		if !ok {
   427  			return fmt.Errorf("Not found: %s", name)
   428  		}
   429  
   430  		if rs != value {
   431  			return fmt.Errorf(
   432  				"Output '%s': expected %#v, got %#v",
   433  				name,
   434  				value,
   435  				rs)
   436  		}
   437  
   438  		return nil
   439  	}
   440  }
   441  
   442  // TestT is the interface used to handle the test lifecycle of a test.
   443  //
   444  // Users should just use a *testing.T object, which implements this.
   445  type TestT interface {
   446  	Error(args ...interface{})
   447  	Fatal(args ...interface{})
   448  	Skip(args ...interface{})
   449  }
   450  
   451  // This is set to true by unit tests to alter some behavior
   452  var testTesting = false