1package views
2
3import (
4	"bufio"
5	"bytes"
6	"fmt"
7	"strings"
8	"sync"
9	"time"
10	"unicode"
11
12	"github.com/zclconf/go-cty/cty"
13
14	"github.com/hashicorp/terraform/internal/addrs"
15	"github.com/hashicorp/terraform/internal/command/format"
16	"github.com/hashicorp/terraform/internal/plans"
17	"github.com/hashicorp/terraform/internal/providers"
18	"github.com/hashicorp/terraform/internal/states"
19	"github.com/hashicorp/terraform/internal/terraform"
20)
21
22const defaultPeriodicUiTimer = 10 * time.Second
23const maxIdLen = 80
24
25func NewUiHook(view *View) *UiHook {
26	return &UiHook{
27		view:            view,
28		periodicUiTimer: defaultPeriodicUiTimer,
29		resources:       make(map[string]uiResourceState),
30	}
31}
32
33type UiHook struct {
34	terraform.NilHook
35
36	view     *View
37	viewLock sync.Mutex
38
39	periodicUiTimer time.Duration
40
41	resources     map[string]uiResourceState
42	resourcesLock sync.Mutex
43}
44
45var _ terraform.Hook = (*UiHook)(nil)
46
47// uiResourceState tracks the state of a single resource
48type uiResourceState struct {
49	DispAddr       string
50	IDKey, IDValue string
51	Op             uiResourceOp
52	Start          time.Time
53
54	DoneCh chan struct{} // To be used for cancellation
55
56	done chan struct{} // used to coordinate tests
57}
58
59// uiResourceOp is an enum for operations on a resource
60type uiResourceOp byte
61
62const (
63	uiResourceUnknown uiResourceOp = iota
64	uiResourceCreate
65	uiResourceModify
66	uiResourceDestroy
67	uiResourceRead
68)
69
70func (h *UiHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) {
71	dispAddr := addr.String()
72	if gen != states.CurrentGen {
73		dispAddr = fmt.Sprintf("%s (deposed object %s)", dispAddr, gen)
74	}
75
76	var operation string
77	var op uiResourceOp
78	idKey, idValue := format.ObjectValueIDOrName(priorState)
79	switch action {
80	case plans.Delete:
81		operation = "Destroying..."
82		op = uiResourceDestroy
83	case plans.Create:
84		operation = "Creating..."
85		op = uiResourceCreate
86	case plans.Update:
87		operation = "Modifying..."
88		op = uiResourceModify
89	case plans.Read:
90		operation = "Reading..."
91		op = uiResourceRead
92	default:
93		// We don't expect any other actions in here, so anything else is a
94		// bug in the caller but we'll ignore it in order to be robust.
95		h.println(fmt.Sprintf("(Unknown action %s for %s)", action, dispAddr))
96		return terraform.HookActionContinue, nil
97	}
98
99	var stateIdSuffix string
100	if idKey != "" && idValue != "" {
101		stateIdSuffix = fmt.Sprintf(" [%s=%s]", idKey, idValue)
102	} else {
103		// Make sure they are both empty so we can deal with this more
104		// easily in the other hook methods.
105		idKey = ""
106		idValue = ""
107	}
108
109	h.println(fmt.Sprintf(
110		h.view.colorize.Color("[reset][bold]%s: %s%s[reset]"),
111		dispAddr,
112		operation,
113		stateIdSuffix,
114	))
115
116	key := addr.String()
117	uiState := uiResourceState{
118		DispAddr: key,
119		IDKey:    idKey,
120		IDValue:  idValue,
121		Op:       op,
122		Start:    time.Now().Round(time.Second),
123		DoneCh:   make(chan struct{}),
124		done:     make(chan struct{}),
125	}
126
127	h.resourcesLock.Lock()
128	h.resources[key] = uiState
129	h.resourcesLock.Unlock()
130
131	// Start goroutine that shows progress
132	go h.stillApplying(uiState)
133
134	return terraform.HookActionContinue, nil
135}
136
137func (h *UiHook) stillApplying(state uiResourceState) {
138	defer close(state.done)
139	for {
140		select {
141		case <-state.DoneCh:
142			return
143
144		case <-time.After(h.periodicUiTimer):
145			// Timer up, show status
146		}
147
148		var msg string
149		switch state.Op {
150		case uiResourceModify:
151			msg = "Still modifying..."
152		case uiResourceDestroy:
153			msg = "Still destroying..."
154		case uiResourceCreate:
155			msg = "Still creating..."
156		case uiResourceRead:
157			msg = "Still reading..."
158		case uiResourceUnknown:
159			return
160		}
161
162		idSuffix := ""
163		if state.IDKey != "" {
164			idSuffix = fmt.Sprintf("%s=%s, ", state.IDKey, truncateId(state.IDValue, maxIdLen))
165		}
166
167		h.println(fmt.Sprintf(
168			h.view.colorize.Color("[reset][bold]%s: %s [%s%s elapsed][reset]"),
169			state.DispAddr,
170			msg,
171			idSuffix,
172			time.Now().Round(time.Second).Sub(state.Start),
173		))
174	}
175}
176
177func (h *UiHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, applyerr error) (terraform.HookAction, error) {
178	id := addr.String()
179
180	h.resourcesLock.Lock()
181	state := h.resources[id]
182	if state.DoneCh != nil {
183		close(state.DoneCh)
184	}
185
186	delete(h.resources, id)
187	h.resourcesLock.Unlock()
188
189	var stateIdSuffix string
190	if k, v := format.ObjectValueID(newState); k != "" && v != "" {
191		stateIdSuffix = fmt.Sprintf(" [%s=%s]", k, v)
192	}
193
194	var msg string
195	switch state.Op {
196	case uiResourceModify:
197		msg = "Modifications complete"
198	case uiResourceDestroy:
199		msg = "Destruction complete"
200	case uiResourceCreate:
201		msg = "Creation complete"
202	case uiResourceRead:
203		msg = "Read complete"
204	case uiResourceUnknown:
205		return terraform.HookActionContinue, nil
206	}
207
208	if applyerr != nil {
209		// Errors are collected and printed in ApplyCommand, no need to duplicate
210		return terraform.HookActionContinue, nil
211	}
212
213	addrStr := addr.String()
214	if depKey, ok := gen.(states.DeposedKey); ok {
215		addrStr = fmt.Sprintf("%s (deposed object %s)", addrStr, depKey)
216	}
217
218	colorized := fmt.Sprintf(
219		h.view.colorize.Color("[reset][bold]%s: %s after %s%s"),
220		addrStr, msg, time.Now().Round(time.Second).Sub(state.Start), stateIdSuffix)
221
222	h.println(colorized)
223
224	return terraform.HookActionContinue, nil
225}
226
227func (h *UiHook) PreProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string) (terraform.HookAction, error) {
228	h.println(fmt.Sprintf(
229		h.view.colorize.Color("[reset][bold]%s: Provisioning with '%s'...[reset]"),
230		addr, typeName,
231	))
232	return terraform.HookActionContinue, nil
233}
234
235func (h *UiHook) ProvisionOutput(addr addrs.AbsResourceInstance, typeName string, msg string) {
236	var buf bytes.Buffer
237
238	prefix := fmt.Sprintf(
239		h.view.colorize.Color("[reset][bold]%s (%s):[reset] "),
240		addr, typeName,
241	)
242	s := bufio.NewScanner(strings.NewReader(msg))
243	s.Split(scanLines)
244	for s.Scan() {
245		line := strings.TrimRightFunc(s.Text(), unicode.IsSpace)
246		if line != "" {
247			buf.WriteString(fmt.Sprintf("%s%s\n", prefix, line))
248		}
249	}
250
251	h.println(strings.TrimSpace(buf.String()))
252}
253
254func (h *UiHook) PreRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value) (terraform.HookAction, error) {
255	var stateIdSuffix string
256	if k, v := format.ObjectValueID(priorState); k != "" && v != "" {
257		stateIdSuffix = fmt.Sprintf(" [%s=%s]", k, v)
258	}
259
260	addrStr := addr.String()
261	if depKey, ok := gen.(states.DeposedKey); ok {
262		addrStr = fmt.Sprintf("%s (deposed object %s)", addrStr, depKey)
263	}
264
265	h.println(fmt.Sprintf(
266		h.view.colorize.Color("[reset][bold]%s: Refreshing state...%s"),
267		addrStr, stateIdSuffix))
268	return terraform.HookActionContinue, nil
269}
270
271func (h *UiHook) PreImportState(addr addrs.AbsResourceInstance, importID string) (terraform.HookAction, error) {
272	h.println(fmt.Sprintf(
273		h.view.colorize.Color("[reset][bold]%s: Importing from ID %q..."),
274		addr, importID,
275	))
276	return terraform.HookActionContinue, nil
277}
278
279func (h *UiHook) PostImportState(addr addrs.AbsResourceInstance, imported []providers.ImportedResource) (terraform.HookAction, error) {
280	h.println(fmt.Sprintf(
281		h.view.colorize.Color("[reset][bold][green]%s: Import prepared!"),
282		addr,
283	))
284	for _, s := range imported {
285		h.println(fmt.Sprintf(
286			h.view.colorize.Color("[reset][green]  Prepared %s for import"),
287			s.TypeName,
288		))
289	}
290
291	return terraform.HookActionContinue, nil
292}
293
294// Wrap calls to the view so that concurrent calls do not interleave println.
295func (h *UiHook) println(s string) {
296	h.viewLock.Lock()
297	defer h.viewLock.Unlock()
298	h.view.streams.Println(s)
299}
300
301// scanLines is basically copied from the Go standard library except
302// we've modified it to also fine `\r`.
303func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
304	if atEOF && len(data) == 0 {
305		return 0, nil, nil
306	}
307	if i := bytes.IndexByte(data, '\n'); i >= 0 {
308		// We have a full newline-terminated line.
309		return i + 1, dropCR(data[0:i]), nil
310	}
311	if i := bytes.IndexByte(data, '\r'); i >= 0 {
312		// We have a full carriage-return-terminated line.
313		return i + 1, dropCR(data[0:i]), nil
314	}
315	// If we're at EOF, we have a final, non-terminated line. Return it.
316	if atEOF {
317		return len(data), dropCR(data), nil
318	}
319	// Request more data.
320	return 0, nil, nil
321}
322
323// dropCR drops a terminal \r from the data.
324func dropCR(data []byte) []byte {
325	if len(data) > 0 && data[len(data)-1] == '\r' {
326		return data[0 : len(data)-1]
327	}
328	return data
329}
330
331func truncateId(id string, maxLen int) string {
332	// Note that the id may contain multibyte characters.
333	// We need to truncate it to maxLen characters, not maxLen bytes.
334	rid := []rune(id)
335	totalLength := len(rid)
336	if totalLength <= maxLen {
337		return id
338	}
339	if maxLen < 5 {
340		// We don't shorten to less than 5 chars
341		// as that would be pointless with ... (3 chars)
342		maxLen = 5
343	}
344
345	dots := []rune("...")
346	partLen := maxLen / 2
347
348	leftIdx := partLen - 1
349	leftPart := rid[0:leftIdx]
350
351	rightIdx := totalLength - partLen - 1
352
353	overlap := maxLen - (partLen*2 + len(dots))
354	if overlap < 0 {
355		rightIdx -= overlap
356	}
357
358	rightPart := rid[rightIdx:]
359
360	return string(leftPart) + string(dots) + string(rightPart)
361}
362