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 }