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