github.com/jdextraze/terraform@v0.6.17-0.20160511153921-e33847c8a8af/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 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 249 "[reset][bold]%s: Refreshing state... (ID: %s)", 250 id, s.ID))) 251 return terraform.HookActionContinue, nil 252 } 253 254 func (h *UiHook) init() { 255 if h.Colorize == nil { 256 panic("colorize not given") 257 } 258 259 h.resources = make(map[string]uiResourceState) 260 261 // Wrap the ui so that it is safe for concurrency regardless of the 262 // underlying reader/writer that is in place. 263 h.ui = &cli.ConcurrentUi{Ui: h.Ui} 264 } 265 266 // scanLines is basically copied from the Go standard library except 267 // we've modified it to also fine `\r`. 268 func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) { 269 if atEOF && len(data) == 0 { 270 return 0, nil, nil 271 } 272 if i := bytes.IndexByte(data, '\n'); i >= 0 { 273 // We have a full newline-terminated line. 274 return i + 1, dropCR(data[0:i]), nil 275 } 276 if i := bytes.IndexByte(data, '\r'); i >= 0 { 277 // We have a full newline-terminated line. 278 return i + 1, dropCR(data[0:i]), nil 279 } 280 // If we're at EOF, we have a final, non-terminated line. Return it. 281 if atEOF { 282 return len(data), dropCR(data), nil 283 } 284 // Request more data. 285 return 0, nil, nil 286 } 287 288 // dropCR drops a terminal \r from the data. 289 func dropCR(data []byte) []byte { 290 if len(data) > 0 && data[len(data)-1] == '\r' { 291 return data[0 : len(data)-1] 292 } 293 return data 294 }