github.com/hashicorp/packer@v1.14.3/packer/ui.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: BUSL-1.1 3 4 package packer 5 6 import ( 7 "bytes" 8 "errors" 9 "fmt" 10 "io" 11 "log" 12 "os" 13 "runtime" 14 "strings" 15 "syscall" 16 "time" 17 "unicode" 18 19 getter "github.com/hashicorp/go-getter/v2" 20 packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 21 ) 22 23 var ErrInterrupted = errors.New("interrupted") 24 25 type UiColor uint 26 27 const ( 28 UiColorRed UiColor = 31 29 UiColorGreen = 32 30 UiColorYellow = 33 31 UiColorBlue = 34 32 UiColorMagenta = 35 33 UiColorCyan = 36 34 ) 35 36 // ColoredUi is a UI that is colored using terminal colors. 37 type ColoredUi struct { 38 Color UiColor 39 ErrorColor UiColor 40 Ui packersdk.Ui 41 PB getter.ProgressTracker 42 } 43 44 var _ packersdk.Ui = new(ColoredUi) 45 46 func (u *ColoredUi) Ask(query string) (string, error) { 47 return u.Ui.Ask(u.colorize(query, u.Color, true)) 48 } 49 50 func (u *ColoredUi) Askf(query string, vals ...any) (string, error) { 51 return u.Ask(fmt.Sprintf(query, vals...)) 52 } 53 54 func (u *ColoredUi) Say(message string) { 55 u.Ui.Say(u.colorize(message, u.Color, true)) 56 } 57 58 func (u *ColoredUi) Sayf(message string, vals ...any) { 59 u.Say(fmt.Sprintf(message, vals...)) 60 } 61 62 // Deprecated: Use `Say` instead. 63 func (u *ColoredUi) Message(message string) { 64 u.Say(message) 65 } 66 67 func (u *ColoredUi) Error(message string) { 68 color := u.ErrorColor 69 if color == 0 { 70 color = UiColorRed 71 } 72 73 u.Ui.Error(u.colorize(message, color, true)) 74 } 75 76 func (u *ColoredUi) Errorf(message string, vals ...any) { 77 u.Error(fmt.Sprintf(message, vals...)) 78 } 79 80 func (u *ColoredUi) Machine(t string, args ...string) { 81 // Don't colorize machine-readable output 82 u.Ui.Machine(t, args...) 83 } 84 85 func (u *ColoredUi) TrackProgress(src string, currentSize, totalSize int64, stream io.ReadCloser) io.ReadCloser { 86 return u.Ui.TrackProgress(u.colorize(src, u.Color, false), currentSize, totalSize, stream) 87 } 88 89 func (u *ColoredUi) colorize(message string, color UiColor, bold bool) string { 90 if !u.supportsColors() { 91 return message 92 } 93 94 attr := 0 95 if bold { 96 attr = 1 97 } 98 99 return fmt.Sprintf("\033[%d;%dm%s\033[0m", attr, color, message) 100 } 101 102 func (u *ColoredUi) supportsColors() bool { 103 // Never use colors if we have this environmental variable 104 if os.Getenv("PACKER_NO_COLOR") != "" { 105 return false 106 } 107 108 // For now, on non-Windows machine, just assume it does 109 if runtime.GOOS != "windows" { 110 return true 111 } 112 113 // On Windows, if we appear to be in Cygwin, then it does 114 cygwin := os.Getenv("CYGWIN") != "" || 115 os.Getenv("OSTYPE") == "cygwin" || 116 os.Getenv("TERM") == "cygwin" 117 118 return cygwin 119 } 120 121 // TargetedUI is a UI that wraps another UI implementation and modifies 122 // the output to indicate a specific target. Specifically, all Say output 123 // is prefixed with the target name. Message output is not prefixed but 124 // is offset by the length of the target so that output is lined up properly 125 // with Say output. Machine-readable output has the proper target set. 126 type TargetedUI struct { 127 Target string 128 Ui packersdk.Ui 129 } 130 131 var _ packersdk.Ui = new(TargetedUI) 132 133 func (u *TargetedUI) Ask(query string) (string, error) { 134 return u.Ui.Ask(u.prefixLines(true, query)) 135 } 136 137 func (u *TargetedUI) Askf(query string, args ...any) (string, error) { 138 return u.Ask(fmt.Sprintf(query, args...)) 139 } 140 141 func (u *TargetedUI) Say(message string) { 142 u.Ui.Say(u.prefixLines(true, message)) 143 } 144 145 func (u *TargetedUI) Sayf(message string, args ...any) { 146 u.Say(fmt.Sprintf(message, args...)) 147 } 148 149 // Deprecated: Use `Say` instead. 150 func (u *TargetedUI) Message(message string) { 151 u.Say(message) 152 } 153 154 func (u *TargetedUI) Error(message string) { 155 u.Ui.Error(u.prefixLines(true, message)) 156 } 157 158 func (u *TargetedUI) Errorf(message string, args ...any) { 159 u.Error(fmt.Sprintf(message, args...)) 160 } 161 162 func (u *TargetedUI) Machine(t string, args ...string) { 163 // Prefix in the target, then pass through 164 u.Ui.Machine(fmt.Sprintf("%s,%s", u.Target, t), args...) 165 } 166 167 func (u *TargetedUI) prefixLines(arrow bool, message string) string { 168 arrowText := "==>" 169 if !arrow { 170 arrowText = strings.Repeat(" ", len(arrowText)) 171 } 172 173 var result bytes.Buffer 174 175 for _, line := range strings.Split(message, "\n") { 176 result.WriteString(fmt.Sprintf("%s %s: %s\n", arrowText, u.Target, line)) 177 } 178 179 return strings.TrimRightFunc(result.String(), unicode.IsSpace) 180 } 181 182 func (u *TargetedUI) TrackProgress(src string, currentSize, totalSize int64, stream io.ReadCloser) io.ReadCloser { 183 return u.Ui.TrackProgress(u.prefixLines(false, src), currentSize, totalSize, stream) 184 } 185 186 // MachineReadableUi is a UI that only outputs machine-readable output 187 // to the given Writer. 188 type MachineReadableUi struct { 189 Writer io.Writer 190 PB packersdk.NoopProgressTracker 191 } 192 193 var _ packersdk.Ui = new(MachineReadableUi) 194 195 func (u *MachineReadableUi) Ask(query string) (string, error) { 196 return "", errors.New("machine-readable UI can't ask") 197 } 198 199 func (u *MachineReadableUi) Askf(query string, args ...any) (string, error) { 200 return u.Ask(fmt.Sprintf(query, args...)) 201 } 202 203 func (u *MachineReadableUi) Say(message string) { 204 u.Machine("ui", "say", message) 205 } 206 207 func (u *MachineReadableUi) Sayf(message string, args ...any) { 208 u.Say(fmt.Sprintf(message, args...)) 209 } 210 211 // Deprecated: Use `Say` instead. 212 func (u *MachineReadableUi) Message(message string) { 213 u.Machine("ui", "message", message) 214 } 215 216 func (u *MachineReadableUi) Error(message string) { 217 u.Machine("ui", "error", message) 218 } 219 220 func (u *MachineReadableUi) Errorf(message string, args ...any) { 221 u.Error(fmt.Sprintf(message, args...)) 222 } 223 224 func (u *MachineReadableUi) Machine(category string, args ...string) { 225 now := time.Now().UTC() 226 227 // Determine if we have a target, and set it 228 target := "" 229 commaIdx := strings.Index(category, ",") 230 if commaIdx > -1 { 231 target = category[0:commaIdx] 232 category = category[commaIdx+1:] 233 } 234 235 // Prepare the args 236 for i, v := range args { 237 // Use packersdk.LogSecretFilter to scrub out sensitive variables 238 args[i] = packersdk.LogSecretFilter.FilterString(args[i]) 239 args[i] = strings.Replace(v, ",", "%!(PACKER_COMMA)", -1) 240 args[i] = strings.Replace(args[i], "\r", "\\r", -1) 241 args[i] = strings.Replace(args[i], "\n", "\\n", -1) 242 } 243 argsString := strings.Join(args, ",") 244 245 _, err := fmt.Fprintf(u.Writer, "%d,%s,%s,%s\n", now.Unix(), target, category, argsString) 246 if err != nil { 247 if err == syscall.EPIPE || strings.Contains(err.Error(), "broken pipe") { 248 // Ignore epipe errors because that just means that the file 249 // is probably closed or going to /dev/null or something. 250 } else { 251 panic(err) 252 } 253 } 254 log.Printf("%d,%s,%s,%s\n", now.Unix(), target, category, argsString) 255 } 256 257 func (u *MachineReadableUi) TrackProgress(src string, currentSize, totalSize int64, stream io.ReadCloser) (body io.ReadCloser) { 258 return u.PB.TrackProgress(src, currentSize, totalSize, stream) 259 } 260 261 // TimestampedUi is a UI that wraps another UI implementation and 262 // prefixes each message with an RFC3339 timestamp 263 type TimestampedUi struct { 264 Ui packersdk.Ui 265 PB getter.ProgressTracker 266 } 267 268 var _ packersdk.Ui = new(TimestampedUi) 269 270 func (u *TimestampedUi) Ask(query string) (string, error) { 271 return u.Ui.Ask(query) 272 } 273 274 func (u *TimestampedUi) Askf(query string, args ...any) (string, error) { 275 return u.Ask(fmt.Sprintf(query, args...)) 276 } 277 278 func (u *TimestampedUi) Say(message string) { 279 u.Ui.Say(u.timestampLine(message)) 280 } 281 282 func (u *TimestampedUi) Sayf(message string, args ...any) { 283 u.Say(fmt.Sprintf(message, args...)) 284 } 285 286 // Deprecated: Use `Say` instead. 287 func (u *TimestampedUi) Message(message string) { 288 u.Say(message) 289 } 290 291 func (u *TimestampedUi) Error(message string) { 292 u.Ui.Error(u.timestampLine(message)) 293 } 294 295 func (u *TimestampedUi) Errorf(message string, args ...any) { 296 u.Error(fmt.Sprintf(message, args...)) 297 } 298 299 func (u *TimestampedUi) Machine(message string, args ...string) { 300 u.Ui.Machine(message, args...) 301 } 302 303 func (u *TimestampedUi) TrackProgress(src string, currentSize, totalSize int64, stream io.ReadCloser) (body io.ReadCloser) { 304 return u.Ui.TrackProgress(src, currentSize, totalSize, stream) 305 } 306 307 func (u *TimestampedUi) timestampLine(string string) string { 308 return fmt.Sprintf("%v: %v", time.Now().Format(time.RFC3339), string) 309 }