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