]>
Commit | Line | Data |
---|---|---|
bae9f6d2 JC |
1 | package resource |
2 | ||
3 | import ( | |
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 | |
23 | func testStepConfig( | |
24 | opts terraform.ContextOpts, | |
25 | state *terraform.State, | |
26 | step TestStep) (*terraform.State, error) { | |
27 | return testStep(opts, state, step) | |
28 | } | |
29 | ||
107c1cdb | 30 | func 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. | |
191 | func 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. | |
206 | func 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 |
390 | func 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 | } |