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