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