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