]> git.immae.eu Git - github/fretlink/terraform-provider-statuscake.git/blame - 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
CommitLineData
bae9f6d2
JC
1package resource
2
3import (
107c1cdb
ND
4 "bufio"
5 "bytes"
15c0b25d 6 "errors"
bae9f6d2
JC
7 "fmt"
8 "log"
107c1cdb 9 "sort"
bae9f6d2
JC
10 "strings"
11
107c1cdb
ND
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
15c0b25d 17 "github.com/hashicorp/errwrap"
107c1cdb 18 "github.com/hashicorp/terraform/plans"
bae9f6d2 19 "github.com/hashicorp/terraform/terraform"
107c1cdb 20 "github.com/hashicorp/terraform/tfdiags"
bae9f6d2
JC
21)
22
23// testStepConfig runs a config-mode test step
24func testStepConfig(
25 opts terraform.ContextOpts,
26 state *terraform.State,
27 step TestStep) (*terraform.State, error) {
28 return testStep(opts, state, step)
29}
30
107c1cdb 31func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep) (*terraform.State, error) {
15c0b25d
AP
32 if !step.Destroy {
33 if err := testStepTaint(state, step); err != nil {
34 return state, err
35 }
36 }
37
107c1cdb 38 cfg, err := testConfig(opts, step)
bae9f6d2
JC
39 if err != nil {
40 return state, err
41 }
42
107c1cdb
ND
43 var stepDiags tfdiags.Diagnostics
44
bae9f6d2 45 // Build the context
107c1cdb
ND
46 opts.Config = cfg
47 opts.State, err = terraform.ShimLegacyState(state)
bae9f6d2 48 if err != nil {
107c1cdb
ND
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())
bae9f6d2 56 }
107c1cdb
ND
57 if stepDiags := ctx.Validate(); len(stepDiags) > 0 {
58 if stepDiags.HasErrors() {
59 return state, errwrap.Wrapf("config is invalid: {{err}}", stepDiags.Err())
bae9f6d2 60 }
15c0b25d 61
107c1cdb 62 log.Printf("[WARN] Config warnings:\n%s", stepDiags)
bae9f6d2
JC
63 }
64
65 // Refresh!
107c1cdb
ND
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)
bae9f6d2 70 if err != nil {
107c1cdb
ND
71 return nil, err
72 }
73 if stepDiags.HasErrors() {
74 return state, newOperationError("refresh", stepDiags)
bae9f6d2
JC
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!
107c1cdb
ND
81 if p, stepDiags := ctx.Plan(); stepDiags.HasErrors() {
82 return state, newOperationError("plan", stepDiags)
bae9f6d2 83 } else {
107c1cdb 84 log.Printf("[WARN] Test: Step plan: %s", legacyPlanComparisonString(newState, p.Changes))
bae9f6d2
JC
85 }
86
87 // We need to keep a copy of the state prior to destroying
107c1cdb 88 // such that destroy steps can verify their behavior in the check
bae9f6d2
JC
89 // function
90 stateBeforeApplication := state.DeepCopy()
91
107c1cdb
ND
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)
bae9f6d2 96 if err != nil {
107c1cdb
ND
97 return nil, err
98 }
99 if stepDiags.HasErrors() {
100 return state, newOperationError("apply", stepDiags)
bae9f6d2
JC
101 }
102
107c1cdb 103 // Run any configured checks
bae9f6d2
JC
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.
107c1cdb
ND
119 var p *plans.Plan
120 if p, stepDiags = ctx.Plan(); stepDiags.HasErrors() {
121 return state, newOperationError("follow-up plan", stepDiags)
bae9f6d2 122 }
107c1cdb 123 if !p.Changes.Empty() {
bae9f6d2 124 if step.ExpectNonEmptyPlan {
107c1cdb 125 log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", legacyPlanComparisonString(newState, p.Changes))
bae9f6d2
JC
126 } else {
127 return state, fmt.Errorf(
107c1cdb 128 "After applying this step, the plan was not empty:\n\n%s", legacyPlanComparisonString(newState, p.Changes))
bae9f6d2
JC
129 }
130 }
131
132 // And another after a Refresh.
133 if !step.Destroy || (step.Destroy && !step.PreventPostDestroyRefresh) {
107c1cdb
ND
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)
bae9f6d2 140 if err != nil {
107c1cdb 141 return nil, err
bae9f6d2
JC
142 }
143 }
107c1cdb
ND
144 if p, stepDiags = ctx.Plan(); stepDiags.HasErrors() {
145 return state, newOperationError("second follow-up refresh", stepDiags)
bae9f6d2 146 }
107c1cdb 147 empty := p.Changes.Empty()
bae9f6d2
JC
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.
107c1cdb 155 if step.Destroy && !empty {
bae9f6d2 156 empty = true
107c1cdb
ND
157 for _, change := range p.Changes.Resources {
158 if change.Addr.Resource.Resource.Mode != addrs.DataResourceMode {
159 empty = false
160 break
bae9f6d2
JC
161 }
162 }
163 }
164
165 if !empty {
166 if step.ExpectNonEmptyPlan {
107c1cdb 167 log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", legacyPlanComparisonString(newState, p.Changes))
bae9f6d2
JC
168 } else {
169 return state, fmt.Errorf(
170 "After applying this step and refreshing, "+
107c1cdb 171 "the plan was not empty:\n\n%s", legacyPlanComparisonString(newState, p.Changes))
bae9f6d2
JC
172 }
173 }
174
175 // Made it here, but expected a non-empty plan, fail!
107c1cdb 176 if step.ExpectNonEmptyPlan && empty {
bae9f6d2
JC
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}
15c0b25d 183
107c1cdb
ND
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.
192func 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.
207func 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
15c0b25d
AP
391func 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}