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