]> git.immae.eu Git - github/fretlink/terraform-provider-statuscake.git/blob - vendor/github.com/hashicorp/terraform/helper/resource/testing_config.go
Upgrade to 0.12
[github/fretlink/terraform-provider-statuscake.git] / vendor / github.com / hashicorp / terraform / helper / resource / testing_config.go
1 package resource
2
3 import (
4 "bufio"
5 "bytes"
6 "errors"
7 "fmt"
8 "log"
9 "sort"
10 "strings"
11
12 "github.com/hashicorp/terraform/addrs"
13 "github.com/hashicorp/terraform/config"
14 "github.com/hashicorp/terraform/config/hcl2shim"
15 "github.com/hashicorp/terraform/states"
16
17 "github.com/hashicorp/errwrap"
18 "github.com/hashicorp/terraform/plans"
19 "github.com/hashicorp/terraform/terraform"
20 "github.com/hashicorp/terraform/tfdiags"
21 )
22
23 // testStepConfig runs a config-mode test step
24 func testStepConfig(
25 opts terraform.ContextOpts,
26 state *terraform.State,
27 step TestStep) (*terraform.State, error) {
28 return testStep(opts, state, step)
29 }
30
31 func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep) (*terraform.State, error) {
32 if !step.Destroy {
33 if err := testStepTaint(state, step); err != nil {
34 return state, err
35 }
36 }
37
38 cfg, err := testConfig(opts, step)
39 if err != nil {
40 return state, err
41 }
42
43 var stepDiags tfdiags.Diagnostics
44
45 // Build the context
46 opts.Config = cfg
47 opts.State, err = terraform.ShimLegacyState(state)
48 if err != nil {
49 return nil, err
50 }
51
52 opts.Destroy = step.Destroy
53 ctx, stepDiags := terraform.NewContext(&opts)
54 if stepDiags.HasErrors() {
55 return state, fmt.Errorf("Error initializing context: %s", stepDiags.Err())
56 }
57 if stepDiags := ctx.Validate(); len(stepDiags) > 0 {
58 if stepDiags.HasErrors() {
59 return state, errwrap.Wrapf("config is invalid: {{err}}", stepDiags.Err())
60 }
61
62 log.Printf("[WARN] Config warnings:\n%s", stepDiags)
63 }
64
65 // Refresh!
66 newState, stepDiags := ctx.Refresh()
67 // shim the state first so the test can check the state on errors
68
69 state, err = shimNewState(newState, step.providers)
70 if err != nil {
71 return nil, err
72 }
73 if stepDiags.HasErrors() {
74 return state, newOperationError("refresh", stepDiags)
75 }
76
77 // If this step is a PlanOnly step, skip over this first Plan and subsequent
78 // Apply, and use the follow up Plan that checks for perpetual diffs
79 if !step.PlanOnly {
80 // Plan!
81 if p, stepDiags := ctx.Plan(); stepDiags.HasErrors() {
82 return state, newOperationError("plan", stepDiags)
83 } else {
84 log.Printf("[WARN] Test: Step plan: %s", legacyPlanComparisonString(newState, p.Changes))
85 }
86
87 // We need to keep a copy of the state prior to destroying
88 // such that destroy steps can verify their behavior in the check
89 // function
90 stateBeforeApplication := state.DeepCopy()
91
92 // Apply the diff, creating real resources.
93 newState, stepDiags = ctx.Apply()
94 // shim the state first so the test can check the state on errors
95 state, err = shimNewState(newState, step.providers)
96 if err != nil {
97 return nil, err
98 }
99 if stepDiags.HasErrors() {
100 return state, newOperationError("apply", stepDiags)
101 }
102
103 // Run any configured checks
104 if step.Check != nil {
105 if step.Destroy {
106 if err := step.Check(stateBeforeApplication); err != nil {
107 return state, fmt.Errorf("Check failed: %s", err)
108 }
109 } else {
110 if err := step.Check(state); err != nil {
111 return state, fmt.Errorf("Check failed: %s", err)
112 }
113 }
114 }
115 }
116
117 // Now, verify that Plan is now empty and we don't have a perpetual diff issue
118 // We do this with TWO plans. One without a refresh.
119 var p *plans.Plan
120 if p, stepDiags = ctx.Plan(); stepDiags.HasErrors() {
121 return state, newOperationError("follow-up plan", stepDiags)
122 }
123 if !p.Changes.Empty() {
124 if step.ExpectNonEmptyPlan {
125 log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", legacyPlanComparisonString(newState, p.Changes))
126 } else {
127 return state, fmt.Errorf(
128 "After applying this step, the plan was not empty:\n\n%s", legacyPlanComparisonString(newState, p.Changes))
129 }
130 }
131
132 // And another after a Refresh.
133 if !step.Destroy || (step.Destroy && !step.PreventPostDestroyRefresh) {
134 newState, stepDiags = ctx.Refresh()
135 if stepDiags.HasErrors() {
136 return state, newOperationError("follow-up refresh", stepDiags)
137 }
138
139 state, err = shimNewState(newState, step.providers)
140 if err != nil {
141 return nil, err
142 }
143 }
144 if p, stepDiags = ctx.Plan(); stepDiags.HasErrors() {
145 return state, newOperationError("second follow-up refresh", stepDiags)
146 }
147 empty := p.Changes.Empty()
148
149 // Data resources are tricky because they legitimately get instantiated
150 // during refresh so that they will be already populated during the
151 // plan walk. Because of this, if we have any data resources in the
152 // config we'll end up wanting to destroy them again here. This is
153 // acceptable and expected, and we'll treat it as "empty" for the
154 // sake of this testing.
155 if step.Destroy && !empty {
156 empty = true
157 for _, change := range p.Changes.Resources {
158 if change.Addr.Resource.Resource.Mode != addrs.DataResourceMode {
159 empty = false
160 break
161 }
162 }
163 }
164
165 if !empty {
166 if step.ExpectNonEmptyPlan {
167 log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", legacyPlanComparisonString(newState, p.Changes))
168 } else {
169 return state, fmt.Errorf(
170 "After applying this step and refreshing, "+
171 "the plan was not empty:\n\n%s", legacyPlanComparisonString(newState, p.Changes))
172 }
173 }
174
175 // Made it here, but expected a non-empty plan, fail!
176 if step.ExpectNonEmptyPlan && empty {
177 return state, fmt.Errorf("Expected a non-empty plan, but got an empty plan!")
178 }
179
180 // Made it here? Good job test step!
181 return state, nil
182 }
183
184 // legacyPlanComparisonString produces a string representation of the changes
185 // from a plan and a given state togther, as was formerly produced by the
186 // String method of terraform.Plan.
187 //
188 // This is here only for compatibility with existing tests that predate our
189 // new plan and state types, and should not be used in new tests. Instead, use
190 // a library like "cmp" to do a deep equality and diff on the two
191 // data structures.
192 func legacyPlanComparisonString(state *states.State, changes *plans.Changes) string {
193 return fmt.Sprintf(
194 "DIFF:\n\n%s\n\nSTATE:\n\n%s",
195 legacyDiffComparisonString(changes),
196 state.String(),
197 )
198 }
199
200 // legacyDiffComparisonString produces a string representation of the changes
201 // from a planned changes object, as was formerly produced by the String method
202 // of terraform.Diff.
203 //
204 // This is here only for compatibility with existing tests that predate our
205 // new plan types, and should not be used in new tests. Instead, use a library
206 // like "cmp" to do a deep equality check and diff on the two data structures.
207 func legacyDiffComparisonString(changes *plans.Changes) string {
208 // The old string representation of a plan was grouped by module, but
209 // our new plan structure is not grouped in that way and so we'll need
210 // to preprocess it in order to produce that grouping.
211 type ResourceChanges struct {
212 Current *plans.ResourceInstanceChangeSrc
213 Deposed map[states.DeposedKey]*plans.ResourceInstanceChangeSrc
214 }
215 byModule := map[string]map[string]*ResourceChanges{}
216 resourceKeys := map[string][]string{}
217 requiresReplace := map[string][]string{}
218 var moduleKeys []string
219 for _, rc := range changes.Resources {
220 if rc.Action == plans.NoOp {
221 // We won't mention no-op changes here at all, since the old plan
222 // model we are emulating here didn't have such a concept.
223 continue
224 }
225 moduleKey := rc.Addr.Module.String()
226 if _, exists := byModule[moduleKey]; !exists {
227 moduleKeys = append(moduleKeys, moduleKey)
228 byModule[moduleKey] = make(map[string]*ResourceChanges)
229 }
230 resourceKey := rc.Addr.Resource.String()
231 if _, exists := byModule[moduleKey][resourceKey]; !exists {
232 resourceKeys[moduleKey] = append(resourceKeys[moduleKey], resourceKey)
233 byModule[moduleKey][resourceKey] = &ResourceChanges{
234 Deposed: make(map[states.DeposedKey]*plans.ResourceInstanceChangeSrc),
235 }
236 }
237
238 if rc.DeposedKey == states.NotDeposed {
239 byModule[moduleKey][resourceKey].Current = rc
240 } else {
241 byModule[moduleKey][resourceKey].Deposed[rc.DeposedKey] = rc
242 }
243
244 rr := []string{}
245 for _, p := range rc.RequiredReplace.List() {
246 rr = append(rr, hcl2shim.FlatmapKeyFromPath(p))
247 }
248 requiresReplace[resourceKey] = rr
249 }
250 sort.Strings(moduleKeys)
251 for _, ks := range resourceKeys {
252 sort.Strings(ks)
253 }
254
255 var buf bytes.Buffer
256
257 for _, moduleKey := range moduleKeys {
258 rcs := byModule[moduleKey]
259 var mBuf bytes.Buffer
260
261 for _, resourceKey := range resourceKeys[moduleKey] {
262 rc := rcs[resourceKey]
263
264 forceNewAttrs := requiresReplace[resourceKey]
265
266 crud := "UPDATE"
267 if rc.Current != nil {
268 switch rc.Current.Action {
269 case plans.DeleteThenCreate:
270 crud = "DESTROY/CREATE"
271 case plans.CreateThenDelete:
272 crud = "CREATE/DESTROY"
273 case plans.Delete:
274 crud = "DESTROY"
275 case plans.Create:
276 crud = "CREATE"
277 }
278 } else {
279 // We must be working on a deposed object then, in which
280 // case destroying is the only possible action.
281 crud = "DESTROY"
282 }
283
284 extra := ""
285 if rc.Current == nil && len(rc.Deposed) > 0 {
286 extra = " (deposed only)"
287 }
288
289 fmt.Fprintf(
290 &mBuf, "%s: %s%s\n",
291 crud, resourceKey, extra,
292 )
293
294 attrNames := map[string]bool{}
295 var oldAttrs map[string]string
296 var newAttrs map[string]string
297 if rc.Current != nil {
298 if before := rc.Current.Before; before != nil {
299 ty, err := before.ImpliedType()
300 if err == nil {
301 val, err := before.Decode(ty)
302 if err == nil {
303 oldAttrs = hcl2shim.FlatmapValueFromHCL2(val)
304 for k := range oldAttrs {
305 attrNames[k] = true
306 }
307 }
308 }
309 }
310 if after := rc.Current.After; after != nil {
311 ty, err := after.ImpliedType()
312 if err == nil {
313 val, err := after.Decode(ty)
314 if err == nil {
315 newAttrs = hcl2shim.FlatmapValueFromHCL2(val)
316 for k := range newAttrs {
317 attrNames[k] = true
318 }
319 }
320 }
321 }
322 }
323 if oldAttrs == nil {
324 oldAttrs = make(map[string]string)
325 }
326 if newAttrs == nil {
327 newAttrs = make(map[string]string)
328 }
329
330 attrNamesOrder := make([]string, 0, len(attrNames))
331 keyLen := 0
332 for n := range attrNames {
333 attrNamesOrder = append(attrNamesOrder, n)
334 if len(n) > keyLen {
335 keyLen = len(n)
336 }
337 }
338 sort.Strings(attrNamesOrder)
339
340 for _, attrK := range attrNamesOrder {
341 v := newAttrs[attrK]
342 u := oldAttrs[attrK]
343
344 if v == config.UnknownVariableValue {
345 v = "<computed>"
346 }
347 // NOTE: we don't support <sensitive> here because we would
348 // need schema to do that. Excluding sensitive values
349 // is now done at the UI layer, and so should not be tested
350 // at the core layer.
351
352 updateMsg := ""
353
354 // This may not be as precise as in the old diff, as it matches
355 // everything under the attribute that was originally marked as
356 // ForceNew, but should help make it easier to determine what
357 // caused replacement here.
358 for _, k := range forceNewAttrs {
359 if strings.HasPrefix(attrK, k) {
360 updateMsg = " (forces new resource)"
361 break
362 }
363 }
364
365 fmt.Fprintf(
366 &mBuf, " %s:%s %#v => %#v%s\n",
367 attrK,
368 strings.Repeat(" ", keyLen-len(attrK)),
369 u, v,
370 updateMsg,
371 )
372 }
373 }
374
375 if moduleKey == "" { // root module
376 buf.Write(mBuf.Bytes())
377 buf.WriteByte('\n')
378 continue
379 }
380
381 fmt.Fprintf(&buf, "%s:\n", moduleKey)
382 s := bufio.NewScanner(&mBuf)
383 for s.Scan() {
384 buf.WriteString(fmt.Sprintf(" %s\n", s.Text()))
385 }
386 }
387
388 return buf.String()
389 }
390
391 func testStepTaint(state *terraform.State, step TestStep) error {
392 for _, p := range step.Taint {
393 m := state.RootModule()
394 if m == nil {
395 return errors.New("no state")
396 }
397 rs, ok := m.Resources[p]
398 if !ok {
399 return fmt.Errorf("resource %q not found in state", p)
400 }
401 log.Printf("[WARN] Test: Explicitly tainting resource %q", p)
402 rs.Taint()
403 }
404 return nil
405 }