github.com/aznashwan/terraform@v0.4.3-0.20151118032030-21f93ca4558d/command/hook_ui.go (about) 1 package command 2 3 import ( 4 "bufio" 5 "bytes" 6 "fmt" 7 "sort" 8 "strings" 9 "sync" 10 "unicode" 11 12 "github.com/hashicorp/terraform/terraform" 13 "github.com/mitchellh/cli" 14 "github.com/mitchellh/colorstring" 15 ) 16 17 type UiHook struct { 18 terraform.NilHook 19 20 Colorize *colorstring.Colorize 21 Ui cli.Ui 22 23 l sync.Mutex 24 once sync.Once 25 resources map[string]uiResourceOp 26 ui cli.Ui 27 } 28 29 type uiResourceOp byte 30 31 const ( 32 uiResourceUnknown uiResourceOp = iota 33 uiResourceCreate 34 uiResourceModify 35 uiResourceDestroy 36 ) 37 38 func (h *UiHook) PreApply( 39 n *terraform.InstanceInfo, 40 s *terraform.InstanceState, 41 d *terraform.InstanceDiff) (terraform.HookAction, error) { 42 h.once.Do(h.init) 43 44 id := n.HumanId() 45 46 op := uiResourceModify 47 if d.Destroy { 48 op = uiResourceDestroy 49 } else if s.ID == "" { 50 op = uiResourceCreate 51 } 52 53 h.l.Lock() 54 h.resources[id] = op 55 h.l.Unlock() 56 57 var operation string 58 switch op { 59 case uiResourceModify: 60 operation = "Modifying..." 61 case uiResourceDestroy: 62 operation = "Destroying..." 63 case uiResourceCreate: 64 operation = "Creating..." 65 case uiResourceUnknown: 66 return terraform.HookActionContinue, nil 67 } 68 69 attrBuf := new(bytes.Buffer) 70 71 // Get all the attributes that are changing, and sort them. Also 72 // determine the longest key so that we can align them all. 73 keyLen := 0 74 keys := make([]string, 0, len(d.Attributes)) 75 for key, _ := range d.Attributes { 76 // Skip the ID since we do that specially 77 if key == "id" { 78 continue 79 } 80 81 keys = append(keys, key) 82 if len(key) > keyLen { 83 keyLen = len(key) 84 } 85 } 86 sort.Strings(keys) 87 88 // Go through and output each attribute 89 for _, attrK := range keys { 90 attrDiff := d.Attributes[attrK] 91 92 v := attrDiff.New 93 if attrDiff.NewComputed { 94 v = "<computed>" 95 } 96 97 attrBuf.WriteString(fmt.Sprintf( 98 " %s:%s %#v => %#v\n", 99 attrK, 100 strings.Repeat(" ", keyLen-len(attrK)), 101 attrDiff.Old, 102 v)) 103 } 104 105 attrString := strings.TrimSpace(attrBuf.String()) 106 if attrString != "" { 107 attrString = "\n " + attrString 108 } 109 110 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 111 "[reset][bold]%s: %s[reset_bold]%s", 112 id, 113 operation, 114 attrString))) 115 116 return terraform.HookActionContinue, nil 117 } 118 119 func (h *UiHook) PostApply( 120 n *terraform.InstanceInfo, 121 s *terraform.InstanceState, 122 applyerr error) (terraform.HookAction, error) { 123 id := n.HumanId() 124 125 h.l.Lock() 126 op := h.resources[id] 127 delete(h.resources, id) 128 h.l.Unlock() 129 130 var msg string 131 switch op { 132 case uiResourceModify: 133 msg = "Modifications complete" 134 case uiResourceDestroy: 135 msg = "Destruction complete" 136 case uiResourceCreate: 137 msg = "Creation complete" 138 case uiResourceUnknown: 139 return terraform.HookActionContinue, nil 140 } 141 142 if applyerr != nil { 143 // Errors are collected and printed in ApplyCommand, no need to duplicate 144 return terraform.HookActionContinue, nil 145 } 146 147 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 148 "[reset][bold]%s: %s[reset_bold]", 149 id, msg))) 150 151 return terraform.HookActionContinue, nil 152 } 153 154 func (h *UiHook) PreDiff( 155 n *terraform.InstanceInfo, 156 s *terraform.InstanceState) (terraform.HookAction, error) { 157 return terraform.HookActionContinue, nil 158 } 159 160 func (h *UiHook) PreProvision( 161 n *terraform.InstanceInfo, 162 provId string) (terraform.HookAction, error) { 163 id := n.HumanId() 164 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 165 "[reset][bold]%s: Provisioning with '%s'...[reset_bold]", 166 id, provId))) 167 return terraform.HookActionContinue, nil 168 } 169 170 func (h *UiHook) ProvisionOutput( 171 n *terraform.InstanceInfo, 172 provId string, 173 msg string) { 174 id := n.HumanId() 175 var buf bytes.Buffer 176 buf.WriteString(h.Colorize.Color("[reset]")) 177 178 prefix := fmt.Sprintf("%s (%s): ", id, provId) 179 s := bufio.NewScanner(strings.NewReader(msg)) 180 s.Split(scanLines) 181 for s.Scan() { 182 line := strings.TrimRightFunc(s.Text(), unicode.IsSpace) 183 if line != "" { 184 buf.WriteString(fmt.Sprintf("%s%s\n", prefix, line)) 185 } 186 } 187 188 h.ui.Output(strings.TrimSpace(buf.String())) 189 } 190 191 func (h *UiHook) PreRefresh( 192 n *terraform.InstanceInfo, 193 s *terraform.InstanceState) (terraform.HookAction, error) { 194 h.once.Do(h.init) 195 196 id := n.HumanId() 197 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 198 "[reset][bold]%s: Refreshing state... (ID: %s)", 199 id, s.ID))) 200 return terraform.HookActionContinue, nil 201 } 202 203 func (h *UiHook) init() { 204 if h.Colorize == nil { 205 panic("colorize not given") 206 } 207 208 h.resources = make(map[string]uiResourceOp) 209 210 // Wrap the ui so that it is safe for concurrency regardless of the 211 // underlying reader/writer that is in place. 212 h.ui = &cli.ConcurrentUi{Ui: h.Ui} 213 } 214 215 // scanLines is basically copied from the Go standard library except 216 // we've modified it to also fine `\r`. 217 func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) { 218 if atEOF && len(data) == 0 { 219 return 0, nil, nil 220 } 221 if i := bytes.IndexByte(data, '\n'); i >= 0 { 222 // We have a full newline-terminated line. 223 return i + 1, dropCR(data[0:i]), nil 224 } 225 if i := bytes.IndexByte(data, '\r'); i >= 0 { 226 // We have a full newline-terminated line. 227 return i + 1, dropCR(data[0:i]), nil 228 } 229 // If we're at EOF, we have a final, non-terminated line. Return it. 230 if atEOF { 231 return len(data), dropCR(data), nil 232 } 233 // Request more data. 234 return 0, nil, nil 235 } 236 237 // dropCR drops a terminal \r from the data. 238 func dropCR(data []byte) []byte { 239 if len(data) > 0 && data[len(data)-1] == '\r' { 240 return data[0 : len(data)-1] 241 } 242 return data 243 }