github.com/peterbale/terraform@v0.9.0-beta2.0.20170315142748-5723acd55547/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 = 20 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 44 // uiResourceOp is an enum for operations on a resource 45 type uiResourceOp byte 46 47 const ( 48 uiResourceUnknown uiResourceOp = iota 49 uiResourceCreate 50 uiResourceModify 51 uiResourceDestroy 52 ) 53 54 func (h *UiHook) PreApply( 55 n *terraform.InstanceInfo, 56 s *terraform.InstanceState, 57 d *terraform.InstanceDiff) (terraform.HookAction, error) { 58 h.once.Do(h.init) 59 60 id := n.HumanId() 61 62 op := uiResourceModify 63 if d.Destroy { 64 op = uiResourceDestroy 65 } else if s.ID == "" { 66 op = uiResourceCreate 67 } 68 69 var operation string 70 switch op { 71 case uiResourceModify: 72 operation = "Modifying..." 73 case uiResourceDestroy: 74 operation = "Destroying..." 75 case uiResourceCreate: 76 operation = "Creating..." 77 case uiResourceUnknown: 78 return terraform.HookActionContinue, nil 79 } 80 81 attrBuf := new(bytes.Buffer) 82 83 // Get all the attributes that are changing, and sort them. Also 84 // determine the longest key so that we can align them all. 85 keyLen := 0 86 87 dAttrs := d.CopyAttributes() 88 keys := make([]string, 0, len(dAttrs)) 89 for key, _ := range dAttrs { 90 // Skip the ID since we do that specially 91 if key == "id" { 92 continue 93 } 94 95 keys = append(keys, key) 96 if len(key) > keyLen { 97 keyLen = len(key) 98 } 99 } 100 sort.Strings(keys) 101 102 // Go through and output each attribute 103 for _, attrK := range keys { 104 attrDiff, _ := d.GetAttribute(attrK) 105 106 v := attrDiff.New 107 u := attrDiff.Old 108 if attrDiff.NewComputed { 109 v = "<computed>" 110 } 111 112 if attrDiff.Sensitive { 113 u = "<sensitive>" 114 v = "<sensitive>" 115 } 116 117 attrBuf.WriteString(fmt.Sprintf( 118 " %s:%s %#v => %#v\n", 119 attrK, 120 strings.Repeat(" ", keyLen-len(attrK)), 121 u, 122 v)) 123 } 124 125 attrString := strings.TrimSpace(attrBuf.String()) 126 if attrString != "" { 127 attrString = "\n " + attrString 128 } 129 130 var stateId, stateIdSuffix string 131 if s != nil && s.ID != "" { 132 stateId = s.ID 133 stateIdSuffix = fmt.Sprintf(" (ID: %s)", truncateId(s.ID, maxIdLen)) 134 } 135 136 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 137 "[reset][bold]%s: %s%s[reset]%s", 138 id, 139 operation, 140 stateIdSuffix, 141 attrString))) 142 143 uiState := uiResourceState{ 144 Name: id, 145 ResourceId: stateId, 146 Op: op, 147 Start: time.Now().Round(time.Second), 148 DoneCh: make(chan struct{}), 149 } 150 151 h.l.Lock() 152 h.resources[id] = uiState 153 h.l.Unlock() 154 155 // Start goroutine that shows progress 156 go h.stillApplying(uiState) 157 158 return terraform.HookActionContinue, nil 159 } 160 161 func (h *UiHook) stillApplying(state uiResourceState) { 162 for { 163 select { 164 case <-state.DoneCh: 165 return 166 167 case <-time.After(h.PeriodicUiTimer): 168 // Timer up, show status 169 } 170 171 var msg string 172 switch state.Op { 173 case uiResourceModify: 174 msg = "Still modifying..." 175 case uiResourceDestroy: 176 msg = "Still destroying..." 177 case uiResourceCreate: 178 msg = "Still creating..." 179 case uiResourceUnknown: 180 return 181 } 182 183 idSuffix := "" 184 if v := state.ResourceId; v != "" { 185 idSuffix = fmt.Sprintf("ID: %s, ", truncateId(v, maxIdLen)) 186 } 187 188 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 189 "[reset][bold]%s: %s (%s%s elapsed)[reset]", 190 state.Name, 191 msg, 192 idSuffix, 193 time.Now().Round(time.Second).Sub(state.Start), 194 ))) 195 } 196 } 197 198 func (h *UiHook) PostApply( 199 n *terraform.InstanceInfo, 200 s *terraform.InstanceState, 201 applyerr error) (terraform.HookAction, error) { 202 203 id := n.HumanId() 204 205 h.l.Lock() 206 state := h.resources[id] 207 if state.DoneCh != nil { 208 close(state.DoneCh) 209 } 210 211 delete(h.resources, id) 212 h.l.Unlock() 213 214 var stateIdSuffix string 215 if s != nil && s.ID != "" { 216 stateIdSuffix = fmt.Sprintf(" (ID: %s)", truncateId(s.ID, maxIdLen)) 217 } 218 219 var msg string 220 switch state.Op { 221 case uiResourceModify: 222 msg = "Modifications complete" 223 case uiResourceDestroy: 224 msg = "Destruction complete" 225 case uiResourceCreate: 226 msg = "Creation complete" 227 case uiResourceUnknown: 228 return terraform.HookActionContinue, nil 229 } 230 231 if applyerr != nil { 232 // Errors are collected and printed in ApplyCommand, no need to duplicate 233 return terraform.HookActionContinue, nil 234 } 235 236 colorized := h.Colorize.Color(fmt.Sprintf( 237 "[reset][bold]%s: %s%s[reset]", 238 id, msg, stateIdSuffix)) 239 240 h.ui.Output(colorized) 241 242 return terraform.HookActionContinue, nil 243 } 244 245 func (h *UiHook) PreDiff( 246 n *terraform.InstanceInfo, 247 s *terraform.InstanceState) (terraform.HookAction, error) { 248 return terraform.HookActionContinue, nil 249 } 250 251 func (h *UiHook) PreProvision( 252 n *terraform.InstanceInfo, 253 provId string) (terraform.HookAction, error) { 254 id := n.HumanId() 255 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 256 "[reset][bold]%s: Provisioning with '%s'...[reset]", 257 id, provId))) 258 return terraform.HookActionContinue, nil 259 } 260 261 func (h *UiHook) ProvisionOutput( 262 n *terraform.InstanceInfo, 263 provId string, 264 msg string) { 265 id := n.HumanId() 266 var buf bytes.Buffer 267 buf.WriteString(h.Colorize.Color("[reset]")) 268 269 prefix := fmt.Sprintf("%s (%s): ", id, provId) 270 s := bufio.NewScanner(strings.NewReader(msg)) 271 s.Split(scanLines) 272 for s.Scan() { 273 line := strings.TrimRightFunc(s.Text(), unicode.IsSpace) 274 if line != "" { 275 buf.WriteString(fmt.Sprintf("%s%s\n", prefix, line)) 276 } 277 } 278 279 h.ui.Output(strings.TrimSpace(buf.String())) 280 } 281 282 func (h *UiHook) PreRefresh( 283 n *terraform.InstanceInfo, 284 s *terraform.InstanceState) (terraform.HookAction, error) { 285 h.once.Do(h.init) 286 287 id := n.HumanId() 288 289 var stateIdSuffix string 290 // Data resources refresh before they have ids, whereas managed 291 // resources are only refreshed when they have ids. 292 if s.ID != "" { 293 stateIdSuffix = fmt.Sprintf(" (ID: %s)", truncateId(s.ID, maxIdLen)) 294 } 295 296 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 297 "[reset][bold]%s: Refreshing state...%s", 298 id, stateIdSuffix))) 299 return terraform.HookActionContinue, nil 300 } 301 302 func (h *UiHook) PreImportState( 303 n *terraform.InstanceInfo, 304 id string) (terraform.HookAction, error) { 305 h.once.Do(h.init) 306 307 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 308 "[reset][bold]%s: Importing from ID %q...", 309 n.HumanId(), id))) 310 return terraform.HookActionContinue, nil 311 } 312 313 func (h *UiHook) PostImportState( 314 n *terraform.InstanceInfo, 315 s []*terraform.InstanceState) (terraform.HookAction, error) { 316 h.once.Do(h.init) 317 318 id := n.HumanId() 319 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 320 "[reset][bold][green]%s: Import complete!", id))) 321 for _, s := range s { 322 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 323 "[reset][green] Imported %s (ID: %s)", 324 s.Ephemeral.Type, s.ID))) 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 totalLength := len(id) 377 if totalLength <= maxLen { 378 return id 379 } 380 if maxLen < 5 { 381 // We don't shorten to less than 5 chars 382 // as that would be pointless with ... (3 chars) 383 maxLen = 5 384 } 385 386 dots := "..." 387 partLen := maxLen / 2 388 389 leftIdx := partLen - 1 390 leftPart := id[0:leftIdx] 391 392 rightIdx := totalLength - partLen - 1 393 394 overlap := maxLen - (partLen*2 + len(dots)) 395 if overlap < 0 { 396 rightIdx -= overlap 397 } 398 399 rightPart := id[rightIdx:] 400 401 return leftPart + dots + rightPart 402 }