github.com/i0n/terraform@v0.4.3-0.20150506151324-010a39a58ec1/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 // TransientResource indicates that resources created as part 80 // of this test step are temporary and might be recreated anew 81 // with every planning step. This should only be set for 82 // pseudo-resources, like the null resource or templates. 83 TransientResource 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 cfgPath, err := ioutil.TempDir("", "tf-test") 170 if err != nil { 171 return state, fmt.Errorf( 172 "Error creating temporary directory for config: %s", err) 173 } 174 defer os.RemoveAll(cfgPath) 175 176 // Write the configuration 177 cfgF, err := os.Create(filepath.Join(cfgPath, "main.tf")) 178 if err != nil { 179 return state, fmt.Errorf( 180 "Error creating temporary file for config: %s", err) 181 } 182 183 _, err = io.Copy(cfgF, strings.NewReader(step.Config)) 184 cfgF.Close() 185 if err != nil { 186 return state, fmt.Errorf( 187 "Error creating temporary file for config: %s", err) 188 } 189 190 // Parse the configuration 191 mod, err := module.NewTreeModule("", cfgPath) 192 if err != nil { 193 return state, fmt.Errorf( 194 "Error loading configuration: %s", err) 195 } 196 197 // Load the modules 198 modStorage := &module.FolderStorage{ 199 StorageDir: filepath.Join(cfgPath, ".tfmodules"), 200 } 201 err = mod.Load(modStorage, module.GetModeGet) 202 if err != nil { 203 return state, fmt.Errorf("Error downloading modules: %s", err) 204 } 205 206 // Build the context 207 opts.Module = mod 208 opts.State = state 209 opts.Destroy = step.Destroy 210 ctx := terraform.NewContext(&opts) 211 if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 { 212 estrs := make([]string, len(es)) 213 for i, e := range es { 214 estrs[i] = e.Error() 215 } 216 return state, fmt.Errorf( 217 "Configuration is invalid.\n\nWarnings: %#v\n\nErrors: %#v", 218 ws, estrs) 219 } 220 221 // Refresh! 222 state, err = ctx.Refresh() 223 if err != nil { 224 return state, fmt.Errorf( 225 "Error refreshing: %s", err) 226 } 227 228 // Plan! 229 if p, err := ctx.Plan(); err != nil { 230 return state, fmt.Errorf( 231 "Error planning: %s", err) 232 } else { 233 log.Printf("[WARN] Test: Step plan: %s", p) 234 } 235 236 // Apply! 237 state, err = ctx.Apply() 238 if err != nil { 239 return state, fmt.Errorf("Error applying: %s", err) 240 } 241 242 // Check! Excitement! 243 if step.Check != nil { 244 if err := step.Check(state); err != nil { 245 return state, fmt.Errorf("Check failed: %s", err) 246 } 247 } 248 249 // Now, verify that Plan is now empty and we don't have a perpetual diff issue 250 // We do this with TWO plans. One without a refresh. 251 if p, err := ctx.Plan(); err != nil { 252 return state, fmt.Errorf("Error on follow-up plan: %s", err) 253 } else { 254 if p.Diff != nil && !p.Diff.Empty() { 255 return state, fmt.Errorf( 256 "After applying this step, the plan was not empty:\n\n%s", p) 257 } 258 } 259 260 // And another after a Refresh. 261 state, err = ctx.Refresh() 262 if err != nil { 263 return state, fmt.Errorf( 264 "Error on follow-up refresh: %s", err) 265 } 266 if p, err := ctx.Plan(); err != nil { 267 return state, fmt.Errorf("Error on second follow-up plan: %s", err) 268 } else { 269 if p.Diff != nil && !p.Diff.Empty() && !step.TransientResource { 270 return state, fmt.Errorf( 271 "After applying this step and refreshing, the plan was not empty:\n\n%s", p) 272 } 273 } 274 275 // Made it here? Good job test step! 276 return state, nil 277 } 278 279 // ComposeTestCheckFunc lets you compose multiple TestCheckFuncs into 280 // a single TestCheckFunc. 281 // 282 // As a user testing their provider, this lets you decompose your checks 283 // into smaller pieces more easily. 284 func ComposeTestCheckFunc(fs ...TestCheckFunc) TestCheckFunc { 285 return func(s *terraform.State) error { 286 for _, f := range fs { 287 if err := f(s); err != nil { 288 return err 289 } 290 } 291 292 return nil 293 } 294 } 295 296 func TestCheckResourceAttr(name, key, value string) TestCheckFunc { 297 return func(s *terraform.State) error { 298 ms := s.RootModule() 299 rs, ok := ms.Resources[name] 300 if !ok { 301 return fmt.Errorf("Not found: %s", name) 302 } 303 304 is := rs.Primary 305 if is == nil { 306 return fmt.Errorf("No primary instance: %s", name) 307 } 308 309 if is.Attributes[key] != value { 310 return fmt.Errorf( 311 "%s: Attribute '%s' expected %#v, got %#v", 312 name, 313 key, 314 value, 315 is.Attributes[key]) 316 } 317 318 return nil 319 } 320 } 321 322 // TestCheckResourceAttrPtr is like TestCheckResourceAttr except the 323 // value is a pointer so that it can be updated while the test is running. 324 // It will only be dereferenced at the point this step is run. 325 func TestCheckResourceAttrPtr(name string, key string, value *string) TestCheckFunc { 326 return func(s *terraform.State) error { 327 return TestCheckResourceAttr(name, key, *value)(s) 328 } 329 } 330 331 // TestT is the interface used to handle the test lifecycle of a test. 332 // 333 // Users should just use a *testing.T object, which implements this. 334 type TestT interface { 335 Error(args ...interface{}) 336 Fatal(args ...interface{}) 337 Skip(args ...interface{}) 338 } 339 340 // This is set to true by unit tests to alter some behavior 341 var testTesting = false