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