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