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