-
Notifications
You must be signed in to change notification settings - Fork 124
/
shell.go
275 lines (225 loc) · 8.87 KB
/
shell.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
package command
import (
"context"
"fmt"
"path/filepath"
"strings"
"github.com/evergreen-ci/evergreen"
"github.com/evergreen-ci/evergreen/agent/internal"
"github.com/evergreen-ci/evergreen/agent/internal/client"
agentutil "github.com/evergreen-ci/evergreen/agent/util"
"github.com/evergreen-ci/evergreen/util"
"github.com/mitchellh/mapstructure"
"github.com/mongodb/grip"
"github.com/mongodb/grip/level"
"github.com/mongodb/grip/message"
"github.com/mongodb/jasper"
"github.com/mongodb/jasper/options"
"github.com/pkg/errors"
)
// shellExec is responsible for running the shell code.
type shellExec struct {
// Script is the shell code to be run on the agent machine.
Script string `mapstructure:"script" plugin:"expand"`
// Silent, if set to true, prevents shell code/output from being
// logged to the agent's task logs. This can be used to avoid
// exposing sensitive expansion parameters and keys.
Silent bool `mapstructure:"silent"`
// Shell describes the shell to execute the script contents
// with. Defaults to "sh", but users can customize to
// explicitly specify another shell.
Shell string `mapstructure:"shell"`
Env map[string]string `mapstructure:"env"`
// AddExpansionsToEnv adds all defined expansions to the shell environment.
AddExpansionsToEnv bool `mapstructure:"add_expansions_to_env"`
// IncludeExpansionsInEnv allows users to specify which expansions should be
// included in the environment, if they are defined. It is not an error to
// specify expansions that are not defined in include_expansions_in_env.
IncludeExpansionsInEnv []string `mapstructure:"include_expansions_in_env"`
// AddToPath allows additional paths to be prepended to the PATH environment
// variable.
AddToPath []string `mapstructure:"add_to_path"`
// Background, if set to true, prevents shell code/output from
// waiting for the script to complete and immediately returns
// to the caller
Background bool `mapstructure:"background"`
// WorkingDir is the working directory to start the shell in.
WorkingDir string `mapstructure:"working_dir"`
// SystemLog if set will write the shell command's output to the system logs, instead of the
// task logs. This can be used to collect diagnostic data in the background of a running task.
SystemLog bool `mapstructure:"system_log"`
// ExecuteAsString forces the script to do something like `sh -c "<script arg>"`. By default this command
// executes sh and passes the script arg to its stdin
ExecuteAsString bool `mapstructure:"exec_as_string"`
// IgnoreStandardOutput and IgnoreStandardError allow users to
// elect to ignore either standard out and/or standard output.
IgnoreStandardOutput bool `mapstructure:"ignore_standard_out"`
IgnoreStandardError bool `mapstructure:"ignore_standard_error"`
// RedirectStandardErrorToOutput allows you to capture
// standard error in the same stream as standard output. This
// improves the synchronization of these streams.
RedirectStandardErrorToOutput bool `mapstructure:"redirect_standard_error_to_output"`
// ContinueOnError determines whether or not a failed return code
// should cause the task to be marked as failed. Setting this to true
// allows following commands to execute even if this shell command fails.
ContinueOnError bool `mapstructure:"continue_on_err"`
base
}
func shellExecFactory() Command { return &shellExec{} }
func (*shellExec) Name() string { return evergreen.ShellExecCommandName }
// ParseParams reads in the command's parameters.
func (c *shellExec) ParseParams(params map[string]interface{}) error {
if params == nil {
return errors.New("params cannot be nil")
}
err := mapstructure.Decode(params, c)
if err != nil {
return errors.Wrap(err, "decoding mapstructure params")
}
if c.Silent {
c.IgnoreStandardError = true
c.IgnoreStandardOutput = true
}
if c.Script == "" {
return errors.New("must specify a script")
}
if c.Shell == "" {
c.Shell = "sh"
}
if c.IgnoreStandardOutput && c.RedirectStandardErrorToOutput {
return errors.New("cannot ignore standard output and also redirect standard error to it")
}
if c.Env == nil {
c.Env = map[string]string{}
}
return nil
}
// Execute starts the shell with its given parameters.
func (c *shellExec) Execute(ctx context.Context, _ client.Communicator, logger client.LoggerProducer, conf *internal.TaskConfig) error {
logger.Execution().Debug("Preparing script...")
// We do this before expanding expansions so that expansions are not logged.
if c.Silent {
logger.Execution().Infof("Executing script with shell '%s' (source hidden)...",
c.Shell)
} else {
logger.Execution().Infof("Executing script with shell '%s':\n%s",
c.Shell, c.Script)
}
var err error
if err = c.doExpansions(&conf.Expansions); err != nil {
return errors.WithStack(err)
}
logger.Execution().WarningWhen(filepath.IsAbs(c.WorkingDir) && !strings.HasPrefix(c.WorkingDir, conf.WorkDir),
fmt.Sprintf("The working directory is an absolute path [%s], which isn't supported except when prefixed by '%s'.",
c.WorkingDir, conf.WorkDir))
c.WorkingDir, err = getWorkingDirectoryLegacy(conf, c.WorkingDir)
if err != nil {
return errors.Wrap(err, "getting working directory")
}
taskTmpDir, err := getWorkingDirectoryLegacy(conf, "tmp")
if err != nil {
logger.Execution().Notice(errors.Wrap(err, "getting task temporary directory"))
}
c.Env = defaultAndApplyExpansionsToEnv(c.Env, modifyEnvOptions{
taskID: conf.Task.Id,
workingDir: c.WorkingDir,
tmpDir: taskTmpDir,
expansions: conf.Expansions,
includeExpansionsInEnv: c.IncludeExpansionsInEnv,
addExpansionsToEnv: c.AddExpansionsToEnv,
addToPath: c.AddToPath,
})
logger.Execution().Debug(message.Fields{
"working_directory": c.WorkingDir,
"shell": c.Shell,
})
cmd := c.JasperManager().CreateCommand(ctx).
Background(c.Background).Directory(c.WorkingDir).Environment(c.Env).Append(c.Shell).
SuppressStandardError(c.IgnoreStandardError).SuppressStandardOutput(c.IgnoreStandardOutput).RedirectErrorToOutput(c.RedirectStandardErrorToOutput).
ProcConstructor(func(lctx context.Context, opts *options.Create) (jasper.Process, error) {
if c.ExecuteAsString {
opts.Args = append(opts.Args, "-c", c.Script)
} else {
opts.StandardInput = strings.NewReader(c.Script)
}
var cancel context.CancelFunc
var ictx context.Context
if c.Background {
ictx, cancel = context.WithCancel(context.Background())
} else {
ictx = lctx
}
var proc jasper.Process
proc, err = c.JasperManager().CreateProcess(ictx, opts)
if err != nil {
if cancel != nil {
cancel()
}
return proc, errors.WithStack(err)
}
if cancel != nil {
grip.Warning(message.WrapError(proc.RegisterTrigger(lctx, func(info jasper.ProcessInfo) {
cancel()
}), "registering cancellation for process"))
}
pid := proc.Info(ctx).PID
agentutil.TrackProcess(conf.Task.Id, pid, logger.System())
if c.Background {
logger.Execution().Debugf("Running process with PID %d in the background.", pid)
} else {
logger.Execution().Infof("Running process with PID %d.", pid)
}
return proc, nil
})
if !c.IgnoreStandardOutput {
if c.SystemLog {
cmd.SetOutputSender(level.Info, logger.System().GetSender())
} else {
cmd.SetOutputSender(level.Info, logger.Task().GetSender())
}
}
if !c.IgnoreStandardError {
if c.SystemLog {
cmd.SetErrorSender(level.Error, logger.System().GetSender())
} else {
cmd.SetErrorSender(level.Error, logger.Task().GetSender())
}
}
err = cmd.Run(ctx)
if !c.Background && err != nil {
if exitCode, _ := cmd.Wait(ctx); exitCode != 0 {
err = errors.Errorf("exit code %d", exitCode)
}
}
err = errors.Wrapf(err, "shell script encountered problem")
if ctxErr := ctx.Err(); ctxErr != nil {
logger.System().Debugf("Canceled command '%s', dumping running processes.", c.Name())
logger.System().Debug(message.CollectAllProcesses())
logger.Execution().Notice(err)
return errors.Wrapf(ctxErr, "canceled while running command '%s'", c.Name())
}
if c.ContinueOnError && err != nil {
logger.Execution().Noticef("Script errored, but continue on error is set - continuing task execution. Error: %s.", err)
return nil
}
return err
}
func (c *shellExec) doExpansions(exp *util.Expansions) error {
catcher := grip.NewBasicCatcher()
var err error
c.WorkingDir, err = exp.ExpandString(c.WorkingDir)
catcher.Wrap(err, "expanding working directory")
c.Script, err = exp.ExpandString(c.Script)
catcher.Wrap(err, "expanding script")
c.Shell, err = exp.ExpandString(c.Shell)
catcher.Wrap(err, "expanding shell")
for k, v := range c.Env {
c.Env[k], err = exp.ExpandString(v)
catcher.Wrapf(err, "expanding environment variable '%s'", k)
}
for idx := range c.AddToPath {
c.AddToPath[idx], err = exp.ExpandString(c.AddToPath[idx])
catcher.Wrapf(err, "expanding element %d to add to path", idx)
}
return catcher.Resolve()
}