github.com/spirius/terraform@v0.10.0-beta2.0.20170714185654-87b2c0cf8fea/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/hashicorp/terraform/terraform" 14 "github.com/mitchellh/cli" 15 "github.com/mitchellh/colorstring" 16 ) 17 18 const defaultPeriodicUiTimer = 10 * time.Second 19 const maxIdLen = 80 20 21 type UiHook struct { 22 terraform.NilHook 23 24 Colorize *colorstring.Colorize 25 Ui cli.Ui 26 PeriodicUiTimer time.Duration 27 28 l sync.Mutex 29 once sync.Once 30 resources map[string]uiResourceState 31 ui cli.Ui 32 } 33 34 // uiResourceState tracks the state of a single resource 35 type uiResourceState struct { 36 Name string 37 ResourceId string 38 Op uiResourceOp 39 Start time.Time 40 41 DoneCh chan struct{} // To be used for cancellation 42 43 done chan struct{} // used to coordinate tests 44 } 45 46 // uiResourceOp is an enum for operations on a resource 47 type uiResourceOp byte 48 49 const ( 50 uiResourceUnknown uiResourceOp = iota 51 uiResourceCreate 52 uiResourceModify 53 uiResourceDestroy 54 ) 55 56 func (h *UiHook) PreApply( 57 n *terraform.InstanceInfo, 58 s *terraform.InstanceState, 59 d *terraform.InstanceDiff) (terraform.HookAction, error) { 60 h.once.Do(h.init) 61 62 // if there's no diff, there's nothing to output 63 if d.Empty() { 64 return terraform.HookActionContinue, nil 65 } 66 67 id := n.HumanId() 68 69 op := uiResourceModify 70 if d.Destroy { 71 op = uiResourceDestroy 72 } else if s.ID == "" { 73 op = uiResourceCreate 74 } 75 76 var operation string 77 switch op { 78 case uiResourceModify: 79 operation = "Modifying..." 80 case uiResourceDestroy: 81 operation = "Destroying..." 82 case uiResourceCreate: 83 operation = "Creating..." 84 case uiResourceUnknown: 85 return terraform.HookActionContinue, nil 86 } 87 88 attrBuf := new(bytes.Buffer) 89 90 // Get all the attributes that are changing, and sort them. Also 91 // determine the longest key so that we can align them all. 92 keyLen := 0 93 94 dAttrs := d.CopyAttributes() 95 keys := make([]string, 0, len(dAttrs)) 96 for key, _ := range dAttrs { 97 // Skip the ID since we do that specially 98 if key == "id" { 99 continue 100 } 101 102 keys = append(keys, key) 103 if len(key) > keyLen { 104 keyLen = len(key) 105 } 106 } 107 sort.Strings(keys) 108 109 // Go through and output each attribute 110 for _, attrK := range keys { 111 attrDiff, _ := d.GetAttribute(attrK) 112 113 v := attrDiff.New 114 u := attrDiff.Old 115 if attrDiff.NewComputed { 116 v = "<computed>" 117 } 118 119 if attrDiff.Sensitive { 120 u = "<sensitive>" 121 v = "<sensitive>" 122 } 123 124 attrBuf.WriteString(fmt.Sprintf( 125 " %s:%s %#v => %#v\n", 126 attrK, 127 strings.Repeat(" ", keyLen-len(attrK)), 128 u, 129 v)) 130 } 131 132 attrString := strings.TrimSpace(attrBuf.String()) 133 if attrString != "" { 134 attrString = "\n " + attrString 135 } 136 137 var stateId, stateIdSuffix string 138 if s != nil && s.ID != "" { 139 stateId = s.ID 140 stateIdSuffix = fmt.Sprintf(" (ID: %s)", truncateId(s.ID, maxIdLen)) 141 } 142 143 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 144 "[reset][bold]%s: %s%s[reset]%s", 145 id, 146 operation, 147 stateIdSuffix, 148 attrString))) 149 150 uiState := uiResourceState{ 151 Name: id, 152 ResourceId: stateId, 153 Op: op, 154 Start: time.Now().Round(time.Second), 155 DoneCh: make(chan struct{}), 156 done: make(chan struct{}), 157 } 158 159 h.l.Lock() 160 h.resources[id] = uiState 161 h.l.Unlock() 162 163 // Start goroutine that shows progress 164 go h.stillApplying(uiState) 165 166 return terraform.HookActionContinue, nil 167 } 168 169 func (h *UiHook) stillApplying(state uiResourceState) { 170 defer close(state.done) 171 for { 172 select { 173 case <-state.DoneCh: 174 return 175 176 case <-time.After(h.PeriodicUiTimer): 177 // Timer up, show status 178 } 179 180 var msg string 181 switch state.Op { 182 case uiResourceModify: 183 msg = "Still modifying..." 184 case uiResourceDestroy: 185 msg = "Still destroying..." 186 case uiResourceCreate: 187 msg = "Still creating..." 188 case uiResourceUnknown: 189 return 190 } 191 192 idSuffix := "" 193 if v := state.ResourceId; v != "" { 194 idSuffix = fmt.Sprintf("ID: %s, ", truncateId(v, maxIdLen)) 195 } 196 197 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 198 "[reset][bold]%s: %s (%s%s elapsed)[reset]", 199 state.Name, 200 msg, 201 idSuffix, 202 time.Now().Round(time.Second).Sub(state.Start), 203 ))) 204 } 205 } 206 207 func (h *UiHook) PostApply( 208 n *terraform.InstanceInfo, 209 s *terraform.InstanceState, 210 applyerr error) (terraform.HookAction, error) { 211 212 id := n.HumanId() 213 214 h.l.Lock() 215 state := h.resources[id] 216 if state.DoneCh != nil { 217 close(state.DoneCh) 218 } 219 220 delete(h.resources, id) 221 h.l.Unlock() 222 223 var stateIdSuffix string 224 if s != nil && s.ID != "" { 225 stateIdSuffix = fmt.Sprintf(" (ID: %s)", truncateId(s.ID, maxIdLen)) 226 } 227 228 var msg string 229 switch state.Op { 230 case uiResourceModify: 231 msg = "Modifications complete" 232 case uiResourceDestroy: 233 msg = "Destruction complete" 234 case uiResourceCreate: 235 msg = "Creation complete" 236 case uiResourceUnknown: 237 return terraform.HookActionContinue, nil 238 } 239 240 if applyerr != nil { 241 // Errors are collected and printed in ApplyCommand, no need to duplicate 242 return terraform.HookActionContinue, nil 243 } 244 245 colorized := h.Colorize.Color(fmt.Sprintf( 246 "[reset][bold]%s: %s%s[reset]", 247 id, msg, stateIdSuffix)) 248 249 h.ui.Output(colorized) 250 251 return terraform.HookActionContinue, nil 252 } 253 254 func (h *UiHook) PreDiff( 255 n *terraform.InstanceInfo, 256 s *terraform.InstanceState) (terraform.HookAction, error) { 257 return terraform.HookActionContinue, nil 258 } 259 260 func (h *UiHook) PreProvision( 261 n *terraform.InstanceInfo, 262 provId string) (terraform.HookAction, error) { 263 id := n.HumanId() 264 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 265 "[reset][bold]%s: Provisioning with '%s'...[reset]", 266 id, provId))) 267 return terraform.HookActionContinue, nil 268 } 269 270 func (h *UiHook) ProvisionOutput( 271 n *terraform.InstanceInfo, 272 provId string, 273 msg string) { 274 id := n.HumanId() 275 var buf bytes.Buffer 276 buf.WriteString(h.Colorize.Color("[reset]")) 277 278 prefix := fmt.Sprintf("%s (%s): ", id, provId) 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( 292 n *terraform.InstanceInfo, 293 s *terraform.InstanceState) (terraform.HookAction, error) { 294 h.once.Do(h.init) 295 296 id := n.HumanId() 297 298 var stateIdSuffix string 299 // Data resources refresh before they have ids, whereas managed 300 // resources are only refreshed when they have ids. 301 if s.ID != "" { 302 stateIdSuffix = fmt.Sprintf(" (ID: %s)", truncateId(s.ID, maxIdLen)) 303 } 304 305 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 306 "[reset][bold]%s: Refreshing state...%s", 307 id, stateIdSuffix))) 308 return terraform.HookActionContinue, nil 309 } 310 311 func (h *UiHook) PreImportState( 312 n *terraform.InstanceInfo, 313 id string) (terraform.HookAction, error) { 314 h.once.Do(h.init) 315 316 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 317 "[reset][bold]%s: Importing from ID %q...", 318 n.HumanId(), id))) 319 return terraform.HookActionContinue, nil 320 } 321 322 func (h *UiHook) PostImportState( 323 n *terraform.InstanceInfo, 324 s []*terraform.InstanceState) (terraform.HookAction, error) { 325 h.once.Do(h.init) 326 327 id := n.HumanId() 328 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 329 "[reset][bold][green]%s: Import complete!", id))) 330 for _, s := range s { 331 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 332 "[reset][green] Imported %s (ID: %s)", 333 s.Ephemeral.Type, s.ID))) 334 } 335 336 return terraform.HookActionContinue, nil 337 } 338 339 func (h *UiHook) init() { 340 if h.Colorize == nil { 341 panic("colorize not given") 342 } 343 if h.PeriodicUiTimer == 0 { 344 h.PeriodicUiTimer = defaultPeriodicUiTimer 345 } 346 347 h.resources = make(map[string]uiResourceState) 348 349 // Wrap the ui so that it is safe for concurrency regardless of the 350 // underlying reader/writer that is in place. 351 h.ui = &cli.ConcurrentUi{Ui: h.Ui} 352 } 353 354 // scanLines is basically copied from the Go standard library except 355 // we've modified it to also fine `\r`. 356 func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) { 357 if atEOF && len(data) == 0 { 358 return 0, nil, nil 359 } 360 if i := bytes.IndexByte(data, '\n'); i >= 0 { 361 // We have a full newline-terminated line. 362 return i + 1, dropCR(data[0:i]), nil 363 } 364 if i := bytes.IndexByte(data, '\r'); i >= 0 { 365 // We have a full newline-terminated line. 366 return i + 1, dropCR(data[0:i]), nil 367 } 368 // If we're at EOF, we have a final, non-terminated line. Return it. 369 if atEOF { 370 return len(data), dropCR(data), nil 371 } 372 // Request more data. 373 return 0, nil, nil 374 } 375 376 // dropCR drops a terminal \r from the data. 377 func dropCR(data []byte) []byte { 378 if len(data) > 0 && data[len(data)-1] == '\r' { 379 return data[0 : len(data)-1] 380 } 381 return data 382 } 383 384 func truncateId(id string, maxLen int) string { 385 totalLength := len(id) 386 if totalLength <= maxLen { 387 return id 388 } 389 if maxLen < 5 { 390 // We don't shorten to less than 5 chars 391 // as that would be pointless with ... (3 chars) 392 maxLen = 5 393 } 394 395 dots := "..." 396 partLen := maxLen / 2 397 398 leftIdx := partLen - 1 399 leftPart := id[0:leftIdx] 400 401 rightIdx := totalLength - partLen - 1 402 403 overlap := maxLen - (partLen*2 + len(dots)) 404 if overlap < 0 { 405 rightIdx -= overlap 406 } 407 408 rightPart := id[rightIdx:] 409 410 return leftPart + dots + rightPart 411 }