github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/command/hook_ui.go (about) 1 package command 2 3 import ( 4 "bufio" 5 "bytes" 6 "fmt" 7 "sort" 8 "strings" 9 "sync" 10 "time" 11 "unicode" 12 13 "github.com/mitchellh/cli" 14 "github.com/mitchellh/colorstring" 15 "github.com/zclconf/go-cty/cty" 16 17 "github.com/hashicorp/terraform/addrs" 18 "github.com/hashicorp/terraform/command/format" 19 "github.com/hashicorp/terraform/plans" 20 "github.com/hashicorp/terraform/providers" 21 "github.com/hashicorp/terraform/states" 22 "github.com/hashicorp/terraform/terraform" 23 ) 24 25 const defaultPeriodicUiTimer = 10 * time.Second 26 const maxIdLen = 80 27 28 type UiHook struct { 29 terraform.NilHook 30 31 Colorize *colorstring.Colorize 32 Ui cli.Ui 33 PeriodicUiTimer time.Duration 34 35 l sync.Mutex 36 once sync.Once 37 resources map[string]uiResourceState 38 ui cli.Ui 39 } 40 41 var _ terraform.Hook = (*UiHook)(nil) 42 43 // uiResourceState tracks the state of a single resource 44 type uiResourceState struct { 45 DispAddr string 46 IDKey, IDValue string 47 Op uiResourceOp 48 Start time.Time 49 50 DoneCh chan struct{} // To be used for cancellation 51 52 done chan struct{} // used to coordinate tests 53 } 54 55 // uiResourceOp is an enum for operations on a resource 56 type uiResourceOp byte 57 58 const ( 59 uiResourceUnknown uiResourceOp = iota 60 uiResourceCreate 61 uiResourceModify 62 uiResourceDestroy 63 ) 64 65 func (h *UiHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) { 66 h.once.Do(h.init) 67 68 dispAddr := addr.String() 69 if gen != states.CurrentGen { 70 dispAddr = fmt.Sprintf("%s (%s)", dispAddr, gen) 71 } 72 73 var operation string 74 var op uiResourceOp 75 idKey, idValue := format.ObjectValueIDOrName(priorState) 76 switch action { 77 case plans.Delete: 78 operation = "Destroying..." 79 op = uiResourceDestroy 80 case plans.Create: 81 operation = "Creating..." 82 op = uiResourceCreate 83 case plans.Update: 84 operation = "Modifying..." 85 op = uiResourceModify 86 default: 87 // We don't expect any other actions in here, so anything else is a 88 // bug in the caller but we'll ignore it in order to be robust. 89 h.ui.Output(fmt.Sprintf("(Unknown action %s for %s)", action, dispAddr)) 90 return terraform.HookActionContinue, nil 91 } 92 93 attrBuf := new(bytes.Buffer) 94 95 // Get all the attributes that are changing, and sort them. Also 96 // determine the longest key so that we can align them all. 97 keyLen := 0 98 99 // FIXME: This is stubbed out in preparation for rewriting it to use 100 // a structural presentation rather than the old-style flatmap one. 101 // We just assume no attributes at all for now, pending new code to 102 // work with the two cty.Values we are given. 103 dAttrs := map[string]terraform.ResourceAttrDiff{} 104 keys := make([]string, 0, len(dAttrs)) 105 for key, _ := range dAttrs { 106 keys = append(keys, key) 107 if len(key) > keyLen { 108 keyLen = len(key) 109 } 110 } 111 sort.Strings(keys) 112 113 // Go through and output each attribute 114 for _, attrK := range keys { 115 attrDiff := dAttrs[attrK] 116 117 v := attrDiff.New 118 u := attrDiff.Old 119 if attrDiff.NewComputed { 120 v = "<computed>" 121 } 122 123 if attrDiff.Sensitive { 124 u = "<sensitive>" 125 v = "<sensitive>" 126 } 127 128 attrBuf.WriteString(fmt.Sprintf( 129 " %s:%s %#v => %#v\n", 130 attrK, 131 strings.Repeat(" ", keyLen-len(attrK)), 132 u, 133 v)) 134 } 135 136 attrString := strings.TrimSpace(attrBuf.String()) 137 if attrString != "" { 138 attrString = "\n " + attrString 139 } 140 141 var stateIdSuffix string 142 if idKey != "" && idValue != "" { 143 stateIdSuffix = fmt.Sprintf(" [%s=%s]", idKey, idValue) 144 } else { 145 // Make sure they are both empty so we can deal with this more 146 // easily in the other hook methods. 147 idKey = "" 148 idValue = "" 149 } 150 151 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 152 "[reset][bold]%s: %s%s[reset]%s", 153 dispAddr, 154 operation, 155 stateIdSuffix, 156 attrString, 157 ))) 158 159 key := addr.String() 160 uiState := uiResourceState{ 161 DispAddr: key, 162 IDKey: idKey, 163 IDValue: idValue, 164 Op: op, 165 Start: time.Now().Round(time.Second), 166 DoneCh: make(chan struct{}), 167 done: make(chan struct{}), 168 } 169 170 h.l.Lock() 171 h.resources[key] = uiState 172 h.l.Unlock() 173 174 // Start goroutine that shows progress 175 go h.stillApplying(uiState) 176 177 return terraform.HookActionContinue, nil 178 } 179 180 func (h *UiHook) stillApplying(state uiResourceState) { 181 defer close(state.done) 182 for { 183 select { 184 case <-state.DoneCh: 185 return 186 187 case <-time.After(h.PeriodicUiTimer): 188 // Timer up, show status 189 } 190 191 var msg string 192 switch state.Op { 193 case uiResourceModify: 194 msg = "Still modifying..." 195 case uiResourceDestroy: 196 msg = "Still destroying..." 197 case uiResourceCreate: 198 msg = "Still creating..." 199 case uiResourceUnknown: 200 return 201 } 202 203 idSuffix := "" 204 if state.IDKey != "" { 205 idSuffix = fmt.Sprintf("%s=%s, ", state.IDKey, truncateId(state.IDValue, maxIdLen)) 206 } 207 208 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 209 "[reset][bold]%s: %s [%s%s elapsed][reset]", 210 state.DispAddr, 211 msg, 212 idSuffix, 213 time.Now().Round(time.Second).Sub(state.Start), 214 ))) 215 } 216 } 217 218 func (h *UiHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, applyerr error) (terraform.HookAction, error) { 219 220 id := addr.String() 221 222 h.l.Lock() 223 state := h.resources[id] 224 if state.DoneCh != nil { 225 close(state.DoneCh) 226 } 227 228 delete(h.resources, id) 229 h.l.Unlock() 230 231 var stateIdSuffix string 232 if k, v := format.ObjectValueID(newState); k != "" && v != "" { 233 stateIdSuffix = fmt.Sprintf(" [%s=%s]", k, v) 234 } 235 236 var msg string 237 switch state.Op { 238 case uiResourceModify: 239 msg = "Modifications complete" 240 case uiResourceDestroy: 241 msg = "Destruction complete" 242 case uiResourceCreate: 243 msg = "Creation complete" 244 case uiResourceUnknown: 245 return terraform.HookActionContinue, nil 246 } 247 248 if applyerr != nil { 249 // Errors are collected and printed in ApplyCommand, no need to duplicate 250 return terraform.HookActionContinue, nil 251 } 252 253 colorized := h.Colorize.Color(fmt.Sprintf( 254 "[reset][bold]%s: %s after %s%s[reset]", 255 addr, msg, time.Now().Round(time.Second).Sub(state.Start), stateIdSuffix)) 256 257 h.ui.Output(colorized) 258 259 return terraform.HookActionContinue, nil 260 } 261 262 func (h *UiHook) PreDiff(addr addrs.AbsResourceInstance, gen states.Generation, priorState, proposedNewState cty.Value) (terraform.HookAction, error) { 263 return terraform.HookActionContinue, nil 264 } 265 266 func (h *UiHook) PreProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string) (terraform.HookAction, error) { 267 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 268 "[reset][bold]%s: Provisioning with '%s'...[reset]", 269 addr, typeName, 270 ))) 271 return terraform.HookActionContinue, nil 272 } 273 274 func (h *UiHook) ProvisionOutput(addr addrs.AbsResourceInstance, typeName string, msg string) { 275 var buf bytes.Buffer 276 buf.WriteString(h.Colorize.Color("[reset]")) 277 278 prefix := fmt.Sprintf("%s (%s): ", addr, typeName) 279 s := bufio.NewScanner(strings.NewReader(msg)) 280 s.Split(scanLines) 281 for s.Scan() { 282 line := strings.TrimRightFunc(s.Text(), unicode.IsSpace) 283 if line != "" { 284 buf.WriteString(fmt.Sprintf("%s%s\n", prefix, line)) 285 } 286 } 287 288 h.ui.Output(strings.TrimSpace(buf.String())) 289 } 290 291 func (h *UiHook) PreRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value) (terraform.HookAction, error) { 292 h.once.Do(h.init) 293 294 var stateIdSuffix string 295 if k, v := format.ObjectValueID(priorState); k != "" && v != "" { 296 stateIdSuffix = fmt.Sprintf(" [%s=%s]", k, v) 297 } 298 299 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 300 "[reset][bold]%s: Refreshing state...%s", 301 addr, stateIdSuffix))) 302 return terraform.HookActionContinue, nil 303 } 304 305 func (h *UiHook) PreImportState(addr addrs.AbsResourceInstance, importID string) (terraform.HookAction, error) { 306 h.once.Do(h.init) 307 308 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 309 "[reset][bold]%s: Importing from ID %q...", 310 addr, importID, 311 ))) 312 return terraform.HookActionContinue, nil 313 } 314 315 func (h *UiHook) PostImportState(addr addrs.AbsResourceInstance, imported []providers.ImportedResource) (terraform.HookAction, error) { 316 h.once.Do(h.init) 317 318 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 319 "[reset][bold][green]%s: Import prepared!", addr))) 320 for _, s := range imported { 321 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 322 "[reset][green] Prepared %s for import", 323 s.TypeName, 324 ))) 325 } 326 327 return terraform.HookActionContinue, nil 328 } 329 330 func (h *UiHook) init() { 331 if h.Colorize == nil { 332 panic("colorize not given") 333 } 334 if h.PeriodicUiTimer == 0 { 335 h.PeriodicUiTimer = defaultPeriodicUiTimer 336 } 337 338 h.resources = make(map[string]uiResourceState) 339 340 // Wrap the ui so that it is safe for concurrency regardless of the 341 // underlying reader/writer that is in place. 342 h.ui = &cli.ConcurrentUi{Ui: h.Ui} 343 } 344 345 // scanLines is basically copied from the Go standard library except 346 // we've modified it to also fine `\r`. 347 func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) { 348 if atEOF && len(data) == 0 { 349 return 0, nil, nil 350 } 351 if i := bytes.IndexByte(data, '\n'); i >= 0 { 352 // We have a full newline-terminated line. 353 return i + 1, dropCR(data[0:i]), nil 354 } 355 if i := bytes.IndexByte(data, '\r'); i >= 0 { 356 // We have a full newline-terminated line. 357 return i + 1, dropCR(data[0:i]), nil 358 } 359 // If we're at EOF, we have a final, non-terminated line. Return it. 360 if atEOF { 361 return len(data), dropCR(data), nil 362 } 363 // Request more data. 364 return 0, nil, nil 365 } 366 367 // dropCR drops a terminal \r from the data. 368 func dropCR(data []byte) []byte { 369 if len(data) > 0 && data[len(data)-1] == '\r' { 370 return data[0 : len(data)-1] 371 } 372 return data 373 } 374 375 func truncateId(id string, maxLen int) string { 376 // Note that the id may contain multibyte characters. 377 // We need to truncate it to maxLen characters, not maxLen bytes. 378 rid := []rune(id) 379 totalLength := len(rid) 380 if totalLength <= maxLen { 381 return id 382 } 383 if maxLen < 5 { 384 // We don't shorten to less than 5 chars 385 // as that would be pointless with ... (3 chars) 386 maxLen = 5 387 } 388 389 dots := []rune("...") 390 partLen := maxLen / 2 391 392 leftIdx := partLen - 1 393 leftPart := rid[0:leftIdx] 394 395 rightIdx := totalLength - partLen - 1 396 397 overlap := maxLen - (partLen*2 + len(dots)) 398 if overlap < 0 { 399 rightIdx -= overlap 400 } 401 402 rightPart := rid[rightIdx:] 403 404 return string(leftPart) + string(dots) + string(rightPart) 405 }