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