github.com/ezbercih/terraform@v0.1.1-0.20140729011846-3c33865e0839/helper/resource/testing.go (about)

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