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