github.com/danrjohnson/terraform@v0.7.0-rc2.0.20160627135212-d0fc1fa086ff/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 "reflect" 11 "regexp" 12 "strings" 13 "testing" 14 15 "github.com/davecgh/go-spew/spew" 16 "github.com/hashicorp/go-getter" 17 "github.com/hashicorp/terraform/config/module" 18 "github.com/hashicorp/terraform/helper/logging" 19 "github.com/hashicorp/terraform/terraform" 20 ) 21 22 const TestEnvVar = "TF_ACC" 23 24 // UnitTestOverride is a value that when set in TestEnvVar indicates that this 25 // is a unit test borrowing the acceptance testing framework. 26 const UnitTestOverride = "UnitTestOverride" 27 28 // TestCheckFunc is the callback type used with acceptance tests to check 29 // the state of a resource. The state passed in is the latest state known, 30 // or in the case of being after a destroy, it is the last known state when 31 // it was created. 32 type TestCheckFunc func(*terraform.State) error 33 34 // ImportStateCheckFunc is the check function for ImportState tests 35 type ImportStateCheckFunc func([]*terraform.InstanceState) error 36 37 // TestCase is a single acceptance test case used to test the apply/destroy 38 // lifecycle of a resource in a specific configuration. 39 // 40 // When the destroy plan is executed, the config from the last TestStep 41 // is used to plan it. 42 type TestCase struct { 43 // OverrideEnvVar allows a test to run regardless of the TF_ACC 44 // environment variable. This should be used with care - only for 45 // fast tests on local resources (e.g. remote state with a local 46 // backend) but can be used to increase confidence in correct 47 // operation of Terraform without waiting for a full acctest run. 48 OverrideEnvVar bool 49 50 // PreCheck, if non-nil, will be called before any test steps are 51 // executed. It will only be executed in the case that the steps 52 // would run, so it can be used for some validation before running 53 // acceptance tests, such as verifying that keys are setup. 54 PreCheck func() 55 56 // Providers is the ResourceProvider that will be under test. 57 // 58 // Alternately, ProviderFactories can be specified for the providers 59 // that are valid. This takes priority over Providers. 60 // 61 // The end effect of each is the same: specifying the providers that 62 // are used within the tests. 63 Providers map[string]terraform.ResourceProvider 64 ProviderFactories map[string]terraform.ResourceProviderFactory 65 66 // PreventPostDestroyRefresh can be set to true for cases where data sources 67 // are tested alongside real resources 68 PreventPostDestroyRefresh bool 69 70 // CheckDestroy is called after the resource is finally destroyed 71 // to allow the tester to test that the resource is truly gone. 72 CheckDestroy TestCheckFunc 73 74 // Steps are the apply sequences done within the context of the 75 // same state. Each step can have its own check to verify correctness. 76 Steps []TestStep 77 78 // The settings below control the "ID-only refresh test." This is 79 // an enabled-by-default test that tests that a refresh can be 80 // refreshed with only an ID to result in the same attributes. 81 // This validates completeness of Refresh. 82 // 83 // IDRefreshName is the name of the resource to check. This will 84 // default to the first non-nil primary resource in the state. 85 // 86 // IDRefreshIgnore is a list of configuration keys that will be ignored. 87 IDRefreshName string 88 IDRefreshIgnore []string 89 } 90 91 // TestStep is a single apply sequence of a test, done within the 92 // context of a state. 93 // 94 // Multiple TestSteps can be sequenced in a Test to allow testing 95 // potentially complex update logic. In general, simply create/destroy 96 // tests will only need one step. 97 type TestStep struct { 98 // ResourceName should be set to the name of the resource 99 // that is being tested. Example: "aws_instance.foo". Various test 100 // modes use this to auto-detect state information. 101 // 102 // This is only required if the test mode settings below say it is 103 // for the mode you're using. 104 ResourceName string 105 106 // PreConfig is called before the Config is applied to perform any per-step 107 // setup that needs to happen. This is called regardless of "test mode" 108 // below. 109 PreConfig func() 110 111 //--------------------------------------------------------------- 112 // Test modes. One of the following groups of settings must be 113 // set to determine what the test step will do. Ideally we would've 114 // used Go interfaces here but there are now hundreds of tests we don't 115 // want to re-type so instead we just determine which step logic 116 // to run based on what settings below are set. 117 //--------------------------------------------------------------- 118 119 //--------------------------------------------------------------- 120 // Plan, Apply testing 121 //--------------------------------------------------------------- 122 123 // Config a string of the configuration to give to Terraform. If this 124 // is set, then the TestCase will execute this step with the same logic 125 // as a `terraform apply`. 126 Config string 127 128 // Check is called after the Config is applied. Use this step to 129 // make your own API calls to check the status of things, and to 130 // inspect the format of the ResourceState itself. 131 // 132 // If an error is returned, the test will fail. In this case, a 133 // destroy plan will still be attempted. 134 // 135 // If this is nil, no check is done on this step. 136 Check TestCheckFunc 137 138 // Destroy will create a destroy plan if set to true. 139 Destroy bool 140 141 // ExpectNonEmptyPlan can be set to true for specific types of tests that are 142 // looking to verify that a diff occurs 143 ExpectNonEmptyPlan bool 144 145 // PreventPostDestroyRefresh can be set to true for cases where data sources 146 // are tested alongside real resources 147 PreventPostDestroyRefresh bool 148 149 //--------------------------------------------------------------- 150 // ImportState testing 151 //--------------------------------------------------------------- 152 153 // ImportState, if true, will test the functionality of ImportState 154 // by importing the resource with ResourceName (must be set) and the 155 // ID of that resource. 156 ImportState bool 157 158 // ImportStateId is the ID to perform an ImportState operation with. 159 // This is optional. If it isn't set, then the resource ID is automatically 160 // determined by inspecting the state for ResourceName's ID. 161 ImportStateId string 162 163 // ImportStateCheck checks the results of ImportState. It should be 164 // used to verify that the resulting value of ImportState has the 165 // proper resources, IDs, and attributes. 166 ImportStateCheck ImportStateCheckFunc 167 168 // ImportStateVerify, if true, will also check that the state values 169 // that are finally put into the state after import match for all the 170 // IDs returned by the Import. 171 // 172 // ImportStateVerifyIgnore are fields that should not be verified to 173 // be equal. These can be set to ephemeral fields or fields that can't 174 // be refreshed and don't matter. 175 ImportStateVerify bool 176 ImportStateVerifyIgnore []string 177 } 178 179 // Test performs an acceptance test on a resource. 180 // 181 // Tests are not run unless an environmental variable "TF_ACC" is 182 // set to some non-empty value. This is to avoid test cases surprising 183 // a user by creating real resources. 184 // 185 // Tests will fail unless the verbose flag (`go test -v`, or explicitly 186 // the "-test.v" flag) is set. Because some acceptance tests take quite 187 // long, we require the verbose flag so users are able to see progress 188 // output. 189 func Test(t TestT, c TestCase) { 190 // We only run acceptance tests if an env var is set because they're 191 // slow and generally require some outside configuration. You can opt out 192 // of this with OverrideEnvVar on individual TestCases. 193 if os.Getenv(TestEnvVar) == "" && !c.OverrideEnvVar { 194 t.Skip(fmt.Sprintf( 195 "Acceptance tests skipped unless env '%s' set", 196 TestEnvVar)) 197 return 198 } 199 200 isUnitTest := (os.Getenv(TestEnvVar) == UnitTestOverride) 201 202 logWriter, err := logging.LogOutput() 203 if err != nil { 204 t.Error(fmt.Errorf("error setting up logging: %s", err)) 205 } 206 log.SetOutput(logWriter) 207 208 // We require verbose mode so that the user knows what is going on. 209 if !testTesting && !testing.Verbose() && !isUnitTest && !c.OverrideEnvVar { 210 t.Fatal("Acceptance tests must be run with the -v flag on tests") 211 return 212 } 213 214 // Run the PreCheck if we have it 215 if c.PreCheck != nil { 216 c.PreCheck() 217 } 218 219 // Build our context options that we can 220 ctxProviders := c.ProviderFactories 221 if ctxProviders == nil { 222 ctxProviders = make(map[string]terraform.ResourceProviderFactory) 223 for k, p := range c.Providers { 224 ctxProviders[k] = terraform.ResourceProviderFactoryFixed(p) 225 } 226 } 227 opts := terraform.ContextOpts{Providers: ctxProviders} 228 229 // A single state variable to track the lifecycle, starting with no state 230 var state *terraform.State 231 232 // Go through each step and run it 233 var idRefreshCheck *terraform.ResourceState 234 idRefresh := c.IDRefreshName != "" 235 errored := false 236 for i, step := range c.Steps { 237 var err error 238 log.Printf("[WARN] Test: Executing step %d", i) 239 240 // Determine the test mode to execute 241 if step.Config != "" { 242 state, err = testStepConfig(opts, state, step) 243 } else if step.ImportState { 244 state, err = testStepImportState(opts, state, step) 245 } else { 246 err = fmt.Errorf( 247 "unknown test mode for step. Please see TestStep docs\n\n%#v", 248 step) 249 } 250 251 // If there was an error, exit 252 if err != nil { 253 errored = true 254 t.Error(fmt.Sprintf( 255 "Step %d error: %s", i, err)) 256 break 257 } 258 259 // If we've never checked an id-only refresh and our state isn't 260 // empty, find the first resource and test it. 261 if idRefresh && idRefreshCheck == nil && !state.Empty() { 262 // Find the first non-nil resource in the state 263 for _, m := range state.Modules { 264 if len(m.Resources) > 0 { 265 if v, ok := m.Resources[c.IDRefreshName]; ok { 266 idRefreshCheck = v 267 } 268 269 break 270 } 271 } 272 273 // If we have an instance to check for refreshes, do it 274 // immediately. We do it in the middle of another test 275 // because it shouldn't affect the overall state (refresh 276 // is read-only semantically) and we want to fail early if 277 // this fails. If refresh isn't read-only, then this will have 278 // caught a different bug. 279 if idRefreshCheck != nil { 280 log.Printf( 281 "[WARN] Test: Running ID-only refresh check on %s", 282 idRefreshCheck.Primary.ID) 283 if err := testIDOnlyRefresh(c, opts, step, idRefreshCheck); err != nil { 284 log.Printf("[ERROR] Test: ID-only test failed: %s", err) 285 t.Error(fmt.Sprintf( 286 "[ERROR] Test: ID-only test failed: %s", err)) 287 break 288 } 289 } 290 } 291 } 292 293 // If we never checked an id-only refresh, it is a failure. 294 if idRefresh { 295 if !errored && len(c.Steps) > 0 && idRefreshCheck == nil { 296 t.Error("ID-only refresh check never ran.") 297 } 298 } 299 300 // If we have a state, then run the destroy 301 if state != nil { 302 lastStep := c.Steps[len(c.Steps)-1] 303 destroyStep := TestStep{ 304 Config: lastStep.Config, 305 Check: c.CheckDestroy, 306 Destroy: true, 307 PreventPostDestroyRefresh: c.PreventPostDestroyRefresh, 308 } 309 310 log.Printf("[WARN] Test: Executing destroy step") 311 state, err := testStep(opts, state, destroyStep) 312 if err != nil { 313 t.Error(fmt.Sprintf( 314 "Error destroying resource! WARNING: Dangling resources\n"+ 315 "may exist. The full state and error is shown below.\n\n"+ 316 "Error: %s\n\nState: %s", 317 err, 318 state)) 319 } 320 } else { 321 log.Printf("[WARN] Skipping destroy test since there is no state.") 322 } 323 } 324 325 // UnitTest is a helper to force the acceptance testing harness to run in the 326 // normal unit test suite. This should only be used for resource that don't 327 // have any external dependencies. 328 func UnitTest(t TestT, c TestCase) { 329 oldEnv := os.Getenv(TestEnvVar) 330 if err := os.Setenv(TestEnvVar, UnitTestOverride); err != nil { 331 t.Fatal(err) 332 } 333 defer func() { 334 if err := os.Setenv(TestEnvVar, oldEnv); err != nil { 335 t.Fatal(err) 336 } 337 }() 338 Test(t, c) 339 } 340 341 func testIDOnlyRefresh(c TestCase, opts terraform.ContextOpts, step TestStep, r *terraform.ResourceState) error { 342 // TODO: We guard by this right now so master doesn't explode. We 343 // need to remove this eventually to make this part of the normal tests. 344 if os.Getenv("TF_ACC_IDONLY") == "" { 345 return nil 346 } 347 348 name := fmt.Sprintf("%s.foo", r.Type) 349 350 // Build the state. The state is just the resource with an ID. There 351 // are no attributes. We only set what is needed to perform a refresh. 352 state := terraform.NewState() 353 state.RootModule().Resources[name] = &terraform.ResourceState{ 354 Type: r.Type, 355 Primary: &terraform.InstanceState{ 356 ID: r.Primary.ID, 357 }, 358 } 359 360 // Create the config module. We use the full config because Refresh 361 // doesn't have access to it and we may need things like provider 362 // configurations. The initial implementation of id-only checks used 363 // an empty config module, but that caused the aforementioned problems. 364 mod, err := testModule(opts, step) 365 if err != nil { 366 return err 367 } 368 369 // Initialize the context 370 opts.Module = mod 371 opts.State = state 372 ctx, err := terraform.NewContext(&opts) 373 if err != nil { 374 return err 375 } 376 if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 { 377 if len(es) > 0 { 378 estrs := make([]string, len(es)) 379 for i, e := range es { 380 estrs[i] = e.Error() 381 } 382 return fmt.Errorf( 383 "Configuration is invalid.\n\nWarnings: %#v\n\nErrors: %#v", 384 ws, estrs) 385 } 386 387 log.Printf("[WARN] Config warnings: %#v", ws) 388 } 389 390 // Refresh! 391 state, err = ctx.Refresh() 392 if err != nil { 393 return fmt.Errorf("Error refreshing: %s", err) 394 } 395 396 // Verify attribute equivalence. 397 actualR := state.RootModule().Resources[name] 398 if actualR == nil { 399 return fmt.Errorf("Resource gone!") 400 } 401 if actualR.Primary == nil { 402 return fmt.Errorf("Resource has no primary instance") 403 } 404 actual := actualR.Primary.Attributes 405 expected := r.Primary.Attributes 406 // Remove fields we're ignoring 407 for _, v := range c.IDRefreshIgnore { 408 for k, _ := range actual { 409 if strings.HasPrefix(k, v) { 410 delete(actual, k) 411 } 412 } 413 for k, _ := range expected { 414 if strings.HasPrefix(k, v) { 415 delete(expected, k) 416 } 417 } 418 } 419 420 if !reflect.DeepEqual(actual, expected) { 421 // Determine only the different attributes 422 for k, v := range expected { 423 if av, ok := actual[k]; ok && v == av { 424 delete(expected, k) 425 delete(actual, k) 426 } 427 } 428 429 spewConf := spew.NewDefaultConfig() 430 spewConf.SortKeys = true 431 return fmt.Errorf( 432 "Attributes not equivalent. Difference is shown below. Top is actual, bottom is expected."+ 433 "\n\n%s\n\n%s", 434 spewConf.Sdump(actual), spewConf.Sdump(expected)) 435 } 436 437 return nil 438 } 439 440 func testModule( 441 opts terraform.ContextOpts, 442 step TestStep) (*module.Tree, error) { 443 if step.PreConfig != nil { 444 step.PreConfig() 445 } 446 447 cfgPath, err := ioutil.TempDir("", "tf-test") 448 if err != nil { 449 return nil, fmt.Errorf( 450 "Error creating temporary directory for config: %s", err) 451 } 452 defer os.RemoveAll(cfgPath) 453 454 // Write the configuration 455 cfgF, err := os.Create(filepath.Join(cfgPath, "main.tf")) 456 if err != nil { 457 return nil, fmt.Errorf( 458 "Error creating temporary file for config: %s", err) 459 } 460 461 _, err = io.Copy(cfgF, strings.NewReader(step.Config)) 462 cfgF.Close() 463 if err != nil { 464 return nil, fmt.Errorf( 465 "Error creating temporary file for config: %s", err) 466 } 467 468 // Parse the configuration 469 mod, err := module.NewTreeModule("", cfgPath) 470 if err != nil { 471 return nil, fmt.Errorf( 472 "Error loading configuration: %s", err) 473 } 474 475 // Load the modules 476 modStorage := &getter.FolderStorage{ 477 StorageDir: filepath.Join(cfgPath, ".tfmodules"), 478 } 479 err = mod.Load(modStorage, module.GetModeGet) 480 if err != nil { 481 return nil, fmt.Errorf("Error downloading modules: %s", err) 482 } 483 484 return mod, nil 485 } 486 487 func testResource(c TestStep, state *terraform.State) (*terraform.ResourceState, error) { 488 if c.ResourceName == "" { 489 return nil, fmt.Errorf("ResourceName must be set in TestStep") 490 } 491 492 for _, m := range state.Modules { 493 if len(m.Resources) > 0 { 494 if v, ok := m.Resources[c.ResourceName]; ok { 495 return v, nil 496 } 497 } 498 } 499 500 return nil, fmt.Errorf( 501 "Resource specified by ResourceName couldn't be found: %s", c.ResourceName) 502 } 503 504 // ComposeTestCheckFunc lets you compose multiple TestCheckFuncs into 505 // a single TestCheckFunc. 506 // 507 // As a user testing their provider, this lets you decompose your checks 508 // into smaller pieces more easily. 509 func ComposeTestCheckFunc(fs ...TestCheckFunc) TestCheckFunc { 510 return func(s *terraform.State) error { 511 for i, f := range fs { 512 if err := f(s); err != nil { 513 return fmt.Errorf("Check %d/%d error: %s", i+1, len(fs), err) 514 } 515 } 516 517 return nil 518 } 519 } 520 521 func TestCheckResourceAttr(name, key, value string) TestCheckFunc { 522 return func(s *terraform.State) error { 523 ms := s.RootModule() 524 rs, ok := ms.Resources[name] 525 if !ok { 526 return fmt.Errorf("Not found: %s", name) 527 } 528 529 is := rs.Primary 530 if is == nil { 531 return fmt.Errorf("No primary instance: %s", name) 532 } 533 534 if is.Attributes[key] != value { 535 return fmt.Errorf( 536 "%s: Attribute '%s' expected %#v, got %#v", 537 name, 538 key, 539 value, 540 is.Attributes[key]) 541 } 542 543 return nil 544 } 545 } 546 547 func TestMatchResourceAttr(name, key string, r *regexp.Regexp) TestCheckFunc { 548 return func(s *terraform.State) error { 549 ms := s.RootModule() 550 rs, ok := ms.Resources[name] 551 if !ok { 552 return fmt.Errorf("Not found: %s", name) 553 } 554 555 is := rs.Primary 556 if is == nil { 557 return fmt.Errorf("No primary instance: %s", name) 558 } 559 560 if !r.MatchString(is.Attributes[key]) { 561 return fmt.Errorf( 562 "%s: Attribute '%s' didn't match %q, got %#v", 563 name, 564 key, 565 r.String(), 566 is.Attributes[key]) 567 } 568 569 return nil 570 } 571 } 572 573 // TestCheckResourceAttrPtr is like TestCheckResourceAttr except the 574 // value is a pointer so that it can be updated while the test is running. 575 // It will only be dereferenced at the point this step is run. 576 func TestCheckResourceAttrPtr(name string, key string, value *string) TestCheckFunc { 577 return func(s *terraform.State) error { 578 return TestCheckResourceAttr(name, key, *value)(s) 579 } 580 } 581 582 // TestCheckOutput checks an output in the Terraform configuration 583 func TestCheckOutput(name, value string) TestCheckFunc { 584 return func(s *terraform.State) error { 585 ms := s.RootModule() 586 rs, ok := ms.Outputs[name] 587 if !ok { 588 return fmt.Errorf("Not found: %s", name) 589 } 590 591 if rs.Value != value { 592 return fmt.Errorf( 593 "Output '%s': expected %#v, got %#v", 594 name, 595 value, 596 rs) 597 } 598 599 return nil 600 } 601 } 602 603 func TestMatchOutput(name string, r *regexp.Regexp) TestCheckFunc { 604 return func(s *terraform.State) error { 605 ms := s.RootModule() 606 rs, ok := ms.Outputs[name] 607 if !ok { 608 return fmt.Errorf("Not found: %s", name) 609 } 610 611 if !r.MatchString(rs.Value.(string)) { 612 return fmt.Errorf( 613 "Output '%s': %#v didn't match %q", 614 name, 615 rs, 616 r.String()) 617 } 618 619 return nil 620 } 621 } 622 623 // TestT is the interface used to handle the test lifecycle of a test. 624 // 625 // Users should just use a *testing.T object, which implements this. 626 type TestT interface { 627 Error(args ...interface{}) 628 Fatal(args ...interface{}) 629 Skip(args ...interface{}) 630 } 631 632 // This is set to true by unit tests to alter some behavior 633 var testTesting = false