github.com/danrjohnson/terraform@v0.7.0-rc2.0.20160627135212-d0fc1fa086ff/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  	"reflect"
    11  	"regexp"
    12  	"strings"
    13  	"testing"
    14  
    15  	"github.com/davecgh/go-spew/spew"
    16  	"github.com/hashicorp/go-getter"
    17  	"github.com/hashicorp/terraform/config/module"
    18  	"github.com/hashicorp/terraform/helper/logging"
    19  	"github.com/hashicorp/terraform/terraform"
    20  )
    21  
    22  const TestEnvVar = "TF_ACC"
    23  
    24  // UnitTestOverride is a value that when set in TestEnvVar indicates that this
    25  // is a unit test borrowing the acceptance testing framework.
    26  const UnitTestOverride = "UnitTestOverride"
    27  
    28  // TestCheckFunc is the callback type used with acceptance tests to check
    29  // the state of a resource. The state passed in is the latest state known,
    30  // or in the case of being after a destroy, it is the last known state when
    31  // it was created.
    32  type TestCheckFunc func(*terraform.State) error
    33  
    34  // ImportStateCheckFunc is the check function for ImportState tests
    35  type ImportStateCheckFunc func([]*terraform.InstanceState) error
    36  
    37  // TestCase is a single acceptance test case used to test the apply/destroy
    38  // lifecycle of a resource in a specific configuration.
    39  //
    40  // When the destroy plan is executed, the config from the last TestStep
    41  // is used to plan it.
    42  type TestCase struct {
    43  	// OverrideEnvVar allows a test to run regardless of the TF_ACC
    44  	// environment variable. This should be used with care - only for
    45  	// fast tests on local resources (e.g. remote state with a local
    46  	// backend) but can be used to increase confidence in correct
    47  	// operation of Terraform without waiting for a full acctest run.
    48  	OverrideEnvVar bool
    49  
    50  	// PreCheck, if non-nil, will be called before any test steps are
    51  	// executed. It will only be executed in the case that the steps
    52  	// would run, so it can be used for some validation before running
    53  	// acceptance tests, such as verifying that keys are setup.
    54  	PreCheck func()
    55  
    56  	// Providers is the ResourceProvider that will be under test.
    57  	//
    58  	// Alternately, ProviderFactories can be specified for the providers
    59  	// that are valid. This takes priority over Providers.
    60  	//
    61  	// The end effect of each is the same: specifying the providers that
    62  	// are used within the tests.
    63  	Providers         map[string]terraform.ResourceProvider
    64  	ProviderFactories map[string]terraform.ResourceProviderFactory
    65  
    66  	// PreventPostDestroyRefresh can be set to true for cases where data sources
    67  	// are tested alongside real resources
    68  	PreventPostDestroyRefresh bool
    69  
    70  	// CheckDestroy is called after the resource is finally destroyed
    71  	// to allow the tester to test that the resource is truly gone.
    72  	CheckDestroy TestCheckFunc
    73  
    74  	// Steps are the apply sequences done within the context of the
    75  	// same state. Each step can have its own check to verify correctness.
    76  	Steps []TestStep
    77  
    78  	// The settings below control the "ID-only refresh test." This is
    79  	// an enabled-by-default test that tests that a refresh can be
    80  	// refreshed with only an ID to result in the same attributes.
    81  	// This validates completeness of Refresh.
    82  	//
    83  	// IDRefreshName is the name of the resource to check. This will
    84  	// default to the first non-nil primary resource in the state.
    85  	//
    86  	// IDRefreshIgnore is a list of configuration keys that will be ignored.
    87  	IDRefreshName   string
    88  	IDRefreshIgnore []string
    89  }
    90  
    91  // TestStep is a single apply sequence of a test, done within the
    92  // context of a state.
    93  //
    94  // Multiple TestSteps can be sequenced in a Test to allow testing
    95  // potentially complex update logic. In general, simply create/destroy
    96  // tests will only need one step.
    97  type TestStep struct {
    98  	// ResourceName should be set to the name of the resource
    99  	// that is being tested. Example: "aws_instance.foo". Various test
   100  	// modes use this to auto-detect state information.
   101  	//
   102  	// This is only required if the test mode settings below say it is
   103  	// for the mode you're using.
   104  	ResourceName string
   105  
   106  	// PreConfig is called before the Config is applied to perform any per-step
   107  	// setup that needs to happen. This is called regardless of "test mode"
   108  	// below.
   109  	PreConfig func()
   110  
   111  	//---------------------------------------------------------------
   112  	// Test modes. One of the following groups of settings must be
   113  	// set to determine what the test step will do. Ideally we would've
   114  	// used Go interfaces here but there are now hundreds of tests we don't
   115  	// want to re-type so instead we just determine which step logic
   116  	// to run based on what settings below are set.
   117  	//---------------------------------------------------------------
   118  
   119  	//---------------------------------------------------------------
   120  	// Plan, Apply testing
   121  	//---------------------------------------------------------------
   122  
   123  	// Config a string of the configuration to give to Terraform. If this
   124  	// is set, then the TestCase will execute this step with the same logic
   125  	// as a `terraform apply`.
   126  	Config string
   127  
   128  	// Check is called after the Config is applied. Use this step to
   129  	// make your own API calls to check the status of things, and to
   130  	// inspect the format of the ResourceState itself.
   131  	//
   132  	// If an error is returned, the test will fail. In this case, a
   133  	// destroy plan will still be attempted.
   134  	//
   135  	// If this is nil, no check is done on this step.
   136  	Check TestCheckFunc
   137  
   138  	// Destroy will create a destroy plan if set to true.
   139  	Destroy bool
   140  
   141  	// ExpectNonEmptyPlan can be set to true for specific types of tests that are
   142  	// looking to verify that a diff occurs
   143  	ExpectNonEmptyPlan bool
   144  
   145  	// PreventPostDestroyRefresh can be set to true for cases where data sources
   146  	// are tested alongside real resources
   147  	PreventPostDestroyRefresh bool
   148  
   149  	//---------------------------------------------------------------
   150  	// ImportState testing
   151  	//---------------------------------------------------------------
   152  
   153  	// ImportState, if true, will test the functionality of ImportState
   154  	// by importing the resource with ResourceName (must be set) and the
   155  	// ID of that resource.
   156  	ImportState bool
   157  
   158  	// ImportStateId is the ID to perform an ImportState operation with.
   159  	// This is optional. If it isn't set, then the resource ID is automatically
   160  	// determined by inspecting the state for ResourceName's ID.
   161  	ImportStateId string
   162  
   163  	// ImportStateCheck checks the results of ImportState. It should be
   164  	// used to verify that the resulting value of ImportState has the
   165  	// proper resources, IDs, and attributes.
   166  	ImportStateCheck ImportStateCheckFunc
   167  
   168  	// ImportStateVerify, if true, will also check that the state values
   169  	// that are finally put into the state after import match for all the
   170  	// IDs returned by the Import.
   171  	//
   172  	// ImportStateVerifyIgnore are fields that should not be verified to
   173  	// be equal. These can be set to ephemeral fields or fields that can't
   174  	// be refreshed and don't matter.
   175  	ImportStateVerify       bool
   176  	ImportStateVerifyIgnore []string
   177  }
   178  
   179  // Test performs an acceptance test on a resource.
   180  //
   181  // Tests are not run unless an environmental variable "TF_ACC" is
   182  // set to some non-empty value. This is to avoid test cases surprising
   183  // a user by creating real resources.
   184  //
   185  // Tests will fail unless the verbose flag (`go test -v`, or explicitly
   186  // the "-test.v" flag) is set. Because some acceptance tests take quite
   187  // long, we require the verbose flag so users are able to see progress
   188  // output.
   189  func Test(t TestT, c TestCase) {
   190  	// We only run acceptance tests if an env var is set because they're
   191  	// slow and generally require some outside configuration. You can opt out
   192  	// of this with OverrideEnvVar on individual TestCases.
   193  	if os.Getenv(TestEnvVar) == "" && !c.OverrideEnvVar {
   194  		t.Skip(fmt.Sprintf(
   195  			"Acceptance tests skipped unless env '%s' set",
   196  			TestEnvVar))
   197  		return
   198  	}
   199  
   200  	isUnitTest := (os.Getenv(TestEnvVar) == UnitTestOverride)
   201  
   202  	logWriter, err := logging.LogOutput()
   203  	if err != nil {
   204  		t.Error(fmt.Errorf("error setting up logging: %s", err))
   205  	}
   206  	log.SetOutput(logWriter)
   207  
   208  	// We require verbose mode so that the user knows what is going on.
   209  	if !testTesting && !testing.Verbose() && !isUnitTest && !c.OverrideEnvVar {
   210  		t.Fatal("Acceptance tests must be run with the -v flag on tests")
   211  		return
   212  	}
   213  
   214  	// Run the PreCheck if we have it
   215  	if c.PreCheck != nil {
   216  		c.PreCheck()
   217  	}
   218  
   219  	// Build our context options that we can
   220  	ctxProviders := c.ProviderFactories
   221  	if ctxProviders == nil {
   222  		ctxProviders = make(map[string]terraform.ResourceProviderFactory)
   223  		for k, p := range c.Providers {
   224  			ctxProviders[k] = terraform.ResourceProviderFactoryFixed(p)
   225  		}
   226  	}
   227  	opts := terraform.ContextOpts{Providers: ctxProviders}
   228  
   229  	// A single state variable to track the lifecycle, starting with no state
   230  	var state *terraform.State
   231  
   232  	// Go through each step and run it
   233  	var idRefreshCheck *terraform.ResourceState
   234  	idRefresh := c.IDRefreshName != ""
   235  	errored := false
   236  	for i, step := range c.Steps {
   237  		var err error
   238  		log.Printf("[WARN] Test: Executing step %d", i)
   239  
   240  		// Determine the test mode to execute
   241  		if step.Config != "" {
   242  			state, err = testStepConfig(opts, state, step)
   243  		} else if step.ImportState {
   244  			state, err = testStepImportState(opts, state, step)
   245  		} else {
   246  			err = fmt.Errorf(
   247  				"unknown test mode for step. Please see TestStep docs\n\n%#v",
   248  				step)
   249  		}
   250  
   251  		// If there was an error, exit
   252  		if err != nil {
   253  			errored = true
   254  			t.Error(fmt.Sprintf(
   255  				"Step %d error: %s", i, err))
   256  			break
   257  		}
   258  
   259  		// If we've never checked an id-only refresh and our state isn't
   260  		// empty, find the first resource and test it.
   261  		if idRefresh && idRefreshCheck == nil && !state.Empty() {
   262  			// Find the first non-nil resource in the state
   263  			for _, m := range state.Modules {
   264  				if len(m.Resources) > 0 {
   265  					if v, ok := m.Resources[c.IDRefreshName]; ok {
   266  						idRefreshCheck = v
   267  					}
   268  
   269  					break
   270  				}
   271  			}
   272  
   273  			// If we have an instance to check for refreshes, do it
   274  			// immediately. We do it in the middle of another test
   275  			// because it shouldn't affect the overall state (refresh
   276  			// is read-only semantically) and we want to fail early if
   277  			// this fails. If refresh isn't read-only, then this will have
   278  			// caught a different bug.
   279  			if idRefreshCheck != nil {
   280  				log.Printf(
   281  					"[WARN] Test: Running ID-only refresh check on %s",
   282  					idRefreshCheck.Primary.ID)
   283  				if err := testIDOnlyRefresh(c, opts, step, idRefreshCheck); err != nil {
   284  					log.Printf("[ERROR] Test: ID-only test failed: %s", err)
   285  					t.Error(fmt.Sprintf(
   286  						"[ERROR] Test: ID-only test failed: %s", err))
   287  					break
   288  				}
   289  			}
   290  		}
   291  	}
   292  
   293  	// If we never checked an id-only refresh, it is a failure.
   294  	if idRefresh {
   295  		if !errored && len(c.Steps) > 0 && idRefreshCheck == nil {
   296  			t.Error("ID-only refresh check never ran.")
   297  		}
   298  	}
   299  
   300  	// If we have a state, then run the destroy
   301  	if state != nil {
   302  		lastStep := c.Steps[len(c.Steps)-1]
   303  		destroyStep := TestStep{
   304  			Config:                    lastStep.Config,
   305  			Check:                     c.CheckDestroy,
   306  			Destroy:                   true,
   307  			PreventPostDestroyRefresh: c.PreventPostDestroyRefresh,
   308  		}
   309  
   310  		log.Printf("[WARN] Test: Executing destroy step")
   311  		state, err := testStep(opts, state, destroyStep)
   312  		if err != nil {
   313  			t.Error(fmt.Sprintf(
   314  				"Error destroying resource! WARNING: Dangling resources\n"+
   315  					"may exist. The full state and error is shown below.\n\n"+
   316  					"Error: %s\n\nState: %s",
   317  				err,
   318  				state))
   319  		}
   320  	} else {
   321  		log.Printf("[WARN] Skipping destroy test since there is no state.")
   322  	}
   323  }
   324  
   325  // UnitTest is a helper to force the acceptance testing harness to run in the
   326  // normal unit test suite. This should only be used for resource that don't
   327  // have any external dependencies.
   328  func UnitTest(t TestT, c TestCase) {
   329  	oldEnv := os.Getenv(TestEnvVar)
   330  	if err := os.Setenv(TestEnvVar, UnitTestOverride); err != nil {
   331  		t.Fatal(err)
   332  	}
   333  	defer func() {
   334  		if err := os.Setenv(TestEnvVar, oldEnv); err != nil {
   335  			t.Fatal(err)
   336  		}
   337  	}()
   338  	Test(t, c)
   339  }
   340  
   341  func testIDOnlyRefresh(c TestCase, opts terraform.ContextOpts, step TestStep, r *terraform.ResourceState) error {
   342  	// TODO: We guard by this right now so master doesn't explode. We
   343  	// need to remove this eventually to make this part of the normal tests.
   344  	if os.Getenv("TF_ACC_IDONLY") == "" {
   345  		return nil
   346  	}
   347  
   348  	name := fmt.Sprintf("%s.foo", r.Type)
   349  
   350  	// Build the state. The state is just the resource with an ID. There
   351  	// are no attributes. We only set what is needed to perform a refresh.
   352  	state := terraform.NewState()
   353  	state.RootModule().Resources[name] = &terraform.ResourceState{
   354  		Type: r.Type,
   355  		Primary: &terraform.InstanceState{
   356  			ID: r.Primary.ID,
   357  		},
   358  	}
   359  
   360  	// Create the config module. We use the full config because Refresh
   361  	// doesn't have access to it and we may need things like provider
   362  	// configurations. The initial implementation of id-only checks used
   363  	// an empty config module, but that caused the aforementioned problems.
   364  	mod, err := testModule(opts, step)
   365  	if err != nil {
   366  		return err
   367  	}
   368  
   369  	// Initialize the context
   370  	opts.Module = mod
   371  	opts.State = state
   372  	ctx, err := terraform.NewContext(&opts)
   373  	if err != nil {
   374  		return err
   375  	}
   376  	if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 {
   377  		if len(es) > 0 {
   378  			estrs := make([]string, len(es))
   379  			for i, e := range es {
   380  				estrs[i] = e.Error()
   381  			}
   382  			return fmt.Errorf(
   383  				"Configuration is invalid.\n\nWarnings: %#v\n\nErrors: %#v",
   384  				ws, estrs)
   385  		}
   386  
   387  		log.Printf("[WARN] Config warnings: %#v", ws)
   388  	}
   389  
   390  	// Refresh!
   391  	state, err = ctx.Refresh()
   392  	if err != nil {
   393  		return fmt.Errorf("Error refreshing: %s", err)
   394  	}
   395  
   396  	// Verify attribute equivalence.
   397  	actualR := state.RootModule().Resources[name]
   398  	if actualR == nil {
   399  		return fmt.Errorf("Resource gone!")
   400  	}
   401  	if actualR.Primary == nil {
   402  		return fmt.Errorf("Resource has no primary instance")
   403  	}
   404  	actual := actualR.Primary.Attributes
   405  	expected := r.Primary.Attributes
   406  	// Remove fields we're ignoring
   407  	for _, v := range c.IDRefreshIgnore {
   408  		for k, _ := range actual {
   409  			if strings.HasPrefix(k, v) {
   410  				delete(actual, k)
   411  			}
   412  		}
   413  		for k, _ := range expected {
   414  			if strings.HasPrefix(k, v) {
   415  				delete(expected, k)
   416  			}
   417  		}
   418  	}
   419  
   420  	if !reflect.DeepEqual(actual, expected) {
   421  		// Determine only the different attributes
   422  		for k, v := range expected {
   423  			if av, ok := actual[k]; ok && v == av {
   424  				delete(expected, k)
   425  				delete(actual, k)
   426  			}
   427  		}
   428  
   429  		spewConf := spew.NewDefaultConfig()
   430  		spewConf.SortKeys = true
   431  		return fmt.Errorf(
   432  			"Attributes not equivalent. Difference is shown below. Top is actual, bottom is expected."+
   433  				"\n\n%s\n\n%s",
   434  			spewConf.Sdump(actual), spewConf.Sdump(expected))
   435  	}
   436  
   437  	return nil
   438  }
   439  
   440  func testModule(
   441  	opts terraform.ContextOpts,
   442  	step TestStep) (*module.Tree, error) {
   443  	if step.PreConfig != nil {
   444  		step.PreConfig()
   445  	}
   446  
   447  	cfgPath, err := ioutil.TempDir("", "tf-test")
   448  	if err != nil {
   449  		return nil, fmt.Errorf(
   450  			"Error creating temporary directory for config: %s", err)
   451  	}
   452  	defer os.RemoveAll(cfgPath)
   453  
   454  	// Write the configuration
   455  	cfgF, err := os.Create(filepath.Join(cfgPath, "main.tf"))
   456  	if err != nil {
   457  		return nil, fmt.Errorf(
   458  			"Error creating temporary file for config: %s", err)
   459  	}
   460  
   461  	_, err = io.Copy(cfgF, strings.NewReader(step.Config))
   462  	cfgF.Close()
   463  	if err != nil {
   464  		return nil, fmt.Errorf(
   465  			"Error creating temporary file for config: %s", err)
   466  	}
   467  
   468  	// Parse the configuration
   469  	mod, err := module.NewTreeModule("", cfgPath)
   470  	if err != nil {
   471  		return nil, fmt.Errorf(
   472  			"Error loading configuration: %s", err)
   473  	}
   474  
   475  	// Load the modules
   476  	modStorage := &getter.FolderStorage{
   477  		StorageDir: filepath.Join(cfgPath, ".tfmodules"),
   478  	}
   479  	err = mod.Load(modStorage, module.GetModeGet)
   480  	if err != nil {
   481  		return nil, fmt.Errorf("Error downloading modules: %s", err)
   482  	}
   483  
   484  	return mod, nil
   485  }
   486  
   487  func testResource(c TestStep, state *terraform.State) (*terraform.ResourceState, error) {
   488  	if c.ResourceName == "" {
   489  		return nil, fmt.Errorf("ResourceName must be set in TestStep")
   490  	}
   491  
   492  	for _, m := range state.Modules {
   493  		if len(m.Resources) > 0 {
   494  			if v, ok := m.Resources[c.ResourceName]; ok {
   495  				return v, nil
   496  			}
   497  		}
   498  	}
   499  
   500  	return nil, fmt.Errorf(
   501  		"Resource specified by ResourceName couldn't be found: %s", c.ResourceName)
   502  }
   503  
   504  // ComposeTestCheckFunc lets you compose multiple TestCheckFuncs into
   505  // a single TestCheckFunc.
   506  //
   507  // As a user testing their provider, this lets you decompose your checks
   508  // into smaller pieces more easily.
   509  func ComposeTestCheckFunc(fs ...TestCheckFunc) TestCheckFunc {
   510  	return func(s *terraform.State) error {
   511  		for i, f := range fs {
   512  			if err := f(s); err != nil {
   513  				return fmt.Errorf("Check %d/%d error: %s", i+1, len(fs), err)
   514  			}
   515  		}
   516  
   517  		return nil
   518  	}
   519  }
   520  
   521  func TestCheckResourceAttr(name, key, value string) TestCheckFunc {
   522  	return func(s *terraform.State) error {
   523  		ms := s.RootModule()
   524  		rs, ok := ms.Resources[name]
   525  		if !ok {
   526  			return fmt.Errorf("Not found: %s", name)
   527  		}
   528  
   529  		is := rs.Primary
   530  		if is == nil {
   531  			return fmt.Errorf("No primary instance: %s", name)
   532  		}
   533  
   534  		if is.Attributes[key] != value {
   535  			return fmt.Errorf(
   536  				"%s: Attribute '%s' expected %#v, got %#v",
   537  				name,
   538  				key,
   539  				value,
   540  				is.Attributes[key])
   541  		}
   542  
   543  		return nil
   544  	}
   545  }
   546  
   547  func TestMatchResourceAttr(name, key string, r *regexp.Regexp) TestCheckFunc {
   548  	return func(s *terraform.State) error {
   549  		ms := s.RootModule()
   550  		rs, ok := ms.Resources[name]
   551  		if !ok {
   552  			return fmt.Errorf("Not found: %s", name)
   553  		}
   554  
   555  		is := rs.Primary
   556  		if is == nil {
   557  			return fmt.Errorf("No primary instance: %s", name)
   558  		}
   559  
   560  		if !r.MatchString(is.Attributes[key]) {
   561  			return fmt.Errorf(
   562  				"%s: Attribute '%s' didn't match %q, got %#v",
   563  				name,
   564  				key,
   565  				r.String(),
   566  				is.Attributes[key])
   567  		}
   568  
   569  		return nil
   570  	}
   571  }
   572  
   573  // TestCheckResourceAttrPtr is like TestCheckResourceAttr except the
   574  // value is a pointer so that it can be updated while the test is running.
   575  // It will only be dereferenced at the point this step is run.
   576  func TestCheckResourceAttrPtr(name string, key string, value *string) TestCheckFunc {
   577  	return func(s *terraform.State) error {
   578  		return TestCheckResourceAttr(name, key, *value)(s)
   579  	}
   580  }
   581  
   582  // TestCheckOutput checks an output in the Terraform configuration
   583  func TestCheckOutput(name, value string) TestCheckFunc {
   584  	return func(s *terraform.State) error {
   585  		ms := s.RootModule()
   586  		rs, ok := ms.Outputs[name]
   587  		if !ok {
   588  			return fmt.Errorf("Not found: %s", name)
   589  		}
   590  
   591  		if rs.Value != value {
   592  			return fmt.Errorf(
   593  				"Output '%s': expected %#v, got %#v",
   594  				name,
   595  				value,
   596  				rs)
   597  		}
   598  
   599  		return nil
   600  	}
   601  }
   602  
   603  func TestMatchOutput(name string, r *regexp.Regexp) TestCheckFunc {
   604  	return func(s *terraform.State) error {
   605  		ms := s.RootModule()
   606  		rs, ok := ms.Outputs[name]
   607  		if !ok {
   608  			return fmt.Errorf("Not found: %s", name)
   609  		}
   610  
   611  		if !r.MatchString(rs.Value.(string)) {
   612  			return fmt.Errorf(
   613  				"Output '%s': %#v didn't match %q",
   614  				name,
   615  				rs,
   616  				r.String())
   617  		}
   618  
   619  		return nil
   620  	}
   621  }
   622  
   623  // TestT is the interface used to handle the test lifecycle of a test.
   624  //
   625  // Users should just use a *testing.T object, which implements this.
   626  type TestT interface {
   627  	Error(args ...interface{})
   628  	Fatal(args ...interface{})
   629  	Skip(args ...interface{})
   630  }
   631  
   632  // This is set to true by unit tests to alter some behavior
   633  var testTesting = false