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