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