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