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