1package views
2
3import (
4	"bufio"
5	"strings"
6	"sync"
7	"time"
8	"unicode"
9
10	"github.com/hashicorp/terraform/internal/addrs"
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/terraform"
16	"github.com/zclconf/go-cty/cty"
17)
18
19// How long to wait between sending heartbeat/progress messages
20const heartbeatInterval = 10 * time.Second
21
22func newJSONHook(view *JSONView) *jsonHook {
23	return &jsonHook{
24		view:      view,
25		applying:  make(map[string]applyProgress),
26		timeNow:   time.Now,
27		timeAfter: time.After,
28	}
29}
30
31type jsonHook struct {
32	terraform.NilHook
33
34	view *JSONView
35
36	// Concurrent map of resource addresses to allow the sequence of pre-apply,
37	// progress, and post-apply messages to share data about the resource
38	applying     map[string]applyProgress
39	applyingLock sync.Mutex
40
41	// Mockable functions for testing the progress timer goroutine
42	timeNow   func() time.Time
43	timeAfter func(time.Duration) <-chan time.Time
44}
45
46var _ terraform.Hook = (*jsonHook)(nil)
47
48type applyProgress struct {
49	addr   addrs.AbsResourceInstance
50	action plans.Action
51	start  time.Time
52
53	// done is used for post-apply to stop the progress goroutine
54	done chan struct{}
55
56	// heartbeatDone is used to allow tests to safely wait for the progress
57	// goroutine to finish
58	heartbeatDone chan struct{}
59}
60
61func (h *jsonHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) {
62	idKey, idValue := format.ObjectValueIDOrName(priorState)
63	h.view.Hook(json.NewApplyStart(addr, action, idKey, idValue))
64
65	progress := applyProgress{
66		addr:          addr,
67		action:        action,
68		start:         h.timeNow().Round(time.Second),
69		done:          make(chan struct{}),
70		heartbeatDone: make(chan struct{}),
71	}
72	h.applyingLock.Lock()
73	h.applying[addr.String()] = progress
74	h.applyingLock.Unlock()
75
76	go h.applyingHeartbeat(progress)
77	return terraform.HookActionContinue, nil
78}
79
80func (h *jsonHook) applyingHeartbeat(progress applyProgress) {
81	defer close(progress.heartbeatDone)
82	for {
83		select {
84		case <-progress.done:
85			return
86		case <-h.timeAfter(heartbeatInterval):
87		}
88
89		elapsed := h.timeNow().Round(time.Second).Sub(progress.start)
90		h.view.Hook(json.NewApplyProgress(progress.addr, progress.action, elapsed))
91	}
92}
93
94func (h *jsonHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, err error) (terraform.HookAction, error) {
95	key := addr.String()
96	h.applyingLock.Lock()
97	progress := h.applying[key]
98	if progress.done != nil {
99		close(progress.done)
100	}
101	delete(h.applying, key)
102	h.applyingLock.Unlock()
103
104	elapsed := h.timeNow().Round(time.Second).Sub(progress.start)
105
106	if err != nil {
107		// Errors are collected and displayed post-apply, so no need to
108		// re-render them here. Instead just signal that this resource failed
109		// to apply.
110		h.view.Hook(json.NewApplyErrored(addr, progress.action, elapsed))
111	} else {
112		idKey, idValue := format.ObjectValueID(newState)
113		h.view.Hook(json.NewApplyComplete(addr, progress.action, idKey, idValue, elapsed))
114	}
115	return terraform.HookActionContinue, nil
116}
117
118func (h *jsonHook) PreProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string) (terraform.HookAction, error) {
119	h.view.Hook(json.NewProvisionStart(addr, typeName))
120	return terraform.HookActionContinue, nil
121}
122
123func (h *jsonHook) PostProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string, err error) (terraform.HookAction, error) {
124	if err != nil {
125		// Errors are collected and displayed post-apply, so no need to
126		// re-render them here. Instead just signal that this provisioner step
127		// failed.
128		h.view.Hook(json.NewProvisionErrored(addr, typeName))
129	} else {
130		h.view.Hook(json.NewProvisionComplete(addr, typeName))
131	}
132	return terraform.HookActionContinue, nil
133}
134
135func (h *jsonHook) ProvisionOutput(addr addrs.AbsResourceInstance, typeName string, msg string) {
136	s := bufio.NewScanner(strings.NewReader(msg))
137	s.Split(scanLines)
138	for s.Scan() {
139		line := strings.TrimRightFunc(s.Text(), unicode.IsSpace)
140		if line != "" {
141			h.view.Hook(json.NewProvisionProgress(addr, typeName, line))
142		}
143	}
144}
145
146func (h *jsonHook) PreRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value) (terraform.HookAction, error) {
147	idKey, idValue := format.ObjectValueID(priorState)
148	h.view.Hook(json.NewRefreshStart(addr, idKey, idValue))
149	return terraform.HookActionContinue, nil
150}
151
152func (h *jsonHook) PostRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value, newState cty.Value) (terraform.HookAction, error) {
153	idKey, idValue := format.ObjectValueID(newState)
154	h.view.Hook(json.NewRefreshComplete(addr, idKey, idValue))
155	return terraform.HookActionContinue, nil
156}
157