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