get.porter.sh/porter@v1.3.0/pkg/exec/builder/execute.go (about)

     1  package builder
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"os/exec"
    10  	"strings"
    11  
    12  	"get.porter.sh/porter/pkg/runtime"
    13  	"get.porter.sh/porter/pkg/tracing"
    14  )
    15  
    16  var DefaultFlagDashes = Dashes{
    17  	Long:  "--",
    18  	Short: "-",
    19  }
    20  
    21  // BuildableAction is an Action that can be marshaled and unmarshaled "generically"
    22  type BuildableAction interface {
    23  	// MakeSteps returns a Steps struct to unmarshal into.
    24  	MakeSteps() interface{}
    25  }
    26  
    27  type ExecutableAction interface {
    28  	GetSteps() []ExecutableStep
    29  }
    30  
    31  type ExecutableStep interface {
    32  	GetCommand() string
    33  	//GetArguments() puts the arguments at the beginning of the command
    34  	GetArguments() []string
    35  	GetFlags() Flags
    36  	GetWorkingDir() string
    37  }
    38  
    39  type HasEnvironmentVars interface {
    40  	GetEnvironmentVars() map[string]string
    41  }
    42  
    43  type HasOrderedArguments interface {
    44  	GetSuffixArguments() []string
    45  }
    46  
    47  type HasCustomDashes interface {
    48  	GetDashes() Dashes
    49  }
    50  
    51  type SuppressesOutput interface {
    52  	SuppressesOutput() bool
    53  }
    54  
    55  // HasErrorHandling is implemented by mixin commands that want to handle errors
    56  // themselves, and possibly allow failed commands to either pass, or to improve
    57  // the displayed error message
    58  type HasErrorHandling interface {
    59  	HandleError(ctx context.Context, err ExitError, stdout string, stderr string) error
    60  }
    61  
    62  type ExitError interface {
    63  	error
    64  	ExitCode() int
    65  }
    66  
    67  // ExecuteSingleStepAction runs the command represented by an ExecutableAction, where only
    68  // a single step is allowed to be defined in the Action (which is what happens when Porter
    69  // executes steps one at a time).
    70  func ExecuteSingleStepAction(ctx context.Context, cfg runtime.RuntimeConfig, action ExecutableAction) (string, error) {
    71  	steps := action.GetSteps()
    72  	if len(steps) != 1 {
    73  		return "", fmt.Errorf("expected a single step, but got %d", len(steps))
    74  	}
    75  	step := steps[0]
    76  
    77  	output, err := ExecuteStep(ctx, cfg, step)
    78  	if err != nil {
    79  		return output, err
    80  	}
    81  
    82  	swo, ok := step.(StepWithOutputs)
    83  	if !ok {
    84  		return output, nil
    85  	}
    86  
    87  	err = ProcessJsonPathOutputs(ctx, cfg, swo, output)
    88  	if err != nil {
    89  		return output, err
    90  	}
    91  
    92  	err = ProcessRegexOutputs(ctx, cfg, swo, output)
    93  	if err != nil {
    94  		return output, err
    95  	}
    96  
    97  	err = ProcessFileOutputs(ctx, cfg, swo)
    98  	return output, err
    99  }
   100  
   101  // ExecuteStep runs the command represented by an ExecutableStep, piping stdout/stderr
   102  // back to the context and returns the buffered output for subsequent processing.
   103  func ExecuteStep(ctx context.Context, cfg runtime.RuntimeConfig, step ExecutableStep) (string, error) {
   104  	ctx, span := tracing.StartSpan(ctx)
   105  	defer span.EndSpan()
   106  
   107  	// Identify if any suffix arguments are defined
   108  	var suffixArgs []string
   109  	orderedArgs, ok := step.(HasOrderedArguments)
   110  	if ok {
   111  		suffixArgs = orderedArgs.GetSuffixArguments()
   112  	}
   113  
   114  	// Preallocate an array big enough to hold all arguments
   115  	arguments := step.GetArguments()
   116  	flags := step.GetFlags()
   117  	args := make([]string, len(arguments), 1+len(arguments)+len(flags)*2+len(suffixArgs))
   118  
   119  	// Copy all prefix arguments
   120  	copy(args, arguments)
   121  
   122  	// Copy all flags
   123  	dashes := DefaultFlagDashes
   124  	if dashing, ok := step.(HasCustomDashes); ok {
   125  		dashes = dashing.GetDashes()
   126  	}
   127  
   128  	// Split up flags that have spaces so that we pass them as separate array elements
   129  	// It doesn't show up any differently in the printed command, but it matters to how the command
   130  	// it executed against the system.
   131  	flagsSlice := splitCommand(flags.ToSlice(dashes))
   132  
   133  	args = append(args, flagsSlice...)
   134  
   135  	// Append any final suffix arguments
   136  	args = append(args, suffixArgs...)
   137  
   138  	// Add env vars if defined
   139  	if stepWithEnvVars, ok := step.(HasEnvironmentVars); ok {
   140  		for k, v := range stepWithEnvVars.GetEnvironmentVars() {
   141  			cfg.Setenv(k, v)
   142  		}
   143  	}
   144  
   145  	cmd := cfg.NewCommand(ctx, step.GetCommand(), args...)
   146  
   147  	// ensure command is executed in the correct directory
   148  	wd := step.GetWorkingDir()
   149  	if len(wd) > 0 && wd != "." {
   150  		cmd.Dir = wd
   151  	}
   152  
   153  	prettyCmd := fmt.Sprintf("%s %s", cmd.Dir, strings.Join(cmd.Args, " "))
   154  
   155  	// Setup output streams for command
   156  	// If Step suppresses output, update streams accordingly
   157  	stdout := &bytes.Buffer{}
   158  	stderr := &bytes.Buffer{}
   159  	suppressOutput := false
   160  	if suppressible, ok := step.(SuppressesOutput); ok {
   161  		suppressOutput = suppressible.SuppressesOutput()
   162  	}
   163  
   164  	if suppressOutput {
   165  		// We still capture the output, but we won't print it
   166  		cmd.Stdout = stdout
   167  		cmd.Stderr = stderr
   168  		span.Debugf("output suppressed for command %s", prettyCmd)
   169  	} else {
   170  		cmd.Stdout = io.MultiWriter(cfg.Out, stdout)
   171  		cmd.Stderr = io.MultiWriter(cfg.Err, stderr)
   172  		span.Debug(prettyCmd)
   173  	}
   174  
   175  	err := cmd.Start()
   176  	if err != nil {
   177  		return "", span.Error(fmt.Errorf("couldn't run command %s: %w", prettyCmd, err))
   178  	}
   179  
   180  	err = cmd.Wait()
   181  
   182  	// Check if the command knows how to handle and recover from its own errors
   183  	if err != nil {
   184  		if exitErr, ok := err.(*exec.ExitError); ok {
   185  			if handler, ok := step.(HasErrorHandling); ok {
   186  				err = handler.HandleError(ctx, exitErr, stdout.String(), stderr.String())
   187  			}
   188  		}
   189  	}
   190  
   191  	// Ok, now check if we still have a problem
   192  	if err != nil {
   193  		return "", span.Error(fmt.Errorf("error running command %s: %w", prettyCmd, err))
   194  	}
   195  
   196  	return stdout.String(), nil
   197  }
   198  
   199  var whitespace = string([]rune{space, newline, tab})
   200  
   201  const (
   202  	space       = rune(' ')
   203  	newline     = rune('\n')
   204  	tab         = rune('\t')
   205  	backslash   = rune('\\')
   206  	doubleQuote = rune('"')
   207  	singleQuote = rune('\'')
   208  )
   209  
   210  // expandOnWhitespace finds elements with multiple words that are not "glued" together with quotes
   211  // and splits them into separate elements in the slice
   212  func splitCommand(slice []string) []string {
   213  	expandedSlice := make([]string, 0, len(slice))
   214  	for _, chunk := range slice {
   215  		chunkettes := findWords(chunk)
   216  		expandedSlice = append(expandedSlice, chunkettes...)
   217  	}
   218  
   219  	return expandedSlice
   220  }
   221  
   222  func findWords(input string) []string {
   223  	words := make([]string, 0, 1)
   224  	next := input
   225  	for len(next) > 0 {
   226  		word, remainder, err := findNextWord(next)
   227  		if err != nil {
   228  			return []string{input}
   229  		}
   230  		next = remainder
   231  		words = append(words, word)
   232  	}
   233  
   234  	return words
   235  }
   236  
   237  func findNextWord(input string) (string, string, error) {
   238  	var buf bytes.Buffer
   239  
   240  	// Remove leading whitespace before starting
   241  	input = strings.TrimLeft(input, whitespace)
   242  
   243  	var escaped bool
   244  	var wordStart, wordStop int
   245  	var closingQuote rune
   246  
   247  	for i, r := range input {
   248  		// Prevent escaped characters from matching below
   249  		if escaped {
   250  			r = -1
   251  			escaped = false
   252  		}
   253  
   254  		switch r {
   255  		case backslash:
   256  			// Escape the next character
   257  			escaped = true
   258  			continue
   259  		case closingQuote:
   260  			wordStop = i
   261  			closingQuote = 0 // Reset looking for a closing quote
   262  		case singleQuote, doubleQuote:
   263  			// Seek to the closing quote only
   264  			if closingQuote != 0 {
   265  				continue
   266  			}
   267  
   268  			wordStart = 1    // Skip opening quote
   269  			closingQuote = r // Seek to the same closing quote
   270  		case space, tab, newline:
   271  			// Seek to the closing quote only
   272  			if closingQuote != 0 {
   273  				continue
   274  			}
   275  
   276  			wordStart = 0
   277  			wordStop = i
   278  		}
   279  
   280  		// Found the end of a word
   281  		if wordStop > 0 {
   282  			_, err := buf.WriteString(input[wordStart:wordStop])
   283  			if err != nil {
   284  				return "", input, errors.New("error writing to buffer")
   285  			}
   286  			return buf.String(), input[wordStop+1:], nil
   287  		}
   288  	}
   289  
   290  	if closingQuote != 0 {
   291  		return "", "", errors.New("unmatched quote found")
   292  	}
   293  
   294  	// Hit the end of input, flush the remainder
   295  	_, err := buf.WriteString(input)
   296  	if err != nil {
   297  		return "", input, errors.New("error writing to buffer")
   298  	}
   299  
   300  	return buf.String(), "", nil
   301  }