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