1package views
2
3import (
4	"bytes"
5	"fmt"
6	"sort"
7	"strings"
8
9	"github.com/hashicorp/terraform/internal/addrs"
10	"github.com/hashicorp/terraform/internal/command/arguments"
11	"github.com/hashicorp/terraform/internal/command/format"
12	"github.com/hashicorp/terraform/internal/command/views/json"
13	"github.com/hashicorp/terraform/internal/plans"
14	"github.com/hashicorp/terraform/internal/states"
15	"github.com/hashicorp/terraform/internal/states/statefile"
16	"github.com/hashicorp/terraform/internal/terraform"
17	"github.com/hashicorp/terraform/internal/tfdiags"
18	"github.com/zclconf/go-cty/cty"
19)
20
21type Operation interface {
22	Interrupted()
23	FatalInterrupt()
24	Stopping()
25	Cancelled(planMode plans.Mode)
26
27	EmergencyDumpState(stateFile *statefile.File) error
28
29	PlannedChange(change *plans.ResourceInstanceChangeSrc)
30	Plan(plan *plans.Plan, schemas *terraform.Schemas)
31	PlanNextStep(planPath string)
32
33	Diagnostics(diags tfdiags.Diagnostics)
34}
35
36func NewOperation(vt arguments.ViewType, inAutomation bool, view *View) Operation {
37	switch vt {
38	case arguments.ViewHuman:
39		return &OperationHuman{view: view, inAutomation: inAutomation}
40	default:
41		panic(fmt.Sprintf("unknown view type %v", vt))
42	}
43}
44
45type OperationHuman struct {
46	view *View
47
48	// inAutomation indicates that commands are being run by an
49	// automated system rather than directly at a command prompt.
50	//
51	// This is a hint not to produce messages that expect that a user can
52	// run a follow-up command, perhaps because Terraform is running in
53	// some sort of workflow automation tool that abstracts away the
54	// exact commands that are being run.
55	inAutomation bool
56}
57
58var _ Operation = (*OperationHuman)(nil)
59
60func (v *OperationHuman) Interrupted() {
61	v.view.streams.Println(format.WordWrap(interrupted, v.view.outputColumns()))
62}
63
64func (v *OperationHuman) FatalInterrupt() {
65	v.view.streams.Eprintln(format.WordWrap(fatalInterrupt, v.view.errorColumns()))
66}
67
68func (v *OperationHuman) Stopping() {
69	v.view.streams.Println("Stopping operation...")
70}
71
72func (v *OperationHuman) Cancelled(planMode plans.Mode) {
73	switch planMode {
74	case plans.DestroyMode:
75		v.view.streams.Println("Destroy cancelled.")
76	default:
77		v.view.streams.Println("Apply cancelled.")
78	}
79}
80
81func (v *OperationHuman) EmergencyDumpState(stateFile *statefile.File) error {
82	stateBuf := new(bytes.Buffer)
83	jsonErr := statefile.Write(stateFile, stateBuf)
84	if jsonErr != nil {
85		return jsonErr
86	}
87	v.view.streams.Eprintln(stateBuf)
88	return nil
89}
90
91func (v *OperationHuman) Plan(plan *plans.Plan, schemas *terraform.Schemas) {
92	renderPlan(plan, schemas, v.view)
93}
94
95func (v *OperationHuman) PlannedChange(change *plans.ResourceInstanceChangeSrc) {
96	// PlannedChange is primarily for machine-readable output in order to
97	// get a per-resource-instance change description. We don't use it
98	// with OperationHuman because the output of Plan already includes the
99	// change details for all resource instances.
100}
101
102// PlanNextStep gives the user some next-steps, unless we're running in an
103// automation tool which is presumed to provide its own UI for further actions.
104func (v *OperationHuman) PlanNextStep(planPath string) {
105	if v.inAutomation {
106		return
107	}
108	v.view.outputHorizRule()
109
110	if planPath == "" {
111		v.view.streams.Print(
112			"\n" + strings.TrimSpace(format.WordWrap(planHeaderNoOutput, v.view.outputColumns())) + "\n",
113		)
114	} else {
115		v.view.streams.Printf(
116			"\n"+strings.TrimSpace(format.WordWrap(planHeaderYesOutput, v.view.outputColumns()))+"\n",
117			planPath, planPath,
118		)
119	}
120}
121
122func (v *OperationHuman) Diagnostics(diags tfdiags.Diagnostics) {
123	v.view.Diagnostics(diags)
124}
125
126type OperationJSON struct {
127	view *JSONView
128}
129
130var _ Operation = (*OperationJSON)(nil)
131
132func (v *OperationJSON) Interrupted() {
133	v.view.Log(interrupted)
134}
135
136func (v *OperationJSON) FatalInterrupt() {
137	v.view.Log(fatalInterrupt)
138}
139
140func (v *OperationJSON) Stopping() {
141	v.view.Log("Stopping operation...")
142}
143
144func (v *OperationJSON) Cancelled(planMode plans.Mode) {
145	switch planMode {
146	case plans.DestroyMode:
147		v.view.Log("Destroy cancelled")
148	default:
149		v.view.Log("Apply cancelled")
150	}
151}
152
153func (v *OperationJSON) EmergencyDumpState(stateFile *statefile.File) error {
154	stateBuf := new(bytes.Buffer)
155	jsonErr := statefile.Write(stateFile, stateBuf)
156	if jsonErr != nil {
157		return jsonErr
158	}
159	v.view.StateDump(stateBuf.String())
160	return nil
161}
162
163// Log a change summary and a series of "planned" messages for the changes in
164// the plan.
165func (v *OperationJSON) Plan(plan *plans.Plan, schemas *terraform.Schemas) {
166	if err := v.resourceDrift(plan.PrevRunState, plan.PriorState, schemas); err != nil {
167		var diags tfdiags.Diagnostics
168		diags = diags.Append(err)
169		v.Diagnostics(diags)
170	}
171
172	cs := &json.ChangeSummary{
173		Operation: json.OperationPlanned,
174	}
175	for _, change := range plan.Changes.Resources {
176		if change.Action == plans.Delete && change.Addr.Resource.Resource.Mode == addrs.DataResourceMode {
177			// Avoid rendering data sources on deletion
178			continue
179		}
180		switch change.Action {
181		case plans.Create:
182			cs.Add++
183		case plans.Delete:
184			cs.Remove++
185		case plans.Update:
186			cs.Change++
187		case plans.CreateThenDelete, plans.DeleteThenCreate:
188			cs.Add++
189			cs.Remove++
190		}
191
192		if change.Action != plans.NoOp {
193			v.view.PlannedChange(json.NewResourceInstanceChange(change))
194		}
195	}
196
197	v.view.ChangeSummary(cs)
198
199	var rootModuleOutputs []*plans.OutputChangeSrc
200	for _, output := range plan.Changes.Outputs {
201		if !output.Addr.Module.IsRoot() {
202			continue
203		}
204		rootModuleOutputs = append(rootModuleOutputs, output)
205	}
206	if len(rootModuleOutputs) > 0 {
207		v.view.Outputs(json.OutputsFromChanges(rootModuleOutputs))
208	}
209}
210
211func (v *OperationJSON) resourceDrift(oldState, newState *states.State, schemas *terraform.Schemas) error {
212	if newState.ManagedResourcesEqual(oldState) {
213		// Nothing to do, because we only detect and report drift for managed
214		// resource instances.
215		return nil
216	}
217	var changes []*json.ResourceInstanceChange
218	for _, ms := range oldState.Modules {
219		for _, rs := range ms.Resources {
220			if rs.Addr.Resource.Mode != addrs.ManagedResourceMode {
221				// Drift reporting is only for managed resources
222				continue
223			}
224
225			provider := rs.ProviderConfig.Provider
226			for key, oldIS := range rs.Instances {
227				if oldIS.Current == nil {
228					// Not interested in instances that only have deposed objects
229					continue
230				}
231				addr := rs.Addr.Instance(key)
232				newIS := newState.ResourceInstance(addr)
233
234				schema, _ := schemas.ResourceTypeConfig(
235					provider,
236					addr.Resource.Resource.Mode,
237					addr.Resource.Resource.Type,
238				)
239				if schema == nil {
240					return fmt.Errorf("no schema found for %s (in provider %s)", addr, provider)
241				}
242				ty := schema.ImpliedType()
243
244				oldObj, err := oldIS.Current.Decode(ty)
245				if err != nil {
246					return fmt.Errorf("failed to decode previous run data for %s: %s", addr, err)
247				}
248
249				var newObj *states.ResourceInstanceObject
250				if newIS != nil && newIS.Current != nil {
251					newObj, err = newIS.Current.Decode(ty)
252					if err != nil {
253						return fmt.Errorf("failed to decode refreshed data for %s: %s", addr, err)
254					}
255				}
256
257				var oldVal, newVal cty.Value
258				oldVal = oldObj.Value
259				if newObj != nil {
260					newVal = newObj.Value
261				} else {
262					newVal = cty.NullVal(ty)
263				}
264
265				if oldVal.RawEquals(newVal) {
266					// No drift if the two values are semantically equivalent
267					continue
268				}
269
270				// We can only detect updates and deletes as drift.
271				action := plans.Update
272				if newVal.IsNull() {
273					action = plans.Delete
274				}
275
276				change := &plans.ResourceInstanceChangeSrc{
277					Addr: addr,
278					ChangeSrc: plans.ChangeSrc{
279						Action: action,
280					},
281				}
282				changes = append(changes, json.NewResourceInstanceChange(change))
283			}
284		}
285	}
286
287	// Sort the change structs lexically by address to give stable output
288	sort.Slice(changes, func(i, j int) bool { return changes[i].Resource.Addr < changes[j].Resource.Addr })
289
290	for _, change := range changes {
291		v.view.ResourceDrift(change)
292	}
293
294	return nil
295}
296
297func (v *OperationJSON) PlannedChange(change *plans.ResourceInstanceChangeSrc) {
298	if change.Action == plans.Delete && change.Addr.Resource.Resource.Mode == addrs.DataResourceMode {
299		// Avoid rendering data sources on deletion
300		return
301	}
302	v.view.PlannedChange(json.NewResourceInstanceChange(change))
303}
304
305// PlanNextStep does nothing for the JSON view as it is a hook for user-facing
306// output only applicable to human-readable UI.
307func (v *OperationJSON) PlanNextStep(planPath string) {
308}
309
310func (v *OperationJSON) Diagnostics(diags tfdiags.Diagnostics) {
311	v.view.Diagnostics(diags)
312}
313
314const fatalInterrupt = `
315Two interrupts received. Exiting immediately. Note that data loss may have occurred.
316`
317
318const interrupted = `
319Interrupt received.
320Please wait for Terraform to exit or data loss may occur.
321Gracefully shutting down...
322`
323
324const planHeaderNoOutput = `
325Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
326`
327
328const planHeaderYesOutput = `
329Saved the plan to: %s
330
331To perform exactly these actions, run the following command to apply:
332    terraform apply %q
333`
334