]>
Commit | Line | Data |
---|---|---|
bae9f6d2 JC |
1 | package resource |
2 | ||
3 | import ( | |
107c1cdb | 4 | "bytes" |
9b12e4fe | 5 | "flag" |
bae9f6d2 JC |
6 | "fmt" |
7 | "io" | |
8 | "io/ioutil" | |
9 | "log" | |
10 | "os" | |
11 | "path/filepath" | |
12 | "reflect" | |
13 | "regexp" | |
14 | "strings" | |
15c0b25d | 15 | "syscall" |
bae9f6d2 JC |
16 | "testing" |
17 | ||
18 | "github.com/davecgh/go-spew/spew" | |
15c0b25d | 19 | "github.com/hashicorp/errwrap" |
bae9f6d2 | 20 | "github.com/hashicorp/go-multierror" |
15c0b25d | 21 | "github.com/hashicorp/logutils" |
107c1cdb ND |
22 | "github.com/mitchellh/colorstring" |
23 | ||
24 | "github.com/hashicorp/terraform/addrs" | |
25 | "github.com/hashicorp/terraform/command/format" | |
26 | "github.com/hashicorp/terraform/configs" | |
27 | "github.com/hashicorp/terraform/configs/configload" | |
bae9f6d2 | 28 | "github.com/hashicorp/terraform/helper/logging" |
107c1cdb ND |
29 | "github.com/hashicorp/terraform/internal/initwd" |
30 | "github.com/hashicorp/terraform/providers" | |
31 | "github.com/hashicorp/terraform/states" | |
bae9f6d2 | 32 | "github.com/hashicorp/terraform/terraform" |
107c1cdb | 33 | "github.com/hashicorp/terraform/tfdiags" |
bae9f6d2 JC |
34 | ) |
35 | ||
9b12e4fe JC |
36 | // flagSweep is a flag available when running tests on the command line. It |
37 | // contains a comma seperated list of regions to for the sweeper functions to | |
38 | // run in. This flag bypasses the normal Test path and instead runs functions designed to | |
39 | // clean up any leaked resources a testing environment could have created. It is | |
40 | // a best effort attempt, and relies on Provider authors to implement "Sweeper" | |
41 | // methods for resources. | |
42 | ||
43 | // Adding Sweeper methods with AddTestSweepers will | |
44 | // construct a list of sweeper funcs to be called here. We iterate through | |
45 | // regions provided by the sweep flag, and for each region we iterate through the | |
46 | // tests, and exit on any errors. At time of writing, sweepers are ran | |
47 | // sequentially, however they can list dependencies to be ran first. We track | |
48 | // the sweepers that have been ran, so as to not run a sweeper twice for a given | |
49 | // region. | |
50 | // | |
51 | // WARNING: | |
52 | // Sweepers are designed to be destructive. You should not use the -sweep flag | |
53 | // in any environment that is not strictly a test environment. Resources will be | |
54 | // destroyed. | |
55 | ||
56 | var flagSweep = flag.String("sweep", "", "List of Regions to run available Sweepers") | |
57 | var flagSweepRun = flag.String("sweep-run", "", "Comma seperated list of Sweeper Tests to run") | |
58 | var sweeperFuncs map[string]*Sweeper | |
59 | ||
60 | // map of sweepers that have ran, and the success/fail status based on any error | |
61 | // raised | |
62 | var sweeperRunList map[string]bool | |
63 | ||
64 | // type SweeperFunc is a signature for a function that acts as a sweeper. It | |
65 | // accepts a string for the region that the sweeper is to be ran in. This | |
66 | // function must be able to construct a valid client for that region. | |
67 | type SweeperFunc func(r string) error | |
68 | ||
69 | type Sweeper struct { | |
70 | // Name for sweeper. Must be unique to be ran by the Sweeper Runner | |
71 | Name string | |
72 | ||
73 | // Dependencies list the const names of other Sweeper functions that must be ran | |
74 | // prior to running this Sweeper. This is an ordered list that will be invoked | |
75 | // recursively at the helper/resource level | |
76 | Dependencies []string | |
77 | ||
78 | // Sweeper function that when invoked sweeps the Provider of specific | |
79 | // resources | |
80 | F SweeperFunc | |
81 | } | |
82 | ||
83 | func init() { | |
84 | sweeperFuncs = make(map[string]*Sweeper) | |
85 | } | |
86 | ||
87 | // AddTestSweepers function adds a given name and Sweeper configuration | |
88 | // pair to the internal sweeperFuncs map. Invoke this function to register a | |
89 | // resource sweeper to be available for running when the -sweep flag is used | |
90 | // with `go test`. Sweeper names must be unique to help ensure a given sweeper | |
91 | // is only ran once per run. | |
92 | func AddTestSweepers(name string, s *Sweeper) { | |
93 | if _, ok := sweeperFuncs[name]; ok { | |
94 | log.Fatalf("[ERR] Error adding (%s) to sweeperFuncs: function already exists in map", name) | |
95 | } | |
96 | ||
97 | sweeperFuncs[name] = s | |
98 | } | |
99 | ||
100 | func TestMain(m *testing.M) { | |
101 | flag.Parse() | |
102 | if *flagSweep != "" { | |
103 | // parse flagSweep contents for regions to run | |
104 | regions := strings.Split(*flagSweep, ",") | |
105 | ||
106 | // get filtered list of sweepers to run based on sweep-run flag | |
107 | sweepers := filterSweepers(*flagSweepRun, sweeperFuncs) | |
108 | for _, region := range regions { | |
109 | region = strings.TrimSpace(region) | |
110 | // reset sweeperRunList for each region | |
111 | sweeperRunList = map[string]bool{} | |
112 | ||
113 | log.Printf("[DEBUG] Running Sweepers for region (%s):\n", region) | |
114 | for _, sweeper := range sweepers { | |
115 | if err := runSweeperWithRegion(region, sweeper); err != nil { | |
116 | log.Fatalf("[ERR] error running (%s): %s", sweeper.Name, err) | |
117 | } | |
118 | } | |
119 | ||
120 | log.Printf("Sweeper Tests ran:\n") | |
121 | for s, _ := range sweeperRunList { | |
122 | fmt.Printf("\t- %s\n", s) | |
123 | } | |
124 | } | |
125 | } else { | |
126 | os.Exit(m.Run()) | |
127 | } | |
128 | } | |
129 | ||
130 | // filterSweepers takes a comma seperated string listing the names of sweepers | |
131 | // to be ran, and returns a filtered set from the list of all of sweepers to | |
132 | // run based on the names given. | |
133 | func filterSweepers(f string, source map[string]*Sweeper) map[string]*Sweeper { | |
134 | filterSlice := strings.Split(strings.ToLower(f), ",") | |
135 | if len(filterSlice) == 1 && filterSlice[0] == "" { | |
136 | // if the filter slice is a single element of "" then no sweeper list was | |
137 | // given, so just return the full list | |
138 | return source | |
139 | } | |
140 | ||
141 | sweepers := make(map[string]*Sweeper) | |
142 | for name, sweeper := range source { | |
143 | for _, s := range filterSlice { | |
144 | if strings.Contains(strings.ToLower(name), s) { | |
145 | sweepers[name] = sweeper | |
146 | } | |
147 | } | |
148 | } | |
149 | return sweepers | |
150 | } | |
151 | ||
152 | // runSweeperWithRegion recieves a sweeper and a region, and recursively calls | |
153 | // itself with that region for every dependency found for that sweeper. If there | |
154 | // are no dependencies, invoke the contained sweeper fun with the region, and | |
155 | // add the success/fail status to the sweeperRunList. | |
156 | func runSweeperWithRegion(region string, s *Sweeper) error { | |
157 | for _, dep := range s.Dependencies { | |
158 | if depSweeper, ok := sweeperFuncs[dep]; ok { | |
159 | log.Printf("[DEBUG] Sweeper (%s) has dependency (%s), running..", s.Name, dep) | |
160 | if err := runSweeperWithRegion(region, depSweeper); err != nil { | |
161 | return err | |
162 | } | |
163 | } else { | |
164 | log.Printf("[DEBUG] Sweeper (%s) has dependency (%s), but that sweeper was not found", s.Name, dep) | |
165 | } | |
166 | } | |
167 | ||
168 | if _, ok := sweeperRunList[s.Name]; ok { | |
169 | log.Printf("[DEBUG] Sweeper (%s) already ran in region (%s)", s.Name, region) | |
170 | return nil | |
171 | } | |
172 | ||
173 | runE := s.F(region) | |
174 | if runE == nil { | |
175 | sweeperRunList[s.Name] = true | |
176 | } else { | |
177 | sweeperRunList[s.Name] = false | |
178 | } | |
179 | ||
180 | return runE | |
181 | } | |
182 | ||
bae9f6d2 JC |
183 | const TestEnvVar = "TF_ACC" |
184 | ||
185 | // TestProvider can be implemented by any ResourceProvider to provide custom | |
186 | // reset functionality at the start of an acceptance test. | |
187 | // The helper/schema Provider implements this interface. | |
188 | type TestProvider interface { | |
189 | TestReset() error | |
190 | } | |
191 | ||
192 | // TestCheckFunc is the callback type used with acceptance tests to check | |
193 | // the state of a resource. The state passed in is the latest state known, | |
194 | // or in the case of being after a destroy, it is the last known state when | |
195 | // it was created. | |
196 | type TestCheckFunc func(*terraform.State) error | |
197 | ||
198 | // ImportStateCheckFunc is the check function for ImportState tests | |
199 | type ImportStateCheckFunc func([]*terraform.InstanceState) error | |
200 | ||
15c0b25d AP |
201 | // ImportStateIdFunc is an ID generation function to help with complex ID |
202 | // generation for ImportState tests. | |
203 | type ImportStateIdFunc func(*terraform.State) (string, error) | |
204 | ||
bae9f6d2 JC |
205 | // TestCase is a single acceptance test case used to test the apply/destroy |
206 | // lifecycle of a resource in a specific configuration. | |
207 | // | |
208 | // When the destroy plan is executed, the config from the last TestStep | |
209 | // is used to plan it. | |
210 | type TestCase struct { | |
211 | // IsUnitTest allows a test to run regardless of the TF_ACC | |
212 | // environment variable. This should be used with care - only for | |
213 | // fast tests on local resources (e.g. remote state with a local | |
214 | // backend) but can be used to increase confidence in correct | |
215 | // operation of Terraform without waiting for a full acctest run. | |
216 | IsUnitTest bool | |
217 | ||
218 | // PreCheck, if non-nil, will be called before any test steps are | |
219 | // executed. It will only be executed in the case that the steps | |
220 | // would run, so it can be used for some validation before running | |
221 | // acceptance tests, such as verifying that keys are setup. | |
222 | PreCheck func() | |
223 | ||
224 | // Providers is the ResourceProvider that will be under test. | |
225 | // | |
226 | // Alternately, ProviderFactories can be specified for the providers | |
227 | // that are valid. This takes priority over Providers. | |
228 | // | |
229 | // The end effect of each is the same: specifying the providers that | |
230 | // are used within the tests. | |
231 | Providers map[string]terraform.ResourceProvider | |
232 | ProviderFactories map[string]terraform.ResourceProviderFactory | |
233 | ||
234 | // PreventPostDestroyRefresh can be set to true for cases where data sources | |
235 | // are tested alongside real resources | |
236 | PreventPostDestroyRefresh bool | |
237 | ||
238 | // CheckDestroy is called after the resource is finally destroyed | |
239 | // to allow the tester to test that the resource is truly gone. | |
240 | CheckDestroy TestCheckFunc | |
241 | ||
242 | // Steps are the apply sequences done within the context of the | |
243 | // same state. Each step can have its own check to verify correctness. | |
244 | Steps []TestStep | |
245 | ||
246 | // The settings below control the "ID-only refresh test." This is | |
247 | // an enabled-by-default test that tests that a refresh can be | |
248 | // refreshed with only an ID to result in the same attributes. | |
249 | // This validates completeness of Refresh. | |
250 | // | |
251 | // IDRefreshName is the name of the resource to check. This will | |
252 | // default to the first non-nil primary resource in the state. | |
253 | // | |
254 | // IDRefreshIgnore is a list of configuration keys that will be ignored. | |
255 | IDRefreshName string | |
256 | IDRefreshIgnore []string | |
257 | } | |
258 | ||
259 | // TestStep is a single apply sequence of a test, done within the | |
260 | // context of a state. | |
261 | // | |
262 | // Multiple TestSteps can be sequenced in a Test to allow testing | |
263 | // potentially complex update logic. In general, simply create/destroy | |
264 | // tests will only need one step. | |
265 | type TestStep struct { | |
266 | // ResourceName should be set to the name of the resource | |
267 | // that is being tested. Example: "aws_instance.foo". Various test | |
268 | // modes use this to auto-detect state information. | |
269 | // | |
270 | // This is only required if the test mode settings below say it is | |
271 | // for the mode you're using. | |
272 | ResourceName string | |
273 | ||
274 | // PreConfig is called before the Config is applied to perform any per-step | |
275 | // setup that needs to happen. This is called regardless of "test mode" | |
276 | // below. | |
277 | PreConfig func() | |
278 | ||
15c0b25d AP |
279 | // Taint is a list of resource addresses to taint prior to the execution of |
280 | // the step. Be sure to only include this at a step where the referenced | |
281 | // address will be present in state, as it will fail the test if the resource | |
282 | // is missing. | |
283 | // | |
284 | // This option is ignored on ImportState tests, and currently only works for | |
285 | // resources in the root module path. | |
286 | Taint []string | |
287 | ||
bae9f6d2 JC |
288 | //--------------------------------------------------------------- |
289 | // Test modes. One of the following groups of settings must be | |
290 | // set to determine what the test step will do. Ideally we would've | |
291 | // used Go interfaces here but there are now hundreds of tests we don't | |
292 | // want to re-type so instead we just determine which step logic | |
293 | // to run based on what settings below are set. | |
294 | //--------------------------------------------------------------- | |
295 | ||
296 | //--------------------------------------------------------------- | |
297 | // Plan, Apply testing | |
298 | //--------------------------------------------------------------- | |
299 | ||
300 | // Config a string of the configuration to give to Terraform. If this | |
301 | // is set, then the TestCase will execute this step with the same logic | |
302 | // as a `terraform apply`. | |
303 | Config string | |
304 | ||
305 | // Check is called after the Config is applied. Use this step to | |
306 | // make your own API calls to check the status of things, and to | |
307 | // inspect the format of the ResourceState itself. | |
308 | // | |
309 | // If an error is returned, the test will fail. In this case, a | |
310 | // destroy plan will still be attempted. | |
311 | // | |
312 | // If this is nil, no check is done on this step. | |
313 | Check TestCheckFunc | |
314 | ||
315 | // Destroy will create a destroy plan if set to true. | |
316 | Destroy bool | |
317 | ||
318 | // ExpectNonEmptyPlan can be set to true for specific types of tests that are | |
319 | // looking to verify that a diff occurs | |
320 | ExpectNonEmptyPlan bool | |
321 | ||
322 | // ExpectError allows the construction of test cases that we expect to fail | |
323 | // with an error. The specified regexp must match against the error for the | |
324 | // test to pass. | |
325 | ExpectError *regexp.Regexp | |
326 | ||
327 | // PlanOnly can be set to only run `plan` with this configuration, and not | |
328 | // actually apply it. This is useful for ensuring config changes result in | |
329 | // no-op plans | |
330 | PlanOnly bool | |
331 | ||
15c0b25d AP |
332 | // PreventDiskCleanup can be set to true for testing terraform modules which |
333 | // require access to disk at runtime. Note that this will leave files in the | |
334 | // temp folder | |
335 | PreventDiskCleanup bool | |
336 | ||
bae9f6d2 JC |
337 | // PreventPostDestroyRefresh can be set to true for cases where data sources |
338 | // are tested alongside real resources | |
339 | PreventPostDestroyRefresh bool | |
340 | ||
15c0b25d AP |
341 | // SkipFunc is called before applying config, but after PreConfig |
342 | // This is useful for defining test steps with platform-dependent checks | |
343 | SkipFunc func() (bool, error) | |
344 | ||
bae9f6d2 JC |
345 | //--------------------------------------------------------------- |
346 | // ImportState testing | |
347 | //--------------------------------------------------------------- | |
348 | ||
349 | // ImportState, if true, will test the functionality of ImportState | |
350 | // by importing the resource with ResourceName (must be set) and the | |
351 | // ID of that resource. | |
352 | ImportState bool | |
353 | ||
354 | // ImportStateId is the ID to perform an ImportState operation with. | |
355 | // This is optional. If it isn't set, then the resource ID is automatically | |
356 | // determined by inspecting the state for ResourceName's ID. | |
357 | ImportStateId string | |
358 | ||
359 | // ImportStateIdPrefix is the prefix added in front of ImportStateId. | |
360 | // This can be useful in complex import cases, where more than one | |
361 | // attribute needs to be passed on as the Import ID. Mainly in cases | |
362 | // where the ID is not known, and a known prefix needs to be added to | |
363 | // the unset ImportStateId field. | |
364 | ImportStateIdPrefix string | |
365 | ||
15c0b25d AP |
366 | // ImportStateIdFunc is a function that can be used to dynamically generate |
367 | // the ID for the ImportState tests. It is sent the state, which can be | |
368 | // checked to derive the attributes necessary and generate the string in the | |
369 | // desired format. | |
370 | ImportStateIdFunc ImportStateIdFunc | |
371 | ||
bae9f6d2 JC |
372 | // ImportStateCheck checks the results of ImportState. It should be |
373 | // used to verify that the resulting value of ImportState has the | |
374 | // proper resources, IDs, and attributes. | |
375 | ImportStateCheck ImportStateCheckFunc | |
376 | ||
377 | // ImportStateVerify, if true, will also check that the state values | |
378 | // that are finally put into the state after import match for all the | |
379 | // IDs returned by the Import. | |
380 | // | |
381 | // ImportStateVerifyIgnore are fields that should not be verified to | |
382 | // be equal. These can be set to ephemeral fields or fields that can't | |
383 | // be refreshed and don't matter. | |
384 | ImportStateVerify bool | |
385 | ImportStateVerifyIgnore []string | |
107c1cdb ND |
386 | |
387 | // provider s is used internally to maintain a reference to the | |
388 | // underlying providers during the tests | |
389 | providers map[string]terraform.ResourceProvider | |
bae9f6d2 JC |
390 | } |
391 | ||
15c0b25d AP |
392 | // Set to a file mask in sprintf format where %s is test name |
393 | const EnvLogPathMask = "TF_LOG_PATH_MASK" | |
394 | ||
395 | func LogOutput(t TestT) (logOutput io.Writer, err error) { | |
396 | logOutput = ioutil.Discard | |
397 | ||
398 | logLevel := logging.LogLevel() | |
399 | if logLevel == "" { | |
400 | return | |
401 | } | |
402 | ||
403 | logOutput = os.Stderr | |
404 | ||
405 | if logPath := os.Getenv(logging.EnvLogFile); logPath != "" { | |
406 | var err error | |
407 | logOutput, err = os.OpenFile(logPath, syscall.O_CREAT|syscall.O_RDWR|syscall.O_APPEND, 0666) | |
408 | if err != nil { | |
409 | return nil, err | |
410 | } | |
411 | } | |
412 | ||
413 | if logPathMask := os.Getenv(EnvLogPathMask); logPathMask != "" { | |
414 | // Escape special characters which may appear if we have subtests | |
415 | testName := strings.Replace(t.Name(), "/", "__", -1) | |
416 | ||
417 | logPath := fmt.Sprintf(logPathMask, testName) | |
418 | var err error | |
419 | logOutput, err = os.OpenFile(logPath, syscall.O_CREAT|syscall.O_RDWR|syscall.O_APPEND, 0666) | |
420 | if err != nil { | |
421 | return nil, err | |
422 | } | |
423 | } | |
424 | ||
425 | // This was the default since the beginning | |
426 | logOutput = &logutils.LevelFilter{ | |
427 | Levels: logging.ValidLevels, | |
428 | MinLevel: logutils.LogLevel(logLevel), | |
429 | Writer: logOutput, | |
430 | } | |
431 | ||
432 | return | |
433 | } | |
434 | ||
435 | // ParallelTest performs an acceptance test on a resource, allowing concurrency | |
436 | // with other ParallelTest. | |
437 | // | |
438 | // Tests will fail if they do not properly handle conditions to allow multiple | |
439 | // tests to occur against the same resource or service (e.g. random naming). | |
440 | // All other requirements of the Test function also apply to this function. | |
441 | func ParallelTest(t TestT, c TestCase) { | |
442 | t.Parallel() | |
443 | Test(t, c) | |
444 | } | |
445 | ||
bae9f6d2 JC |
446 | // Test performs an acceptance test on a resource. |
447 | // | |
448 | // Tests are not run unless an environmental variable "TF_ACC" is | |
449 | // set to some non-empty value. This is to avoid test cases surprising | |
450 | // a user by creating real resources. | |
451 | // | |
452 | // Tests will fail unless the verbose flag (`go test -v`, or explicitly | |
453 | // the "-test.v" flag) is set. Because some acceptance tests take quite | |
454 | // long, we require the verbose flag so users are able to see progress | |
455 | // output. | |
456 | func Test(t TestT, c TestCase) { | |
457 | // We only run acceptance tests if an env var is set because they're | |
458 | // slow and generally require some outside configuration. You can opt out | |
459 | // of this with OverrideEnvVar on individual TestCases. | |
460 | if os.Getenv(TestEnvVar) == "" && !c.IsUnitTest { | |
461 | t.Skip(fmt.Sprintf( | |
462 | "Acceptance tests skipped unless env '%s' set", | |
463 | TestEnvVar)) | |
464 | return | |
465 | } | |
466 | ||
15c0b25d | 467 | logWriter, err := LogOutput(t) |
bae9f6d2 JC |
468 | if err != nil { |
469 | t.Error(fmt.Errorf("error setting up logging: %s", err)) | |
470 | } | |
471 | log.SetOutput(logWriter) | |
472 | ||
473 | // We require verbose mode so that the user knows what is going on. | |
474 | if !testTesting && !testing.Verbose() && !c.IsUnitTest { | |
475 | t.Fatal("Acceptance tests must be run with the -v flag on tests") | |
476 | return | |
477 | } | |
478 | ||
479 | // Run the PreCheck if we have it | |
480 | if c.PreCheck != nil { | |
481 | c.PreCheck() | |
482 | } | |
483 | ||
107c1cdb ND |
484 | // get instances of all providers, so we can use the individual |
485 | // resources to shim the state during the tests. | |
486 | providers := make(map[string]terraform.ResourceProvider) | |
487 | for name, pf := range testProviderFactories(c) { | |
488 | p, err := pf() | |
489 | if err != nil { | |
490 | t.Fatal(err) | |
491 | } | |
492 | providers[name] = p | |
493 | } | |
494 | ||
c680a8e1 | 495 | providerResolver, err := testProviderResolver(c) |
bae9f6d2 JC |
496 | if err != nil { |
497 | t.Fatal(err) | |
498 | } | |
107c1cdb | 499 | |
c680a8e1 | 500 | opts := terraform.ContextOpts{ProviderResolver: providerResolver} |
bae9f6d2 JC |
501 | |
502 | // A single state variable to track the lifecycle, starting with no state | |
503 | var state *terraform.State | |
504 | ||
505 | // Go through each step and run it | |
506 | var idRefreshCheck *terraform.ResourceState | |
507 | idRefresh := c.IDRefreshName != "" | |
508 | errored := false | |
509 | for i, step := range c.Steps { | |
107c1cdb ND |
510 | // insert the providers into the step so we can get the resources for |
511 | // shimming the state | |
512 | step.providers = providers | |
513 | ||
bae9f6d2 | 514 | var err error |
15c0b25d AP |
515 | log.Printf("[DEBUG] Test: Executing step %d", i) |
516 | ||
517 | if step.SkipFunc != nil { | |
518 | skip, err := step.SkipFunc() | |
519 | if err != nil { | |
520 | t.Fatal(err) | |
521 | } | |
522 | if skip { | |
523 | log.Printf("[WARN] Skipping step %d", i) | |
524 | continue | |
525 | } | |
526 | } | |
bae9f6d2 | 527 | |
c680a8e1 | 528 | if step.Config == "" && !step.ImportState { |
bae9f6d2 JC |
529 | err = fmt.Errorf( |
530 | "unknown test mode for step. Please see TestStep docs\n\n%#v", | |
531 | step) | |
c680a8e1 RS |
532 | } else { |
533 | if step.ImportState { | |
534 | if step.Config == "" { | |
535 | step.Config = testProviderConfig(c) | |
536 | } | |
537 | ||
538 | // Can optionally set step.Config in addition to | |
539 | // step.ImportState, to provide config for the import. | |
540 | state, err = testStepImportState(opts, state, step) | |
541 | } else { | |
542 | state, err = testStepConfig(opts, state, step) | |
543 | } | |
bae9f6d2 JC |
544 | } |
545 | ||
15c0b25d AP |
546 | // If we expected an error, but did not get one, fail |
547 | if err == nil && step.ExpectError != nil { | |
548 | errored = true | |
549 | t.Error(fmt.Sprintf( | |
550 | "Step %d, no error received, but expected a match to:\n\n%s\n\n", | |
551 | i, step.ExpectError)) | |
552 | break | |
553 | } | |
554 | ||
bae9f6d2 JC |
555 | // If there was an error, exit |
556 | if err != nil { | |
557 | // Perhaps we expected an error? Check if it matches | |
558 | if step.ExpectError != nil { | |
559 | if !step.ExpectError.MatchString(err.Error()) { | |
560 | errored = true | |
561 | t.Error(fmt.Sprintf( | |
562 | "Step %d, expected error:\n\n%s\n\nTo match:\n\n%s\n\n", | |
563 | i, err, step.ExpectError)) | |
564 | break | |
565 | } | |
566 | } else { | |
567 | errored = true | |
107c1cdb | 568 | t.Error(fmt.Sprintf("Step %d error: %s", i, detailedErrorMessage(err))) |
bae9f6d2 JC |
569 | break |
570 | } | |
571 | } | |
572 | ||
573 | // If we've never checked an id-only refresh and our state isn't | |
574 | // empty, find the first resource and test it. | |
575 | if idRefresh && idRefreshCheck == nil && !state.Empty() { | |
576 | // Find the first non-nil resource in the state | |
577 | for _, m := range state.Modules { | |
578 | if len(m.Resources) > 0 { | |
579 | if v, ok := m.Resources[c.IDRefreshName]; ok { | |
580 | idRefreshCheck = v | |
581 | } | |
582 | ||
583 | break | |
584 | } | |
585 | } | |
586 | ||
587 | // If we have an instance to check for refreshes, do it | |
588 | // immediately. We do it in the middle of another test | |
589 | // because it shouldn't affect the overall state (refresh | |
590 | // is read-only semantically) and we want to fail early if | |
591 | // this fails. If refresh isn't read-only, then this will have | |
592 | // caught a different bug. | |
593 | if idRefreshCheck != nil { | |
594 | log.Printf( | |
595 | "[WARN] Test: Running ID-only refresh check on %s", | |
596 | idRefreshCheck.Primary.ID) | |
597 | if err := testIDOnlyRefresh(c, opts, step, idRefreshCheck); err != nil { | |
598 | log.Printf("[ERROR] Test: ID-only test failed: %s", err) | |
599 | t.Error(fmt.Sprintf( | |
600 | "[ERROR] Test: ID-only test failed: %s", err)) | |
601 | break | |
602 | } | |
603 | } | |
604 | } | |
605 | } | |
606 | ||
607 | // If we never checked an id-only refresh, it is a failure. | |
608 | if idRefresh { | |
609 | if !errored && len(c.Steps) > 0 && idRefreshCheck == nil { | |
610 | t.Error("ID-only refresh check never ran.") | |
611 | } | |
612 | } | |
613 | ||
614 | // If we have a state, then run the destroy | |
615 | if state != nil { | |
616 | lastStep := c.Steps[len(c.Steps)-1] | |
617 | destroyStep := TestStep{ | |
618 | Config: lastStep.Config, | |
619 | Check: c.CheckDestroy, | |
620 | Destroy: true, | |
15c0b25d | 621 | PreventDiskCleanup: lastStep.PreventDiskCleanup, |
bae9f6d2 | 622 | PreventPostDestroyRefresh: c.PreventPostDestroyRefresh, |
107c1cdb | 623 | providers: providers, |
bae9f6d2 JC |
624 | } |
625 | ||
626 | log.Printf("[WARN] Test: Executing destroy step") | |
627 | state, err := testStep(opts, state, destroyStep) | |
628 | if err != nil { | |
629 | t.Error(fmt.Sprintf( | |
630 | "Error destroying resource! WARNING: Dangling resources\n"+ | |
631 | "may exist. The full state and error is shown below.\n\n"+ | |
632 | "Error: %s\n\nState: %s", | |
633 | err, | |
634 | state)) | |
635 | } | |
636 | } else { | |
637 | log.Printf("[WARN] Skipping destroy test since there is no state.") | |
638 | } | |
639 | } | |
640 | ||
c680a8e1 RS |
641 | // testProviderConfig takes the list of Providers in a TestCase and returns a |
642 | // config with only empty provider blocks. This is useful for Import, where no | |
643 | // config is provided, but the providers must be defined. | |
644 | func testProviderConfig(c TestCase) string { | |
645 | var lines []string | |
646 | for p := range c.Providers { | |
647 | lines = append(lines, fmt.Sprintf("provider %q {}\n", p)) | |
648 | } | |
649 | ||
650 | return strings.Join(lines, "") | |
651 | } | |
652 | ||
107c1cdb ND |
653 | // testProviderFactories combines the fixed Providers and |
654 | // ResourceProviderFactory functions into a single map of | |
655 | // ResourceProviderFactory functions. | |
656 | func testProviderFactories(c TestCase) map[string]terraform.ResourceProviderFactory { | |
657 | ctxProviders := make(map[string]terraform.ResourceProviderFactory) | |
658 | for k, pf := range c.ProviderFactories { | |
659 | ctxProviders[k] = pf | |
bae9f6d2 | 660 | } |
c680a8e1 | 661 | |
bae9f6d2 JC |
662 | // add any fixed providers |
663 | for k, p := range c.Providers { | |
664 | ctxProviders[k] = terraform.ResourceProviderFactoryFixed(p) | |
665 | } | |
107c1cdb ND |
666 | return ctxProviders |
667 | } | |
668 | ||
669 | // testProviderResolver is a helper to build a ResourceProviderResolver | |
670 | // with pre instantiated ResourceProviders, so that we can reset them for the | |
671 | // test, while only calling the factory function once. | |
672 | // Any errors are stored so that they can be returned by the factory in | |
673 | // terraform to match non-test behavior. | |
674 | func testProviderResolver(c TestCase) (providers.Resolver, error) { | |
675 | ctxProviders := testProviderFactories(c) | |
676 | ||
677 | // wrap the old provider factories in the test grpc server so they can be | |
678 | // called from terraform. | |
679 | newProviders := make(map[string]providers.Factory) | |
bae9f6d2 | 680 | |
bae9f6d2 | 681 | for k, pf := range ctxProviders { |
107c1cdb ND |
682 | factory := pf // must copy to ensure each closure sees its own value |
683 | newProviders[k] = func() (providers.Interface, error) { | |
684 | p, err := factory() | |
bae9f6d2 | 685 | if err != nil { |
107c1cdb | 686 | return nil, err |
bae9f6d2 | 687 | } |
107c1cdb ND |
688 | |
689 | // The provider is wrapped in a GRPCTestProvider so that it can be | |
690 | // passed back to terraform core as a providers.Interface, rather | |
691 | // than the legacy ResourceProvider. | |
692 | return GRPCTestProvider(p), nil | |
bae9f6d2 JC |
693 | } |
694 | } | |
695 | ||
107c1cdb | 696 | return providers.ResolverFixed(newProviders), nil |
bae9f6d2 JC |
697 | } |
698 | ||
699 | // UnitTest is a helper to force the acceptance testing harness to run in the | |
700 | // normal unit test suite. This should only be used for resource that don't | |
701 | // have any external dependencies. | |
702 | func UnitTest(t TestT, c TestCase) { | |
703 | c.IsUnitTest = true | |
704 | Test(t, c) | |
705 | } | |
706 | ||
707 | func testIDOnlyRefresh(c TestCase, opts terraform.ContextOpts, step TestStep, r *terraform.ResourceState) error { | |
708 | // TODO: We guard by this right now so master doesn't explode. We | |
709 | // need to remove this eventually to make this part of the normal tests. | |
710 | if os.Getenv("TF_ACC_IDONLY") == "" { | |
711 | return nil | |
712 | } | |
713 | ||
107c1cdb ND |
714 | addr := addrs.Resource{ |
715 | Mode: addrs.ManagedResourceMode, | |
716 | Type: r.Type, | |
717 | Name: "foo", | |
718 | }.Instance(addrs.NoKey) | |
719 | absAddr := addr.Absolute(addrs.RootModuleInstance) | |
bae9f6d2 JC |
720 | |
721 | // Build the state. The state is just the resource with an ID. There | |
722 | // are no attributes. We only set what is needed to perform a refresh. | |
107c1cdb ND |
723 | state := states.NewState() |
724 | state.RootModule().SetResourceInstanceCurrent( | |
725 | addr, | |
726 | &states.ResourceInstanceObjectSrc{ | |
727 | AttrsFlat: r.Primary.Attributes, | |
728 | Status: states.ObjectReady, | |
bae9f6d2 | 729 | }, |
107c1cdb ND |
730 | addrs.ProviderConfig{Type: "placeholder"}.Absolute(addrs.RootModuleInstance), |
731 | ) | |
bae9f6d2 JC |
732 | |
733 | // Create the config module. We use the full config because Refresh | |
734 | // doesn't have access to it and we may need things like provider | |
735 | // configurations. The initial implementation of id-only checks used | |
736 | // an empty config module, but that caused the aforementioned problems. | |
107c1cdb | 737 | cfg, err := testConfig(opts, step) |
bae9f6d2 JC |
738 | if err != nil { |
739 | return err | |
740 | } | |
741 | ||
742 | // Initialize the context | |
107c1cdb | 743 | opts.Config = cfg |
bae9f6d2 | 744 | opts.State = state |
107c1cdb ND |
745 | ctx, ctxDiags := terraform.NewContext(&opts) |
746 | if ctxDiags.HasErrors() { | |
747 | return ctxDiags.Err() | |
bae9f6d2 | 748 | } |
15c0b25d AP |
749 | if diags := ctx.Validate(); len(diags) > 0 { |
750 | if diags.HasErrors() { | |
751 | return errwrap.Wrapf("config is invalid: {{err}}", diags.Err()) | |
bae9f6d2 JC |
752 | } |
753 | ||
15c0b25d | 754 | log.Printf("[WARN] Config warnings:\n%s", diags.Err().Error()) |
bae9f6d2 JC |
755 | } |
756 | ||
757 | // Refresh! | |
107c1cdb ND |
758 | state, refreshDiags := ctx.Refresh() |
759 | if refreshDiags.HasErrors() { | |
760 | return refreshDiags.Err() | |
bae9f6d2 JC |
761 | } |
762 | ||
763 | // Verify attribute equivalence. | |
107c1cdb | 764 | actualR := state.ResourceInstance(absAddr) |
bae9f6d2 JC |
765 | if actualR == nil { |
766 | return fmt.Errorf("Resource gone!") | |
767 | } | |
107c1cdb | 768 | if actualR.Current == nil { |
bae9f6d2 JC |
769 | return fmt.Errorf("Resource has no primary instance") |
770 | } | |
107c1cdb | 771 | actual := actualR.Current.AttrsFlat |
bae9f6d2 JC |
772 | expected := r.Primary.Attributes |
773 | // Remove fields we're ignoring | |
774 | for _, v := range c.IDRefreshIgnore { | |
775 | for k, _ := range actual { | |
776 | if strings.HasPrefix(k, v) { | |
777 | delete(actual, k) | |
778 | } | |
779 | } | |
780 | for k, _ := range expected { | |
781 | if strings.HasPrefix(k, v) { | |
782 | delete(expected, k) | |
783 | } | |
784 | } | |
785 | } | |
786 | ||
787 | if !reflect.DeepEqual(actual, expected) { | |
788 | // Determine only the different attributes | |
789 | for k, v := range expected { | |
790 | if av, ok := actual[k]; ok && v == av { | |
791 | delete(expected, k) | |
792 | delete(actual, k) | |
793 | } | |
794 | } | |
795 | ||
796 | spewConf := spew.NewDefaultConfig() | |
797 | spewConf.SortKeys = true | |
798 | return fmt.Errorf( | |
799 | "Attributes not equivalent. Difference is shown below. Top is actual, bottom is expected."+ | |
800 | "\n\n%s\n\n%s", | |
801 | spewConf.Sdump(actual), spewConf.Sdump(expected)) | |
802 | } | |
803 | ||
804 | return nil | |
805 | } | |
806 | ||
107c1cdb | 807 | func testConfig(opts terraform.ContextOpts, step TestStep) (*configs.Config, error) { |
bae9f6d2 JC |
808 | if step.PreConfig != nil { |
809 | step.PreConfig() | |
810 | } | |
811 | ||
812 | cfgPath, err := ioutil.TempDir("", "tf-test") | |
813 | if err != nil { | |
107c1cdb | 814 | return nil, fmt.Errorf("Error creating temporary directory for config: %s", err) |
bae9f6d2 | 815 | } |
15c0b25d AP |
816 | |
817 | if step.PreventDiskCleanup { | |
818 | log.Printf("[INFO] Skipping defer os.RemoveAll call") | |
819 | } else { | |
820 | defer os.RemoveAll(cfgPath) | |
821 | } | |
bae9f6d2 | 822 | |
107c1cdb ND |
823 | // Write the main configuration file |
824 | err = ioutil.WriteFile(filepath.Join(cfgPath, "main.tf"), []byte(step.Config), os.ModePerm) | |
bae9f6d2 | 825 | if err != nil { |
107c1cdb | 826 | return nil, fmt.Errorf("Error creating temporary file for config: %s", err) |
bae9f6d2 JC |
827 | } |
828 | ||
107c1cdb ND |
829 | // Create directory for our child modules, if any. |
830 | modulesDir := filepath.Join(cfgPath, ".modules") | |
831 | err = os.Mkdir(modulesDir, os.ModePerm) | |
bae9f6d2 | 832 | if err != nil { |
107c1cdb | 833 | return nil, fmt.Errorf("Error creating child modules directory: %s", err) |
bae9f6d2 JC |
834 | } |
835 | ||
107c1cdb ND |
836 | inst := initwd.NewModuleInstaller(modulesDir, nil) |
837 | _, installDiags := inst.InstallModules(cfgPath, true, initwd.ModuleInstallHooksImpl{}) | |
838 | if installDiags.HasErrors() { | |
839 | return nil, installDiags.Err() | |
bae9f6d2 JC |
840 | } |
841 | ||
107c1cdb ND |
842 | loader, err := configload.NewLoader(&configload.Config{ |
843 | ModulesDir: modulesDir, | |
844 | }) | |
bae9f6d2 | 845 | if err != nil { |
107c1cdb ND |
846 | return nil, fmt.Errorf("failed to create config loader: %s", err) |
847 | } | |
848 | ||
849 | config, configDiags := loader.LoadConfig(cfgPath) | |
850 | if configDiags.HasErrors() { | |
851 | return nil, configDiags | |
bae9f6d2 JC |
852 | } |
853 | ||
107c1cdb | 854 | return config, nil |
bae9f6d2 JC |
855 | } |
856 | ||
857 | func testResource(c TestStep, state *terraform.State) (*terraform.ResourceState, error) { | |
858 | if c.ResourceName == "" { | |
859 | return nil, fmt.Errorf("ResourceName must be set in TestStep") | |
860 | } | |
861 | ||
862 | for _, m := range state.Modules { | |
863 | if len(m.Resources) > 0 { | |
864 | if v, ok := m.Resources[c.ResourceName]; ok { | |
865 | return v, nil | |
866 | } | |
867 | } | |
868 | } | |
869 | ||
870 | return nil, fmt.Errorf( | |
871 | "Resource specified by ResourceName couldn't be found: %s", c.ResourceName) | |
872 | } | |
873 | ||
874 | // ComposeTestCheckFunc lets you compose multiple TestCheckFuncs into | |
875 | // a single TestCheckFunc. | |
876 | // | |
877 | // As a user testing their provider, this lets you decompose your checks | |
878 | // into smaller pieces more easily. | |
879 | func ComposeTestCheckFunc(fs ...TestCheckFunc) TestCheckFunc { | |
880 | return func(s *terraform.State) error { | |
881 | for i, f := range fs { | |
882 | if err := f(s); err != nil { | |
883 | return fmt.Errorf("Check %d/%d error: %s", i+1, len(fs), err) | |
884 | } | |
885 | } | |
886 | ||
887 | return nil | |
888 | } | |
889 | } | |
890 | ||
891 | // ComposeAggregateTestCheckFunc lets you compose multiple TestCheckFuncs into | |
892 | // a single TestCheckFunc. | |
893 | // | |
894 | // As a user testing their provider, this lets you decompose your checks | |
895 | // into smaller pieces more easily. | |
896 | // | |
897 | // Unlike ComposeTestCheckFunc, ComposeAggergateTestCheckFunc runs _all_ of the | |
898 | // TestCheckFuncs and aggregates failures. | |
899 | func ComposeAggregateTestCheckFunc(fs ...TestCheckFunc) TestCheckFunc { | |
900 | return func(s *terraform.State) error { | |
901 | var result *multierror.Error | |
902 | ||
903 | for i, f := range fs { | |
904 | if err := f(s); err != nil { | |
905 | result = multierror.Append(result, fmt.Errorf("Check %d/%d error: %s", i+1, len(fs), err)) | |
906 | } | |
907 | } | |
908 | ||
909 | return result.ErrorOrNil() | |
910 | } | |
911 | } | |
912 | ||
913 | // TestCheckResourceAttrSet is a TestCheckFunc which ensures a value | |
914 | // exists in state for the given name/key combination. It is useful when | |
915 | // testing that computed values were set, when it is not possible to | |
916 | // know ahead of time what the values will be. | |
917 | func TestCheckResourceAttrSet(name, key string) TestCheckFunc { | |
918 | return func(s *terraform.State) error { | |
919 | is, err := primaryInstanceState(s, name) | |
920 | if err != nil { | |
921 | return err | |
922 | } | |
923 | ||
15c0b25d AP |
924 | return testCheckResourceAttrSet(is, name, key) |
925 | } | |
926 | } | |
927 | ||
928 | // TestCheckModuleResourceAttrSet - as per TestCheckResourceAttrSet but with | |
929 | // support for non-root modules | |
930 | func TestCheckModuleResourceAttrSet(mp []string, name string, key string) TestCheckFunc { | |
107c1cdb | 931 | mpt := addrs.Module(mp).UnkeyedInstanceShim() |
15c0b25d | 932 | return func(s *terraform.State) error { |
107c1cdb | 933 | is, err := modulePathPrimaryInstanceState(s, mpt, name) |
15c0b25d AP |
934 | if err != nil { |
935 | return err | |
bae9f6d2 JC |
936 | } |
937 | ||
15c0b25d AP |
938 | return testCheckResourceAttrSet(is, name, key) |
939 | } | |
940 | } | |
941 | ||
942 | func testCheckResourceAttrSet(is *terraform.InstanceState, name string, key string) error { | |
943 | if val, ok := is.Attributes[key]; !ok || val == "" { | |
bae9f6d2 JC |
944 | return fmt.Errorf("%s: Attribute '%s' expected to be set", name, key) |
945 | } | |
15c0b25d AP |
946 | |
947 | return nil | |
bae9f6d2 JC |
948 | } |
949 | ||
950 | // TestCheckResourceAttr is a TestCheckFunc which validates | |
951 | // the value in state for the given name/key combination. | |
952 | func TestCheckResourceAttr(name, key, value string) TestCheckFunc { | |
953 | return func(s *terraform.State) error { | |
954 | is, err := primaryInstanceState(s, name) | |
955 | if err != nil { | |
956 | return err | |
957 | } | |
958 | ||
15c0b25d AP |
959 | return testCheckResourceAttr(is, name, key, value) |
960 | } | |
961 | } | |
bae9f6d2 | 962 | |
15c0b25d AP |
963 | // TestCheckModuleResourceAttr - as per TestCheckResourceAttr but with |
964 | // support for non-root modules | |
965 | func TestCheckModuleResourceAttr(mp []string, name string, key string, value string) TestCheckFunc { | |
107c1cdb | 966 | mpt := addrs.Module(mp).UnkeyedInstanceShim() |
15c0b25d | 967 | return func(s *terraform.State) error { |
107c1cdb | 968 | is, err := modulePathPrimaryInstanceState(s, mpt, name) |
15c0b25d AP |
969 | if err != nil { |
970 | return err | |
bae9f6d2 JC |
971 | } |
972 | ||
15c0b25d AP |
973 | return testCheckResourceAttr(is, name, key, value) |
974 | } | |
975 | } | |
976 | ||
977 | func testCheckResourceAttr(is *terraform.InstanceState, name string, key string, value string) error { | |
107c1cdb ND |
978 | // Empty containers may be elided from the state. |
979 | // If the intent here is to check for an empty container, allow the key to | |
980 | // also be non-existent. | |
981 | emptyCheck := false | |
982 | if value == "0" && (strings.HasSuffix(key, ".#") || strings.HasSuffix(key, ".%")) { | |
983 | emptyCheck = true | |
984 | } | |
985 | ||
15c0b25d | 986 | if v, ok := is.Attributes[key]; !ok || v != value { |
107c1cdb ND |
987 | if emptyCheck && !ok { |
988 | return nil | |
989 | } | |
990 | ||
15c0b25d AP |
991 | if !ok { |
992 | return fmt.Errorf("%s: Attribute '%s' not found", name, key) | |
993 | } | |
994 | ||
995 | return fmt.Errorf( | |
996 | "%s: Attribute '%s' expected %#v, got %#v", | |
997 | name, | |
998 | key, | |
999 | value, | |
1000 | v) | |
bae9f6d2 | 1001 | } |
15c0b25d | 1002 | return nil |
bae9f6d2 JC |
1003 | } |
1004 | ||
1005 | // TestCheckNoResourceAttr is a TestCheckFunc which ensures that | |
1006 | // NO value exists in state for the given name/key combination. | |
1007 | func TestCheckNoResourceAttr(name, key string) TestCheckFunc { | |
1008 | return func(s *terraform.State) error { | |
1009 | is, err := primaryInstanceState(s, name) | |
1010 | if err != nil { | |
1011 | return err | |
1012 | } | |
1013 | ||
15c0b25d AP |
1014 | return testCheckNoResourceAttr(is, name, key) |
1015 | } | |
1016 | } | |
1017 | ||
1018 | // TestCheckModuleNoResourceAttr - as per TestCheckNoResourceAttr but with | |
1019 | // support for non-root modules | |
1020 | func TestCheckModuleNoResourceAttr(mp []string, name string, key string) TestCheckFunc { | |
107c1cdb | 1021 | mpt := addrs.Module(mp).UnkeyedInstanceShim() |
15c0b25d | 1022 | return func(s *terraform.State) error { |
107c1cdb | 1023 | is, err := modulePathPrimaryInstanceState(s, mpt, name) |
15c0b25d AP |
1024 | if err != nil { |
1025 | return err | |
bae9f6d2 JC |
1026 | } |
1027 | ||
15c0b25d | 1028 | return testCheckNoResourceAttr(is, name, key) |
bae9f6d2 JC |
1029 | } |
1030 | } | |
1031 | ||
15c0b25d | 1032 | func testCheckNoResourceAttr(is *terraform.InstanceState, name string, key string) error { |
107c1cdb ND |
1033 | // Empty containers may sometimes be included in the state. |
1034 | // If the intent here is to check for an empty container, allow the value to | |
1035 | // also be "0". | |
1036 | emptyCheck := false | |
1037 | if strings.HasSuffix(key, ".#") || strings.HasSuffix(key, ".%") { | |
1038 | emptyCheck = true | |
1039 | } | |
1040 | ||
1041 | val, exists := is.Attributes[key] | |
1042 | if emptyCheck && val == "0" { | |
1043 | return nil | |
1044 | } | |
1045 | ||
1046 | if exists { | |
15c0b25d AP |
1047 | return fmt.Errorf("%s: Attribute '%s' found when not expected", name, key) |
1048 | } | |
1049 | ||
1050 | return nil | |
1051 | } | |
1052 | ||
bae9f6d2 JC |
1053 | // TestMatchResourceAttr is a TestCheckFunc which checks that the value |
1054 | // in state for the given name/key combination matches the given regex. | |
1055 | func TestMatchResourceAttr(name, key string, r *regexp.Regexp) TestCheckFunc { | |
1056 | return func(s *terraform.State) error { | |
1057 | is, err := primaryInstanceState(s, name) | |
1058 | if err != nil { | |
1059 | return err | |
1060 | } | |
1061 | ||
15c0b25d AP |
1062 | return testMatchResourceAttr(is, name, key, r) |
1063 | } | |
1064 | } | |
1065 | ||
1066 | // TestModuleMatchResourceAttr - as per TestMatchResourceAttr but with | |
1067 | // support for non-root modules | |
1068 | func TestModuleMatchResourceAttr(mp []string, name string, key string, r *regexp.Regexp) TestCheckFunc { | |
107c1cdb | 1069 | mpt := addrs.Module(mp).UnkeyedInstanceShim() |
15c0b25d | 1070 | return func(s *terraform.State) error { |
107c1cdb | 1071 | is, err := modulePathPrimaryInstanceState(s, mpt, name) |
15c0b25d AP |
1072 | if err != nil { |
1073 | return err | |
bae9f6d2 JC |
1074 | } |
1075 | ||
15c0b25d AP |
1076 | return testMatchResourceAttr(is, name, key, r) |
1077 | } | |
1078 | } | |
1079 | ||
1080 | func testMatchResourceAttr(is *terraform.InstanceState, name string, key string, r *regexp.Regexp) error { | |
1081 | if !r.MatchString(is.Attributes[key]) { | |
1082 | return fmt.Errorf( | |
1083 | "%s: Attribute '%s' didn't match %q, got %#v", | |
1084 | name, | |
1085 | key, | |
1086 | r.String(), | |
1087 | is.Attributes[key]) | |
bae9f6d2 | 1088 | } |
15c0b25d AP |
1089 | |
1090 | return nil | |
bae9f6d2 JC |
1091 | } |
1092 | ||
1093 | // TestCheckResourceAttrPtr is like TestCheckResourceAttr except the | |
1094 | // value is a pointer so that it can be updated while the test is running. | |
1095 | // It will only be dereferenced at the point this step is run. | |
1096 | func TestCheckResourceAttrPtr(name string, key string, value *string) TestCheckFunc { | |
1097 | return func(s *terraform.State) error { | |
1098 | return TestCheckResourceAttr(name, key, *value)(s) | |
1099 | } | |
1100 | } | |
1101 | ||
15c0b25d AP |
1102 | // TestCheckModuleResourceAttrPtr - as per TestCheckResourceAttrPtr but with |
1103 | // support for non-root modules | |
1104 | func TestCheckModuleResourceAttrPtr(mp []string, name string, key string, value *string) TestCheckFunc { | |
1105 | return func(s *terraform.State) error { | |
1106 | return TestCheckModuleResourceAttr(mp, name, key, *value)(s) | |
1107 | } | |
1108 | } | |
1109 | ||
bae9f6d2 JC |
1110 | // TestCheckResourceAttrPair is a TestCheckFunc which validates that the values |
1111 | // in state for a pair of name/key combinations are equal. | |
1112 | func TestCheckResourceAttrPair(nameFirst, keyFirst, nameSecond, keySecond string) TestCheckFunc { | |
1113 | return func(s *terraform.State) error { | |
1114 | isFirst, err := primaryInstanceState(s, nameFirst) | |
1115 | if err != nil { | |
1116 | return err | |
1117 | } | |
bae9f6d2 JC |
1118 | |
1119 | isSecond, err := primaryInstanceState(s, nameSecond) | |
1120 | if err != nil { | |
1121 | return err | |
1122 | } | |
15c0b25d AP |
1123 | |
1124 | return testCheckResourceAttrPair(isFirst, nameFirst, keyFirst, isSecond, nameSecond, keySecond) | |
1125 | } | |
1126 | } | |
1127 | ||
1128 | // TestCheckModuleResourceAttrPair - as per TestCheckResourceAttrPair but with | |
1129 | // support for non-root modules | |
1130 | func TestCheckModuleResourceAttrPair(mpFirst []string, nameFirst string, keyFirst string, mpSecond []string, nameSecond string, keySecond string) TestCheckFunc { | |
107c1cdb ND |
1131 | mptFirst := addrs.Module(mpFirst).UnkeyedInstanceShim() |
1132 | mptSecond := addrs.Module(mpSecond).UnkeyedInstanceShim() | |
15c0b25d | 1133 | return func(s *terraform.State) error { |
107c1cdb | 1134 | isFirst, err := modulePathPrimaryInstanceState(s, mptFirst, nameFirst) |
15c0b25d AP |
1135 | if err != nil { |
1136 | return err | |
bae9f6d2 JC |
1137 | } |
1138 | ||
107c1cdb | 1139 | isSecond, err := modulePathPrimaryInstanceState(s, mptSecond, nameSecond) |
15c0b25d AP |
1140 | if err != nil { |
1141 | return err | |
bae9f6d2 JC |
1142 | } |
1143 | ||
15c0b25d | 1144 | return testCheckResourceAttrPair(isFirst, nameFirst, keyFirst, isSecond, nameSecond, keySecond) |
bae9f6d2 JC |
1145 | } |
1146 | } | |
1147 | ||
15c0b25d | 1148 | func testCheckResourceAttrPair(isFirst *terraform.InstanceState, nameFirst string, keyFirst string, isSecond *terraform.InstanceState, nameSecond string, keySecond string) error { |
107c1cdb ND |
1149 | vFirst, okFirst := isFirst.Attributes[keyFirst] |
1150 | vSecond, okSecond := isSecond.Attributes[keySecond] | |
1151 | ||
1152 | // Container count values of 0 should not be relied upon, and not reliably | |
1153 | // maintained by helper/schema. For the purpose of tests, consider unset and | |
1154 | // 0 to be equal. | |
1155 | if len(keyFirst) > 2 && len(keySecond) > 2 && keyFirst[len(keyFirst)-2:] == keySecond[len(keySecond)-2:] && | |
1156 | (strings.HasSuffix(keyFirst, ".#") || strings.HasSuffix(keyFirst, ".%")) { | |
1157 | // they have the same suffix, and it is a collection count key. | |
1158 | if vFirst == "0" || vFirst == "" { | |
1159 | okFirst = false | |
1160 | } | |
1161 | if vSecond == "0" || vSecond == "" { | |
1162 | okSecond = false | |
1163 | } | |
15c0b25d AP |
1164 | } |
1165 | ||
107c1cdb ND |
1166 | if okFirst != okSecond { |
1167 | if !okFirst { | |
1168 | return fmt.Errorf("%s: Attribute %q not set, but %q is set in %s as %q", nameFirst, keyFirst, keySecond, nameSecond, vSecond) | |
1169 | } | |
1170 | return fmt.Errorf("%s: Attribute %q is %q, but %q is not set in %s", nameFirst, keyFirst, vFirst, keySecond, nameSecond) | |
1171 | } | |
1172 | if !(okFirst || okSecond) { | |
1173 | // If they both don't exist then they are equally unset, so that's okay. | |
1174 | return nil | |
15c0b25d AP |
1175 | } |
1176 | ||
1177 | if vFirst != vSecond { | |
1178 | return fmt.Errorf( | |
1179 | "%s: Attribute '%s' expected %#v, got %#v", | |
1180 | nameFirst, | |
1181 | keyFirst, | |
1182 | vSecond, | |
1183 | vFirst) | |
1184 | } | |
1185 | ||
1186 | return nil | |
1187 | } | |
1188 | ||
bae9f6d2 JC |
1189 | // TestCheckOutput checks an output in the Terraform configuration |
1190 | func TestCheckOutput(name, value string) TestCheckFunc { | |
1191 | return func(s *terraform.State) error { | |
1192 | ms := s.RootModule() | |
1193 | rs, ok := ms.Outputs[name] | |
1194 | if !ok { | |
1195 | return fmt.Errorf("Not found: %s", name) | |
1196 | } | |
1197 | ||
1198 | if rs.Value != value { | |
1199 | return fmt.Errorf( | |
1200 | "Output '%s': expected %#v, got %#v", | |
1201 | name, | |
1202 | value, | |
1203 | rs) | |
1204 | } | |
1205 | ||
1206 | return nil | |
1207 | } | |
1208 | } | |
1209 | ||
1210 | func TestMatchOutput(name string, r *regexp.Regexp) TestCheckFunc { | |
1211 | return func(s *terraform.State) error { | |
1212 | ms := s.RootModule() | |
1213 | rs, ok := ms.Outputs[name] | |
1214 | if !ok { | |
1215 | return fmt.Errorf("Not found: %s", name) | |
1216 | } | |
1217 | ||
1218 | if !r.MatchString(rs.Value.(string)) { | |
1219 | return fmt.Errorf( | |
1220 | "Output '%s': %#v didn't match %q", | |
1221 | name, | |
1222 | rs, | |
1223 | r.String()) | |
1224 | } | |
1225 | ||
1226 | return nil | |
1227 | } | |
1228 | } | |
1229 | ||
1230 | // TestT is the interface used to handle the test lifecycle of a test. | |
1231 | // | |
1232 | // Users should just use a *testing.T object, which implements this. | |
1233 | type TestT interface { | |
1234 | Error(args ...interface{}) | |
1235 | Fatal(args ...interface{}) | |
1236 | Skip(args ...interface{}) | |
15c0b25d AP |
1237 | Name() string |
1238 | Parallel() | |
bae9f6d2 JC |
1239 | } |
1240 | ||
1241 | // This is set to true by unit tests to alter some behavior | |
1242 | var testTesting = false | |
1243 | ||
15c0b25d AP |
1244 | // modulePrimaryInstanceState returns the instance state for the given resource |
1245 | // name in a ModuleState | |
1246 | func modulePrimaryInstanceState(s *terraform.State, ms *terraform.ModuleState, name string) (*terraform.InstanceState, error) { | |
bae9f6d2 JC |
1247 | rs, ok := ms.Resources[name] |
1248 | if !ok { | |
15c0b25d | 1249 | return nil, fmt.Errorf("Not found: %s in %s", name, ms.Path) |
bae9f6d2 JC |
1250 | } |
1251 | ||
1252 | is := rs.Primary | |
1253 | if is == nil { | |
15c0b25d | 1254 | return nil, fmt.Errorf("No primary instance: %s in %s", name, ms.Path) |
bae9f6d2 JC |
1255 | } |
1256 | ||
1257 | return is, nil | |
1258 | } | |
15c0b25d AP |
1259 | |
1260 | // modulePathPrimaryInstanceState returns the primary instance state for the | |
1261 | // given resource name in a given module path. | |
107c1cdb | 1262 | func modulePathPrimaryInstanceState(s *terraform.State, mp addrs.ModuleInstance, name string) (*terraform.InstanceState, error) { |
15c0b25d AP |
1263 | ms := s.ModuleByPath(mp) |
1264 | if ms == nil { | |
1265 | return nil, fmt.Errorf("No module found at: %s", mp) | |
1266 | } | |
1267 | ||
1268 | return modulePrimaryInstanceState(s, ms, name) | |
1269 | } | |
1270 | ||
1271 | // primaryInstanceState returns the primary instance state for the given | |
1272 | // resource name in the root module. | |
1273 | func primaryInstanceState(s *terraform.State, name string) (*terraform.InstanceState, error) { | |
1274 | ms := s.RootModule() | |
1275 | return modulePrimaryInstanceState(s, ms, name) | |
1276 | } | |
107c1cdb ND |
1277 | |
1278 | // operationError is a specialized implementation of error used to describe | |
1279 | // failures during one of the several operations performed for a particular | |
1280 | // test case. | |
1281 | type operationError struct { | |
1282 | OpName string | |
1283 | Diags tfdiags.Diagnostics | |
1284 | } | |
1285 | ||
1286 | func newOperationError(opName string, diags tfdiags.Diagnostics) error { | |
1287 | return operationError{opName, diags} | |
1288 | } | |
1289 | ||
1290 | // Error returns a terse error string containing just the basic diagnostic | |
1291 | // messages, for situations where normal Go error behavior is appropriate. | |
1292 | func (err operationError) Error() string { | |
1293 | return fmt.Sprintf("errors during %s: %s", err.OpName, err.Diags.Err().Error()) | |
1294 | } | |
1295 | ||
1296 | // ErrorDetail is like Error except it includes verbosely-rendered diagnostics | |
1297 | // similar to what would come from a normal Terraform run, which include | |
1298 | // additional context not included in Error(). | |
1299 | func (err operationError) ErrorDetail() string { | |
1300 | var buf bytes.Buffer | |
1301 | fmt.Fprintf(&buf, "errors during %s:", err.OpName) | |
1302 | clr := &colorstring.Colorize{Disable: true, Colors: colorstring.DefaultColors} | |
1303 | for _, diag := range err.Diags { | |
1304 | diagStr := format.Diagnostic(diag, nil, clr, 78) | |
1305 | buf.WriteByte('\n') | |
1306 | buf.WriteString(diagStr) | |
1307 | } | |
1308 | return buf.String() | |
1309 | } | |
1310 | ||
1311 | // detailedErrorMessage is a helper for calling ErrorDetail on an error if | |
1312 | // it is an operationError or just taking Error otherwise. | |
1313 | func detailedErrorMessage(err error) string { | |
1314 | switch tErr := err.(type) { | |
1315 | case operationError: | |
1316 | return tErr.ErrorDetail() | |
1317 | default: | |
1318 | return err.Error() | |
1319 | } | |
1320 | } |