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