]>
Commit | Line | Data |
---|---|---|
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 | } |