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