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  }