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