github.com/markdia/terraform@v0.5.1-0.20150508012022-f1ae920aa970/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 if len(es) > 0 { 213 estrs := make([]string, len(es)) 214 for i, e := range es { 215 estrs[i] = e.Error() 216 } 217 return state, fmt.Errorf( 218 "Configuration is invalid.\n\nWarnings: %#v\n\nErrors: %#v", 219 ws, estrs) 220 } 221 log.Printf("[WARN] Config warnings: %#v", ws) 222 } 223 224 // Refresh! 225 state, err = ctx.Refresh() 226 if err != nil { 227 return state, fmt.Errorf( 228 "Error refreshing: %s", err) 229 } 230 231 // Plan! 232 if p, err := ctx.Plan(); err != nil { 233 return state, fmt.Errorf( 234 "Error planning: %s", err) 235 } else { 236 log.Printf("[WARN] Test: Step plan: %s", p) 237 } 238 239 // Apply! 240 state, err = ctx.Apply() 241 if err != nil { 242 return state, fmt.Errorf("Error applying: %s", err) 243 } 244 245 // Check! Excitement! 246 if step.Check != nil { 247 if err := step.Check(state); err != nil { 248 return state, fmt.Errorf("Check failed: %s", err) 249 } 250 } 251 252 // Now, verify that Plan is now empty and we don't have a perpetual diff issue 253 // We do this with TWO plans. One without a refresh. 254 if p, err := ctx.Plan(); err != nil { 255 return state, fmt.Errorf("Error on follow-up plan: %s", err) 256 } else { 257 if p.Diff != nil && !p.Diff.Empty() { 258 return state, fmt.Errorf( 259 "After applying this step, the plan was not empty:\n\n%s", p) 260 } 261 } 262 263 // And another after a Refresh. 264 state, err = ctx.Refresh() 265 if err != nil { 266 return state, fmt.Errorf( 267 "Error on follow-up refresh: %s", err) 268 } 269 if p, err := ctx.Plan(); err != nil { 270 return state, fmt.Errorf("Error on second follow-up plan: %s", err) 271 } else { 272 if p.Diff != nil && !p.Diff.Empty() && !step.TransientResource { 273 return state, fmt.Errorf( 274 "After applying this step and refreshing, the plan was not empty:\n\n%s", p) 275 } 276 } 277 278 // Made it here? Good job test step! 279 return state, nil 280 } 281 282 // ComposeTestCheckFunc lets you compose multiple TestCheckFuncs into 283 // a single TestCheckFunc. 284 // 285 // As a user testing their provider, this lets you decompose your checks 286 // into smaller pieces more easily. 287 func ComposeTestCheckFunc(fs ...TestCheckFunc) TestCheckFunc { 288 return func(s *terraform.State) error { 289 for _, f := range fs { 290 if err := f(s); err != nil { 291 return err 292 } 293 } 294 295 return nil 296 } 297 } 298 299 func TestCheckResourceAttr(name, key, value string) TestCheckFunc { 300 return func(s *terraform.State) error { 301 ms := s.RootModule() 302 rs, ok := ms.Resources[name] 303 if !ok { 304 return fmt.Errorf("Not found: %s", name) 305 } 306 307 is := rs.Primary 308 if is == nil { 309 return fmt.Errorf("No primary instance: %s", name) 310 } 311 312 if is.Attributes[key] != value { 313 return fmt.Errorf( 314 "%s: Attribute '%s' expected %#v, got %#v", 315 name, 316 key, 317 value, 318 is.Attributes[key]) 319 } 320 321 return nil 322 } 323 } 324 325 // TestCheckResourceAttrPtr is like TestCheckResourceAttr except the 326 // value is a pointer so that it can be updated while the test is running. 327 // It will only be dereferenced at the point this step is run. 328 func TestCheckResourceAttrPtr(name string, key string, value *string) TestCheckFunc { 329 return func(s *terraform.State) error { 330 return TestCheckResourceAttr(name, key, *value)(s) 331 } 332 } 333 334 // TestT is the interface used to handle the test lifecycle of a test. 335 // 336 // Users should just use a *testing.T object, which implements this. 337 type TestT interface { 338 Error(args ...interface{}) 339 Fatal(args ...interface{}) 340 Skip(args ...interface{}) 341 } 342 343 // This is set to true by unit tests to alter some behavior 344 var testTesting = false