github.com/grange74/terraform@v0.7.0-rc3.0.20160722171430-8c8803864753/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 keys := make([]string, 0, len(d.Attributes)) 88 for key, _ := range d.Attributes { 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.Attributes[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 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 130 "[reset][bold]%s: %s[reset_bold]%s", 131 id, 132 operation, 133 attrString))) 134 135 // Set a timer to show an operation is still happening 136 time.AfterFunc(periodicUiTimer, func() { h.stillApplying(id) }) 137 138 return terraform.HookActionContinue, nil 139 } 140 141 func (h *UiHook) stillApplying(id string) { 142 // Grab the operation. We defer the lock here to avoid the "still..." 143 // message showing up after a completion message. 144 h.l.Lock() 145 defer h.l.Unlock() 146 state, ok := h.resources[id] 147 148 // If the resource is out of the map it means we're done with it 149 if !ok { 150 return 151 } 152 153 var msg string 154 switch state.Op { 155 case uiResourceModify: 156 msg = "Still modifying..." 157 case uiResourceDestroy: 158 msg = "Still destroying..." 159 case uiResourceCreate: 160 msg = "Still creating..." 161 case uiResourceUnknown: 162 return 163 } 164 165 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 166 "[reset][bold]%s: %s (%s elapsed)[reset_bold]", 167 id, 168 msg, 169 time.Now().Round(time.Second).Sub(state.Start), 170 ))) 171 172 // Reschedule 173 time.AfterFunc(periodicUiTimer, func() { h.stillApplying(id) }) 174 } 175 176 func (h *UiHook) PostApply( 177 n *terraform.InstanceInfo, 178 s *terraform.InstanceState, 179 applyerr error) (terraform.HookAction, error) { 180 id := n.HumanId() 181 182 h.l.Lock() 183 state := h.resources[id] 184 delete(h.resources, id) 185 h.l.Unlock() 186 187 var msg string 188 switch state.Op { 189 case uiResourceModify: 190 msg = "Modifications complete" 191 case uiResourceDestroy: 192 msg = "Destruction complete" 193 case uiResourceCreate: 194 msg = "Creation complete" 195 case uiResourceUnknown: 196 return terraform.HookActionContinue, nil 197 } 198 199 if applyerr != nil { 200 // Errors are collected and printed in ApplyCommand, no need to duplicate 201 return terraform.HookActionContinue, nil 202 } 203 204 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 205 "[reset][bold]%s: %s[reset_bold]", 206 id, msg))) 207 208 return terraform.HookActionContinue, nil 209 } 210 211 func (h *UiHook) PreDiff( 212 n *terraform.InstanceInfo, 213 s *terraform.InstanceState) (terraform.HookAction, error) { 214 return terraform.HookActionContinue, nil 215 } 216 217 func (h *UiHook) PreProvision( 218 n *terraform.InstanceInfo, 219 provId string) (terraform.HookAction, error) { 220 id := n.HumanId() 221 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 222 "[reset][bold]%s: Provisioning with '%s'...[reset_bold]", 223 id, provId))) 224 return terraform.HookActionContinue, nil 225 } 226 227 func (h *UiHook) ProvisionOutput( 228 n *terraform.InstanceInfo, 229 provId string, 230 msg string) { 231 id := n.HumanId() 232 var buf bytes.Buffer 233 buf.WriteString(h.Colorize.Color("[reset]")) 234 235 prefix := fmt.Sprintf("%s (%s): ", id, provId) 236 s := bufio.NewScanner(strings.NewReader(msg)) 237 s.Split(scanLines) 238 for s.Scan() { 239 line := strings.TrimRightFunc(s.Text(), unicode.IsSpace) 240 if line != "" { 241 buf.WriteString(fmt.Sprintf("%s%s\n", prefix, line)) 242 } 243 } 244 245 h.ui.Output(strings.TrimSpace(buf.String())) 246 } 247 248 func (h *UiHook) PreRefresh( 249 n *terraform.InstanceInfo, 250 s *terraform.InstanceState) (terraform.HookAction, error) { 251 h.once.Do(h.init) 252 253 id := n.HumanId() 254 255 var stateIdSuffix string 256 // Data resources refresh before they have ids, whereas managed 257 // resources are only refreshed when they have ids. 258 if s.ID != "" { 259 stateIdSuffix = fmt.Sprintf(" (ID: %s)", s.ID) 260 } 261 262 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 263 "[reset][bold]%s: Refreshing state...%s", 264 id, stateIdSuffix))) 265 return terraform.HookActionContinue, nil 266 } 267 268 func (h *UiHook) PreImportState( 269 n *terraform.InstanceInfo, 270 id string) (terraform.HookAction, error) { 271 h.once.Do(h.init) 272 273 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 274 "[reset][bold]%s: Importing from ID %q...", 275 n.HumanId(), id))) 276 return terraform.HookActionContinue, nil 277 } 278 279 func (h *UiHook) PostImportState( 280 n *terraform.InstanceInfo, 281 s []*terraform.InstanceState) (terraform.HookAction, error) { 282 h.once.Do(h.init) 283 284 id := n.HumanId() 285 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 286 "[reset][bold][green]%s: Import complete!", id))) 287 for _, s := range s { 288 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 289 "[reset][green] Imported %s (ID: %s)", 290 s.Ephemeral.Type, s.ID))) 291 } 292 293 return terraform.HookActionContinue, nil 294 } 295 296 func (h *UiHook) init() { 297 if h.Colorize == nil { 298 panic("colorize not given") 299 } 300 301 h.resources = make(map[string]uiResourceState) 302 303 // Wrap the ui so that it is safe for concurrency regardless of the 304 // underlying reader/writer that is in place. 305 h.ui = &cli.ConcurrentUi{Ui: h.Ui} 306 } 307 308 // scanLines is basically copied from the Go standard library except 309 // we've modified it to also fine `\r`. 310 func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) { 311 if atEOF && len(data) == 0 { 312 return 0, nil, nil 313 } 314 if i := bytes.IndexByte(data, '\n'); i >= 0 { 315 // We have a full newline-terminated line. 316 return i + 1, dropCR(data[0:i]), nil 317 } 318 if i := bytes.IndexByte(data, '\r'); i >= 0 { 319 // We have a full newline-terminated line. 320 return i + 1, dropCR(data[0:i]), nil 321 } 322 // If we're at EOF, we have a final, non-terminated line. Return it. 323 if atEOF { 324 return len(data), dropCR(data), nil 325 } 326 // Request more data. 327 return 0, nil, nil 328 } 329 330 // dropCR drops a terminal \r from the data. 331 func dropCR(data []byte) []byte { 332 if len(data) > 0 && data[len(data)-1] == '\r' { 333 return data[0 : len(data)-1] 334 } 335 return data 336 }