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