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