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