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