| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package packer |
| |
| import ( |
| "bytes" |
| "errors" |
| "fmt" |
| "io" |
| "log" |
| "os" |
| "runtime" |
| "strings" |
| "syscall" |
| "time" |
| "unicode" |
| |
| getter "github.com/hashicorp/go-getter/v2" |
| packersdk "github.com/hashicorp/packer-plugin-sdk/packer" |
| ) |
| |
| var ErrInterrupted = errors.New("interrupted") |
| |
| type UiColor uint |
| |
| const ( |
| UiColorRed UiColor = 31 |
| UiColorGreen = 32 |
| UiColorYellow = 33 |
| UiColorBlue = 34 |
| UiColorMagenta = 35 |
| UiColorCyan = 36 |
| ) |
| |
| // ColoredUi is a UI that is colored using terminal colors. |
| type ColoredUi struct { |
| Color UiColor |
| ErrorColor UiColor |
| Ui packersdk.Ui |
| PB getter.ProgressTracker |
| } |
| |
| var _ packersdk.Ui = new(ColoredUi) |
| |
| func (u *ColoredUi) Ask(query string) (string, error) { |
| return u.Ui.Ask(u.colorize(query, u.Color, true)) |
| } |
| |
| func (u *ColoredUi) Say(message string) { |
| u.Ui.Say(u.colorize(message, u.Color, true)) |
| } |
| |
| func (u *ColoredUi) Message(message string) { |
| u.Ui.Message(u.colorize(message, u.Color, false)) |
| } |
| |
| func (u *ColoredUi) Error(message string) { |
| color := u.ErrorColor |
| if color == 0 { |
| color = UiColorRed |
| } |
| |
| u.Ui.Error(u.colorize(message, color, true)) |
| } |
| |
| func (u *ColoredUi) Machine(t string, args ...string) { |
| // Don't colorize machine-readable output |
| u.Ui.Machine(t, args...) |
| } |
| |
| func (u *ColoredUi) TrackProgress(src string, currentSize, totalSize int64, stream io.ReadCloser) io.ReadCloser { |
| return u.Ui.TrackProgress(u.colorize(src, u.Color, false), currentSize, totalSize, stream) |
| } |
| |
| func (u *ColoredUi) colorize(message string, color UiColor, bold bool) string { |
| if !u.supportsColors() { |
| return message |
| } |
| |
| attr := 0 |
| if bold { |
| attr = 1 |
| } |
| |
| return fmt.Sprintf("\033[%d;%dm%s\033[0m", attr, color, message) |
| } |
| |
| func (u *ColoredUi) supportsColors() bool { |
| // Never use colors if we have this environmental variable |
| if os.Getenv("PACKER_NO_COLOR") != "" { |
| return false |
| } |
| |
| // For now, on non-Windows machine, just assume it does |
| if runtime.GOOS != "windows" { |
| return true |
| } |
| |
| // On Windows, if we appear to be in Cygwin, then it does |
| cygwin := os.Getenv("CYGWIN") != "" || |
| os.Getenv("OSTYPE") == "cygwin" || |
| os.Getenv("TERM") == "cygwin" |
| |
| return cygwin |
| } |
| |
| // TargetedUI is a UI that wraps another UI implementation and modifies |
| // the output to indicate a specific target. Specifically, all Say output |
| // is prefixed with the target name. Message output is not prefixed but |
| // is offset by the length of the target so that output is lined up properly |
| // with Say output. Machine-readable output has the proper target set. |
| type TargetedUI struct { |
| Target string |
| Ui packersdk.Ui |
| } |
| |
| var _ packersdk.Ui = new(TargetedUI) |
| |
| func (u *TargetedUI) Ask(query string) (string, error) { |
| return u.Ui.Ask(u.prefixLines(true, query)) |
| } |
| |
| func (u *TargetedUI) Say(message string) { |
| u.Ui.Say(u.prefixLines(true, message)) |
| } |
| |
| func (u *TargetedUI) Message(message string) { |
| u.Ui.Message(u.prefixLines(false, message)) |
| } |
| |
| func (u *TargetedUI) Error(message string) { |
| u.Ui.Error(u.prefixLines(true, message)) |
| } |
| |
| func (u *TargetedUI) Machine(t string, args ...string) { |
| // Prefix in the target, then pass through |
| u.Ui.Machine(fmt.Sprintf("%s,%s", u.Target, t), args...) |
| } |
| |
| func (u *TargetedUI) prefixLines(arrow bool, message string) string { |
| arrowText := "==>" |
| if !arrow { |
| arrowText = strings.Repeat(" ", len(arrowText)) |
| } |
| |
| var result bytes.Buffer |
| |
| for _, line := range strings.Split(message, "\n") { |
| result.WriteString(fmt.Sprintf("%s %s: %s\n", arrowText, u.Target, line)) |
| } |
| |
| return strings.TrimRightFunc(result.String(), unicode.IsSpace) |
| } |
| |
| func (u *TargetedUI) TrackProgress(src string, currentSize, totalSize int64, stream io.ReadCloser) io.ReadCloser { |
| return u.Ui.TrackProgress(u.prefixLines(false, src), currentSize, totalSize, stream) |
| } |
| |
| // MachineReadableUi is a UI that only outputs machine-readable output |
| // to the given Writer. |
| type MachineReadableUi struct { |
| Writer io.Writer |
| PB packersdk.NoopProgressTracker |
| } |
| |
| var _ packersdk.Ui = new(MachineReadableUi) |
| |
| func (u *MachineReadableUi) Ask(query string) (string, error) { |
| return "", errors.New("machine-readable UI can't ask") |
| } |
| |
| func (u *MachineReadableUi) Say(message string) { |
| u.Machine("ui", "say", message) |
| } |
| |
| func (u *MachineReadableUi) Message(message string) { |
| u.Machine("ui", "message", message) |
| } |
| |
| func (u *MachineReadableUi) Error(message string) { |
| u.Machine("ui", "error", message) |
| } |
| |
| func (u *MachineReadableUi) Machine(category string, args ...string) { |
| now := time.Now().UTC() |
| |
| // Determine if we have a target, and set it |
| target := "" |
| commaIdx := strings.Index(category, ",") |
| if commaIdx > -1 { |
| target = category[0:commaIdx] |
| category = category[commaIdx+1:] |
| } |
| |
| // Prepare the args |
| for i, v := range args { |
| // Use packersdk.LogSecretFilter to scrub out sensitive variables |
| args[i] = packersdk.LogSecretFilter.FilterString(args[i]) |
| args[i] = strings.Replace(v, ",", "%!(PACKER_COMMA)", -1) |
| args[i] = strings.Replace(args[i], "\r", "\\r", -1) |
| args[i] = strings.Replace(args[i], "\n", "\\n", -1) |
| } |
| argsString := strings.Join(args, ",") |
| |
| _, err := fmt.Fprintf(u.Writer, "%d,%s,%s,%s\n", now.Unix(), target, category, argsString) |
| if err != nil { |
| if err == syscall.EPIPE || strings.Contains(err.Error(), "broken pipe") { |
| // Ignore epipe errors because that just means that the file |
| // is probably closed or going to /dev/null or something. |
| } else { |
| panic(err) |
| } |
| } |
| log.Printf("%d,%s,%s,%s\n", now.Unix(), target, category, argsString) |
| } |
| |
| func (u *MachineReadableUi) TrackProgress(src string, currentSize, totalSize int64, stream io.ReadCloser) (body io.ReadCloser) { |
| return u.PB.TrackProgress(src, currentSize, totalSize, stream) |
| } |
| |
| // TimestampedUi is a UI that wraps another UI implementation and |
| // prefixes each message with an RFC3339 timestamp |
| type TimestampedUi struct { |
| Ui packersdk.Ui |
| PB getter.ProgressTracker |
| } |
| |
| var _ packersdk.Ui = new(TimestampedUi) |
| |
| func (u *TimestampedUi) Ask(query string) (string, error) { |
| return u.Ui.Ask(query) |
| } |
| |
| func (u *TimestampedUi) Say(message string) { |
| u.Ui.Say(u.timestampLine(message)) |
| } |
| |
| func (u *TimestampedUi) Message(message string) { |
| u.Ui.Message(u.timestampLine(message)) |
| } |
| |
| func (u *TimestampedUi) Error(message string) { |
| u.Ui.Error(u.timestampLine(message)) |
| } |
| |
| func (u *TimestampedUi) Machine(message string, args ...string) { |
| u.Ui.Machine(message, args...) |
| } |
| |
| func (u *TimestampedUi) TrackProgress(src string, currentSize, totalSize int64, stream io.ReadCloser) (body io.ReadCloser) { |
| return u.Ui.TrackProgress(src, currentSize, totalSize, stream) |
| } |
| |
| func (u *TimestampedUi) timestampLine(string string) string { |
| return fmt.Sprintf("%v: %v", time.Now().Format(time.RFC3339), string) |
| } |