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