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