github.com/tkak/terraform@v0.5.4-0.20150712180941-7f738dc27225/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 msg = fmt.Sprintf("Error: %s", applyerr) 144 } 145 146 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 147 "[reset][bold]%s: %s[reset_bold]", 148 id, msg))) 149 150 return terraform.HookActionContinue, nil 151 } 152 153 func (h *UiHook) PreDiff( 154 n *terraform.InstanceInfo, 155 s *terraform.InstanceState) (terraform.HookAction, error) { 156 return terraform.HookActionContinue, nil 157 } 158 159 func (h *UiHook) PreProvision( 160 n *terraform.InstanceInfo, 161 provId string) (terraform.HookAction, error) { 162 id := n.HumanId() 163 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 164 "[reset][bold]%s: Provisioning with '%s'...[reset_bold]", 165 id, provId))) 166 return terraform.HookActionContinue, nil 167 } 168 169 func (h *UiHook) ProvisionOutput( 170 n *terraform.InstanceInfo, 171 provId string, 172 msg string) { 173 id := n.HumanId() 174 var buf bytes.Buffer 175 buf.WriteString(h.Colorize.Color("[reset]")) 176 177 prefix := fmt.Sprintf("%s (%s): ", id, provId) 178 s := bufio.NewScanner(strings.NewReader(msg)) 179 s.Split(scanLines) 180 for s.Scan() { 181 line := strings.TrimRightFunc(s.Text(), unicode.IsSpace) 182 if line != "" { 183 buf.WriteString(fmt.Sprintf("%s%s\n", prefix, line)) 184 } 185 } 186 187 h.ui.Output(strings.TrimSpace(buf.String())) 188 } 189 190 func (h *UiHook) PreRefresh( 191 n *terraform.InstanceInfo, 192 s *terraform.InstanceState) (terraform.HookAction, error) { 193 h.once.Do(h.init) 194 195 id := n.HumanId() 196 h.ui.Output(h.Colorize.Color(fmt.Sprintf( 197 "[reset][bold]%s: Refreshing state... (ID: %s)", 198 id, s.ID))) 199 return terraform.HookActionContinue, nil 200 } 201 202 func (h *UiHook) init() { 203 if h.Colorize == nil { 204 panic("colorize not given") 205 } 206 207 h.resources = make(map[string]uiResourceOp) 208 209 // Wrap the ui so that it is safe for concurrency regardless of the 210 // underlying reader/writer that is in place. 211 h.ui = &cli.ConcurrentUi{Ui: h.Ui} 212 } 213 214 // scanLines is basically copied from the Go standard library except 215 // we've modified it to also fine `\r`. 216 func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) { 217 if atEOF && len(data) == 0 { 218 return 0, nil, nil 219 } 220 if i := bytes.IndexByte(data, '\n'); i >= 0 { 221 // We have a full newline-terminated line. 222 return i + 1, dropCR(data[0:i]), nil 223 } 224 if i := bytes.IndexByte(data, '\r'); i >= 0 { 225 // We have a full newline-terminated line. 226 return i + 1, dropCR(data[0:i]), nil 227 } 228 // If we're at EOF, we have a final, non-terminated line. Return it. 229 if atEOF { 230 return len(data), dropCR(data), nil 231 } 232 // Request more data. 233 return 0, nil, nil 234 } 235 236 // dropCR drops a terminal \r from the data. 237 func dropCR(data []byte) []byte { 238 if len(data) > 0 && data[len(data)-1] == '\r' { 239 return data[0 : len(data)-1] 240 } 241 return data 242 }