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