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