feat: set the shell for workflow command steps (#5024)

Signed-off-by: anryko <andrej.svenke@deliveryhero.com>
This commit is contained in:
Andrej Svenke
2024-11-04 19:29:47 +01:00
committed by GitHub
parent e4dea0311d
commit 8285a0faf2
18 changed files with 414 additions and 140 deletions

View File

@@ -599,6 +599,10 @@ Full
```yaml
- run:
command: custom-command arg1 arg2
shell: sh
shellArgs:
- "--debug"
- "-c"
output: show
```
@@ -606,6 +610,8 @@ Full
|-----|--------------------------------------------------------------|---------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| run | map\[string -> string\] | none | no | Run a custom command |
| run.command | string | none | yes | Shell command to run |
| run.shell | string | "sh" | no | Name of the shell to use for command execution |
| run.shellArgs | string or []string | "-c" | no | Command line arguments to be passed to the shell. Cannot be set without `shell` |
| run.output | string | "show" | no | How to post-process the output of this command when posted in the PR comment. The options are<br/>*`show` - preserve the full output<br/>* `hide` - hide output from comment (still visible in the real-time streaming output)<br/> * `strip_refreshing` - hide all output up until and including the last line containing "Refreshing...". This matches the behavior of the built-in `plan` command |
#### Native Environment Variables
@@ -664,6 +670,13 @@ as the environment variable value.
- env:
name: ENV_NAME_2
command: 'echo "dynamic-value-$(date)"'
- env:
name: ENV_NAME_3
command: echo ${DIR%$REPO_REL_DIR}
shell: bash
shellArgs:
- "--verbose"
- "-c"
```
| Key | Type | Default | Required | Description |
@@ -672,6 +685,8 @@ as the environment variable value.
| env.name | string | none | yes | Name of the environment variable |
| env.value | string | none | no | Set the value of the environment variable to a hard-coded string. Cannot be set at the same time as `command` |
| env.command | string | none | no | Set the value of the environment variable to the output of a command. Cannot be set at the same time as `value` |
| env.shell | string | "sh" | no | Name of the shell to use for command execution. Cannot be set without `command` |
| env.shellArgs | string or []string | "-c" | no | Command line arguments to be passed to the shell. Cannot be set without `shell` |
::: tip Notes
@@ -699,14 +714,20 @@ Full:
```yaml
- multienv:
command: custom-command
shell: bash
shellArgs:
- "--verbose"
- "-c"
output: show
```
| Key | Type | Default | Required | Description |
|------------------|-----------------------|---------|----------|-------------------------------------------------------------------------------------|
| multienv | map[string -> string] | none | no | Run a custom command and add printed environment variables |
| multienv.command | string | none | yes | Name of the custom script to run |
| multienv.output | string | "show" | no | Setting output to "hide" will supress the message obout added environment variables |
| Key | Type | Default | Required | Description |
|--------------------|-----------------------|---------|----------|-------------------------------------------------------------------------------------|
| multienv | map[string -> string] | none | no | Run a custom command and add printed environment variables |
| multienv.command | string | none | yes | Name of the custom script to run |
| multienv.shell | string | "sh" | no | Name of the shell to use for command execution |
| multienv.shellArgs | string or []string | "-c" | no | Command line arguments to be passed to the shell. Cannot be set without `shell` |
| multienv.output | string | "show" | no | Setting output to "hide" will supress the message obout added environment variables |
The output of the command execution must have the following format:
`EnvVar1Name=value1,EnvVar2Name=value2,EnvVar3Name=value3`

View File

@@ -9,6 +9,7 @@ import (
validation "github.com/go-ozzo/ozzo-validation"
"github.com/runatlantis/atlantis/server/core/config/valid"
"github.com/runatlantis/atlantis/server/utils"
)
const (
@@ -27,45 +28,58 @@ const (
MultiEnvStepName = "multienv"
ImportStepName = "import"
StateRmStepName = "state_rm"
ShellArgKey = "shell"
ShellArgsArgKey = "shellArgs"
)
// Step represents a single action/command to perform. In YAML, it can be set as
// 1. A single string for a built-in command:
// - init
// - plan
// - policy_check
//
// 2. A map for an env step with name and command or value, or a run step with a command and output config
// - env:
// name: test
// command: echo 312
// value: value
// - multienv:
// command: envs.sh
// outpiut: hide
// - run:
// command: my custom command
// output: hide
//
// 3. A map for a built-in command and extra_args:
// - plan:
// extra_args: [-var-file=staging.tfvars]
//
// 4. A map for a custom run command:
// - run: my custom command
//
// Here we parse step in the most generic fashion possible. See fields for more
// details.
/*
Step represents a single action/command to perform. In YAML, it can be set as
1. A single string for a built-in command:
- init
- plan
- policy_check
2. A map for an env step with name and command or value, or a run step with a command and output config
- env:
name: test_command
command: echo 312
- env:
name: test_value
value: value
- env:
name: test_bash_command
command: echo ${test_value::7}
shell: bash
shellArgs: ["--verbose", "-c"]
- multienv:
command: envs.sh
output: hide
shell: sh
shellArgs: -c
- run:
command: my custom command
output: hide
3. A map for a built-in command and extra_args:
- plan:
extra_args: [-var-file=staging.tfvars]
4. A map for a custom run command:
- run: my custom command
Here we parse step in the most generic fashion possible. See fields for more
details.
*/
type Step struct {
// Key will be set in case #1 and #3 above to the key. In case #2, there
// could be multiple keys (since the element is a map) so we don't set Key.
Key *string
// CommandMap will be set in case #2 above.
CommandMap map[string]map[string]string
// Map will be set in case #3 above.
Map map[string]map[string][]string
// StringVal will be set in case #4 above.
StringVal map[string]string
// Map will be set in case #3 above.
Map map[string]map[string][]string
// CommandMap will be set in case #2 above.
CommandMap map[string]map[string]interface{}
}
func (s *Step) UnmarshalYAML(unmarshal func(interface{}) error) error {
@@ -142,7 +156,8 @@ func (s Step) Validate() error {
}
for k := range args {
if k != ExtraArgsKey {
return fmt.Errorf("built-in steps only support a single %s key, found %q in step %s", ExtraArgsKey, k, stepName)
return fmt.Errorf("built-in steps only support a single %s key, found %q in step %s",
ExtraArgsKey, k, stepName)
}
}
}
@@ -150,7 +165,7 @@ func (s Step) Validate() error {
}
envOrRunOrMultiEnvStep := func(value interface{}) error {
elem := value.(map[string]map[string]string)
elem := value.(map[string]map[string]interface{})
var keys []string
for k := range elem {
keys = append(keys, k)
@@ -169,63 +184,100 @@ func (s Step) Validate() error {
stepName := keys[0]
args := elem[keys[0]]
var argKeys []string
for k := range args {
argKeys = append(argKeys, k)
}
argMap := make(map[string]interface{})
for k, v := range args {
argMap[k] = v
}
// Sort so tests can be deterministic.
sort.Strings(argKeys)
// Validate keys common for all the steps.
if utils.SlicesContains(argKeys, ShellArgKey) && !utils.SlicesContains(argKeys, CommandArgKey) {
return fmt.Errorf("workflow steps only support %q key in combination with %q key",
ShellArgKey, CommandArgKey)
}
if utils.SlicesContains(argKeys, ShellArgsArgKey) && !utils.SlicesContains(argKeys, ShellArgKey) {
return fmt.Errorf("workflow steps only support %q key in combination with %q key",
ShellArgsArgKey, ShellArgKey)
}
switch t := argMap[ShellArgsArgKey].(type) {
case nil:
case string:
case []interface{}:
for _, e := range t {
if _, ok := e.(string); !ok {
return fmt.Errorf("%q step %q option must contain only strings, found %v\n",
stepName, ShellArgsArgKey, e)
}
}
default:
return fmt.Errorf("%q step %q option must be a string or a list of strings, found %v\n",
stepName, ShellArgsArgKey, t)
}
delete(argMap, ShellArgsArgKey)
delete(argMap, ShellArgKey)
// Validate keys per step type.
switch stepName {
case EnvStepName:
var argKeys []string
for k := range args {
argKeys = append(argKeys, k)
}
// Sort so tests can be deterministic.
sort.Strings(argKeys)
foundNameKey := false
for _, k := range argKeys {
if k != NameArgKey && k != CommandArgKey && k != ValueArgKey {
return fmt.Errorf("env steps only support keys %q, %q and %q, found key %q", NameArgKey, ValueArgKey, CommandArgKey, k)
if k != NameArgKey && k != CommandArgKey && k != ValueArgKey && k != ShellArgKey && k != ShellArgsArgKey {
return fmt.Errorf("env steps only support keys %q, %q, %q, %q and %q, found key %q",
NameArgKey, ValueArgKey, CommandArgKey, ShellArgKey, ShellArgsArgKey, k)
}
if k == NameArgKey {
foundNameKey = true
}
}
delete(argMap, CommandArgKey)
if !foundNameKey {
return fmt.Errorf("env steps must have a %q key set", NameArgKey)
}
// If we have 3 keys at this point then they've set both command and value.
if len(argKeys) != 2 {
delete(argMap, NameArgKey)
if utils.SlicesContains(argKeys, ValueArgKey) && utils.SlicesContains(argKeys, CommandArgKey) {
return fmt.Errorf("env steps only support one of the %q or %q keys, found both",
ValueArgKey, CommandArgKey)
}
delete(argMap, ValueArgKey)
case RunStepName, MultiEnvStepName:
argsCopy := make(map[string]string)
for k, v := range args {
argsCopy[k] = v
}
args = argsCopy
if _, ok := args[CommandArgKey]; !ok {
if _, ok := argMap[CommandArgKey].(string); !ok {
return fmt.Errorf("%q step must have a %q key set", stepName, CommandArgKey)
}
delete(args, CommandArgKey)
if v, ok := args[OutputArgKey]; ok {
if stepName == RunStepName && !(v == valid.PostProcessRunOutputShow || v == valid.PostProcessRunOutputHide || v == valid.PostProcessRunOutputStripRefreshing) {
return fmt.Errorf("run step %q option must be one of %q, %q, or %q", OutputArgKey, valid.PostProcessRunOutputShow, valid.PostProcessRunOutputHide, valid.PostProcessRunOutputStripRefreshing)
} else if stepName == MultiEnvStepName && !(v == valid.PostProcessRunOutputShow || v == valid.PostProcessRunOutputHide) {
return fmt.Errorf("multienv step %q option must be %q or %q", OutputArgKey, valid.PostProcessRunOutputShow, valid.PostProcessRunOutputHide)
delete(argMap, CommandArgKey)
if v, ok := argMap[OutputArgKey].(string); ok {
if stepName == RunStepName && !(v == valid.PostProcessRunOutputShow ||
v == valid.PostProcessRunOutputHide || v == valid.PostProcessRunOutputStripRefreshing) {
return fmt.Errorf("run step %q option must be one of %q, %q, or %q",
OutputArgKey, valid.PostProcessRunOutputShow, valid.PostProcessRunOutputHide,
valid.PostProcessRunOutputStripRefreshing)
} else if stepName == MultiEnvStepName && !(v == valid.PostProcessRunOutputShow ||
v == valid.PostProcessRunOutputHide) {
return fmt.Errorf("multienv step %q option must be %q or %q",
OutputArgKey, valid.PostProcessRunOutputShow, valid.PostProcessRunOutputHide)
}
}
delete(args, OutputArgKey)
if len(args) > 0 {
var argKeys []string
for k := range args {
argKeys = append(argKeys, k)
}
// Sort so tests can be deterministic.
sort.Strings(argKeys)
return fmt.Errorf("%q steps only support keys %q and %q, found extra keys %q", stepName, CommandArgKey, OutputArgKey, strings.Join(argKeys, ","))
}
delete(argMap, OutputArgKey)
default:
return fmt.Errorf("%q is not a valid step type", stepName)
}
if len(argMap) > 0 {
var argKeys []string
for k := range argMap {
argKeys = append(argKeys, k)
}
// Sort so tests can be deterministic.
sort.Strings(argKeys)
return fmt.Errorf("%q steps only support keys %q, %q, %q and %q, found extra keys %q",
stepName, CommandArgKey, OutputArgKey, ShellArgKey, ShellArgsArgKey, strings.Join(argKeys, ","))
}
return nil
}
@@ -278,16 +330,40 @@ func (s Step) ToValid() valid.Step {
// After validation we assume there's only one key and it's a valid
// step name so we just use the first one.
for stepName, stepArgs := range s.CommandMap {
step := valid.Step{
StepName: stepName,
EnvVarName: stepArgs[NameArgKey],
RunCommand: stepArgs[CommandArgKey],
EnvVarValue: stepArgs[ValueArgKey],
Output: valid.PostProcessRunOutputOption(stepArgs[OutputArgKey]),
step := valid.Step{StepName: stepName}
if name, ok := stepArgs[NameArgKey].(string); ok {
step.EnvVarName = name
}
if command, ok := stepArgs[CommandArgKey].(string); ok {
step.RunCommand = command
}
if value, ok := stepArgs[ValueArgKey].(string); ok {
step.EnvVarValue = value
}
if output, ok := stepArgs[OutputArgKey].(string); ok {
step.Output = valid.PostProcessRunOutputOption(output)
}
if shell, ok := stepArgs[ShellArgKey].(string); ok {
step.RunShell = &valid.CommandShell{
Shell: shell,
ShellArgs: []string{"-c"},
}
}
if step.StepName == RunStepName && step.Output == "" {
step.Output = valid.PostProcessRunOutputShow
}
switch t := stepArgs[ShellArgsArgKey].(type) {
case nil:
case string:
step.RunShell.ShellArgs = strings.Split(t, " ")
case []interface{}:
step.RunShell.ShellArgs = []string{}
for _, e := range t {
step.RunShell.ShellArgs = append(step.RunShell.ShellArgs, e.(string))
}
}
return step
}
}
@@ -341,6 +417,17 @@ func (s *Step) unmarshalGeneric(unmarshal func(interface{}) error) error {
return nil
}
// Try to unmarshal as a custom run step, ex.
// steps:
// - run: my command
// We validate if the key is run later.
var runStep map[string]string
err = unmarshal(&runStep)
if err == nil {
s.StringVal = runStep
return nil
}
// This represents a step with extra_args, ex:
// init:
// extra_args: [a, b]
@@ -353,26 +440,20 @@ func (s *Step) unmarshalGeneric(unmarshal func(interface{}) error) error {
return nil
}
// This represents an env step, ex:
// env:
// name: k
// value: hi //optional
// command: exec
var envStep map[string]map[string]string
err = unmarshal(&envStep)
if err == nil {
s.CommandMap = envStep
return nil
}
// Try to unmarshal as a custom run step, ex.
// This represents a command steps env, run, and multienv, ex:
// steps:
// - run: my command
// We validate if the key is run later.
var runStep map[string]string
err = unmarshal(&runStep)
// - env:
// name: k
// command: exec
// - run:
// name: test_bash_command
// command: echo ${test_value::7}
// shell: bash
// shellArgs: ["--verbose", "-c"]
var commandStep map[string]map[string]interface{}
err = unmarshal(&commandStep)
if err == nil {
s.StringVal = runStep
s.CommandMap = commandStep
return nil
}

View File

@@ -143,12 +143,12 @@ key: value`,
// Errors
{
description: "extra args style no slice strings",
description: "extra args style no map strings",
input: `
key:
value:
another: map`,
expErr: "yaml: unmarshal errors:\n line 3: cannot unmarshal !!map into string",
- value:
another: map`,
expErr: "yaml: unmarshal errors:\n line 3: cannot unmarshal !!seq into map[string]interface {}",
},
}
@@ -236,6 +236,47 @@ func TestStep_Validate(t *testing.T) {
},
expErr: "",
},
{
description: "env shell",
input: raw.Step{
CommandMap: EnvType{
"env": {
"name": "test",
"command": "echo 123",
"shell": "bash",
},
},
},
expErr: "",
},
{
description: "env shellArgs string",
input: raw.Step{
CommandMap: EnvType{
"env": {
"name": "test",
"command": "echo 123",
"shell": "bash",
"shellArgs": "-c",
},
},
},
expErr: "",
},
{
description: "env shellArgs list of strings",
input: raw.Step{
CommandMap: EnvType{
"env": {
"name": "test",
"command": "echo 123",
"shell": "bash",
"shellArgs": []interface{}{"-c", "--debug"},
},
},
},
expErr: "",
},
{
description: "apply extra_args",
input: raw.Step{
@@ -371,7 +412,7 @@ func TestStep_Validate(t *testing.T) {
},
},
},
expErr: "env steps only support keys \"name\", \"value\" and \"command\", found key \"abc\"",
expErr: "env steps only support keys \"name\", \"value\", \"command\", \"shell\" and \"shellArgs\", found key \"abc\"",
},
{
description: "env step with both command and value set",
@@ -386,6 +427,58 @@ func TestStep_Validate(t *testing.T) {
},
expErr: "env steps only support one of the \"value\" or \"command\" keys, found both",
},
{
description: "env step with shell set but not command",
input: raw.Step{
CommandMap: EnvType{
"env": {
"name": "name",
"shell": "bash",
},
},
},
expErr: "workflow steps only support \"shell\" key in combination with \"command\" key",
},
{
description: "env step with shellArgs set but not shell",
input: raw.Step{
CommandMap: EnvType{
"env": {
"name": "name",
"shellArgs": "-c",
},
},
},
expErr: "workflow steps only support \"shellArgs\" key in combination with \"shell\" key",
},
{
description: "run step with shellArgs is not list of strings",
input: raw.Step{
CommandMap: EnvType{
"run": {
"name": "name",
"command": "echo",
"shell": "shell",
"shellArgs": []int{42, 42},
},
},
},
expErr: "\"run\" step \"shellArgs\" option must be a string or a list of strings, found [42 42]\n",
},
{
description: "run step with shellArgs contain not strings",
input: raw.Step{
CommandMap: EnvType{
"run": {
"name": "name",
"command": "echo",
"shell": "shell",
"shellArgs": []interface{}{"-c", 42},
},
},
},
expErr: "\"run\" step \"shellArgs\" option must contain only strings, found 42\n",
},
{
// For atlantis.yaml v2, this wouldn't parse, but now there should
// be no error.
@@ -611,6 +704,6 @@ func TestStep_ToValid(t *testing.T) {
}
type MapType map[string]map[string][]string
type EnvType map[string]map[string]string
type RunType map[string]map[string]string
type MultiEnvType map[string]map[string]string
type EnvType map[string]map[string]interface{}
type RunType map[string]map[string]interface{}
type MultiEnvType map[string]map[string]interface{}

View File

@@ -189,6 +189,16 @@ type Stage struct {
Steps []Step
}
// CommandShell sets up the shell for command execution
type CommandShell struct {
Shell string
ShellArgs []string
}
func (s CommandShell) String() string {
return fmt.Sprintf("%s %s", s.Shell, strings.Join(s.ShellArgs, " "))
}
type Step struct {
StepName string
ExtraArgs []string
@@ -202,6 +212,8 @@ type Step struct {
EnvVarName string
// EnvVarValue is the value to set EnvVarName to.
EnvVarValue string
// The Shell to use for RunCommand execution.
RunShell *CommandShell
}
type Workflow struct {

View File

@@ -15,13 +15,20 @@ type EnvStepRunner struct {
// Run runs the env step command.
// value is the value for the environment variable. If set this is returned as
// the value. Otherwise command is run and its output is the value returned.
func (r *EnvStepRunner) Run(ctx command.ProjectContext, command string, value string, path string, envs map[string]string) (string, error) {
func (r *EnvStepRunner) Run(
ctx command.ProjectContext,
shell *valid.CommandShell,
command string,
value string,
path string,
envs map[string]string,
) (string, error) {
if value != "" {
return value, nil
}
// Pass `false` for streamOutput because this isn't interesting to the user reading the build logs
// in the web UI.
res, err := r.RunStepRunner.Run(ctx, command, path, envs, false, valid.PostProcessRunOutputShow)
res, err := r.RunStepRunner.Run(ctx, shell, command, path, envs, false, valid.PostProcessRunOutputShow)
// Trim newline from res to support running `echo env_value` which has
// a newline. We don't recommend users run echo -n env_value to remove the
// newline because -n doesn't work in the sh shell which is what we use

View File

@@ -77,7 +77,7 @@ func TestEnvStepRunner_Run(t *testing.T) {
TerraformVersion: tfVersion,
ProjectName: c.ProjectName,
}
value, err := envRunner.Run(ctx, c.Command, c.Value, tmpDir, map[string]string(nil))
value, err := envRunner.Run(ctx, nil, c.Command, c.Value, tmpDir, map[string]string(nil))
if c.ExpErr != "" {
ErrContains(t, c.ExpErr, err)
return

View File

@@ -9,6 +9,7 @@ import (
"time"
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/core/config/valid"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/terraform/ansi"
"github.com/runatlantis/atlantis/server/jobs"
@@ -33,10 +34,27 @@ type ShellCommandRunner struct {
outputHandler jobs.ProjectCommandOutputHandler
streamOutput bool
cmd *exec.Cmd
shell *valid.CommandShell
}
func NewShellCommandRunner(command string, environ []string, workingDir string, streamOutput bool, outputHandler jobs.ProjectCommandOutputHandler) *ShellCommandRunner {
cmd := exec.Command("sh", "-c", command) // #nosec
func NewShellCommandRunner(
shell *valid.CommandShell,
command string,
environ []string,
workingDir string,
streamOutput bool,
outputHandler jobs.ProjectCommandOutputHandler,
) *ShellCommandRunner {
if shell == nil {
shell = &valid.CommandShell{
Shell: "sh",
ShellArgs: []string{"-c"},
}
}
var args []string
args = append(args, shell.ShellArgs...)
args = append(args, command)
cmd := exec.Command(shell.Shell, args...) // #nosec
cmd.Env = environ
cmd.Dir = workingDir
@@ -46,6 +64,7 @@ func NewShellCommandRunner(command string, environ []string, workingDir string,
outputHandler: outputHandler,
streamOutput: streamOutput,
cmd: cmd,
shell: shell,
}
}
@@ -92,10 +111,10 @@ func (s *ShellCommandRunner) RunCommandAsync(ctx command.ProjectContext) (chan<-
stderr, _ := s.cmd.StderrPipe()
stdin, _ := s.cmd.StdinPipe()
ctx.Log.Debug("starting %q in %q", s.command, s.workingDir)
ctx.Log.Debug("starting '%s %q' in '%s'", s.shell.String(), s.command, s.workingDir)
err := s.cmd.Start()
if err != nil {
err = errors.Wrapf(err, "running %q in %q", s.command, s.workingDir)
err = errors.Wrapf(err, "running '%s %q' in '%s'", s.shell.String(), s.command, s.workingDir)
ctx.Log.Err(err.Error())
outCh <- Line{Err: err}
return
@@ -154,11 +173,13 @@ func (s *ShellCommandRunner) RunCommandAsync(ctx command.ProjectContext) (chan<-
// We're done now. Send an error if there was one.
if err != nil {
err = errors.Wrapf(err, "running %q in %q", s.command, s.workingDir)
err = errors.Wrapf(err, "running '%s %q' in '%s'",
s.shell.String(), s.command, s.workingDir)
log.Err(err.Error())
outCh <- Line{Err: err}
} else {
log.Info("successfully ran %q in %q", s.command, s.workingDir)
log.Info("successfully ran '%s %q' in '%s'",
s.shell.String(), s.command, s.workingDir)
}
}()

View File

@@ -54,7 +54,7 @@ func TestShellCommandRunner_Run(t *testing.T) {
expectedOutput := fmt.Sprintf("%s\n", strings.Join(c.ExpLines, "\n"))
// Run once with streaming enabled
runner := models.NewShellCommandRunner(c.Command, environ, cwd, true, projectCmdOutputHandler)
runner := models.NewShellCommandRunner(nil, c.Command, environ, cwd, true, projectCmdOutputHandler)
output, err := runner.Run(ctx)
Ok(t, err)
Equals(t, expectedOutput, output)
@@ -68,7 +68,7 @@ func TestShellCommandRunner_Run(t *testing.T) {
// command output handler should not have received anything
projectCmdOutputHandler = mocks.NewMockProjectCommandOutputHandler()
runner = models.NewShellCommandRunner(c.Command, environ, cwd, false, projectCmdOutputHandler)
runner = models.NewShellCommandRunner(nil, c.Command, environ, cwd, false, projectCmdOutputHandler)
output, err = runner.Run(ctx)
Ok(t, err)
Equals(t, expectedOutput, output)

View File

@@ -16,8 +16,15 @@ type MultiEnvStepRunner struct {
// Run runs the multienv step command.
// The command must return a json string containing the array of name-value pairs that are being added as extra environment variables
func (r *MultiEnvStepRunner) Run(ctx command.ProjectContext, command string, path string, envs map[string]string, postProcessOutput valid.PostProcessRunOutputOption) (string, error) {
res, err := r.RunStepRunner.Run(ctx, command, path, envs, false, postProcessOutput)
func (r *MultiEnvStepRunner) Run(
ctx command.ProjectContext,
shell *valid.CommandShell,
command string,
path string,
envs map[string]string,
postProcessOutput valid.PostProcessRunOutputOption,
) (string, error) {
res, err := r.RunStepRunner.Run(ctx, shell, command, path, envs, false, postProcessOutput)
if err != nil {
return "", err
}

View File

@@ -85,7 +85,7 @@ func TestMultiEnvStepRunner_Run(t *testing.T) {
ProjectName: c.ProjectName,
}
envMap := make(map[string]string)
value, err := multiEnvStepRunner.Run(ctx, c.Command, tmpDir, envMap, valid.PostProcessRunOutputShow)
value, err := multiEnvStepRunner.Run(ctx, nil, c.Command, tmpDir, envMap, valid.PostProcessRunOutputShow)
if c.ExpErr != "" {
ErrContains(t, c.ExpErr, err)
return

View File

@@ -22,7 +22,15 @@ type RunStepRunner struct {
ProjectCmdOutputHandler jobs.ProjectCommandOutputHandler
}
func (r *RunStepRunner) Run(ctx command.ProjectContext, command string, path string, envs map[string]string, streamOutput bool, postProcessOutput valid.PostProcessRunOutputOption) (string, error) {
func (r *RunStepRunner) Run(
ctx command.ProjectContext,
shell *valid.CommandShell,
command string,
path string,
envs map[string]string,
streamOutput bool,
postProcessOutput valid.PostProcessRunOutputOption,
) (string, error) {
tfVersion := r.DefaultTFVersion
if ctx.TerraformVersion != nil {
tfVersion = ctx.TerraformVersion
@@ -68,7 +76,7 @@ func (r *RunStepRunner) Run(ctx command.ProjectContext, command string, path str
finalEnvVars = append(finalEnvVars, fmt.Sprintf("%s=%s", key, val))
}
runner := models.NewShellCommandRunner(command, finalEnvVars, path, streamOutput, r.ProjectCmdOutputHandler)
runner := models.NewShellCommandRunner(shell, command, finalEnvVars, path, streamOutput, r.ProjectCmdOutputHandler)
output, err := runner.Run(ctx)
if postProcessOutput == valid.PostProcessRunOutputStripRefreshing {

View File

@@ -145,7 +145,7 @@ func TestRunStepRunner_Run(t *testing.T) {
ProjectName: c.ProjectName,
EscapedCommentArgs: []string{"-target=resource1", "-target=resource2"},
}
out, err := r.Run(ctx, c.Command, tmpDir, map[string]string{"test": "var"}, true, valid.PostProcessRunOutputShow)
out, err := r.Run(ctx, nil, c.Command, tmpDir, map[string]string{"test": "var"}, true, valid.PostProcessRunOutputShow)
if c.ExpErr != "" {
ErrContains(t, c.ExpErr, err)
return

View File

@@ -466,7 +466,7 @@ func (c *DefaultClient) RunCommandAsync(ctx command.ProjectContext, path string,
envVars = append(envVars, fmt.Sprintf("%s=%s", key, val))
}
runner := models.NewShellCommandRunner(cmd, envVars, path, true, c.projectCmdOutputHandler)
runner := models.NewShellCommandRunner(nil, cmd, envVars, path, true, c.projectCmdOutputHandler)
inCh, outCh := runner.RunCommandAsync(ctx)
return inCh, outCh
}

View File

@@ -344,7 +344,7 @@ func TestDefaultClient_RunCommandAsync_ExitOne(t *testing.T) {
_, outCh := client.RunCommandAsync(ctx, tmp, []string{"dying", "&&", "exit", "1"}, map[string]string{}, nil, "workspace")
out, err := waitCh(outCh)
ErrEquals(t, fmt.Sprintf(`running "echo dying && exit 1" in %q: exit status 1`, tmp), err)
ErrEquals(t, fmt.Sprintf(`running 'sh -c "echo dying && exit 1"' in '%s': exit status 1`, tmp), err)
// Test that we still get our output.
Equals(t, "dying", out)

View File

@@ -26,7 +26,7 @@ func NewMockCustomStepRunner(options ...pegomock.Option) *MockCustomStepRunner {
func (mock *MockCustomStepRunner) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }
func (mock *MockCustomStepRunner) FailHandler() pegomock.FailHandler { return mock.fail }
func (mock *MockCustomStepRunner) Run(ctx command.ProjectContext, cmd string, path string, envs map[string]string, streamOutput bool, postProcessOutput valid.PostProcessRunOutputOption) (string, error) {
func (mock *MockCustomStepRunner) Run(ctx command.ProjectContext, shell *valid.CommandShell, cmd string, path string, envs map[string]string, streamOutput bool, postProcessOutput valid.PostProcessRunOutputOption) (string, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockCustomStepRunner().")
}
@@ -82,7 +82,7 @@ type VerifierMockCustomStepRunner struct {
timeout time.Duration
}
func (verifier *VerifierMockCustomStepRunner) Run(ctx command.ProjectContext, cmd string, path string, envs map[string]string, streamOutput bool, postProcessOutput valid.PostProcessRunOutputOption) *MockCustomStepRunner_Run_OngoingVerification {
func (verifier *VerifierMockCustomStepRunner) Run(ctx command.ProjectContext, shell *valid.CommandShell, cmd string, path string, envs map[string]string, streamOutput bool, postProcessOutput valid.PostProcessRunOutputOption) *MockCustomStepRunner_Run_OngoingVerification {
params := []pegomock.Param{ctx, cmd, path, envs, streamOutput, postProcessOutput}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Run", params, verifier.timeout)
return &MockCustomStepRunner_Run_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}

View File

@@ -4,10 +4,12 @@
package mocks
import (
pegomock "github.com/petergtz/pegomock/v4"
command "github.com/runatlantis/atlantis/server/events/command"
"reflect"
"time"
pegomock "github.com/petergtz/pegomock/v4"
"github.com/runatlantis/atlantis/server/core/config/valid"
command "github.com/runatlantis/atlantis/server/events/command"
)
type MockEnvStepRunner struct {
@@ -25,7 +27,7 @@ func NewMockEnvStepRunner(options ...pegomock.Option) *MockEnvStepRunner {
func (mock *MockEnvStepRunner) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }
func (mock *MockEnvStepRunner) FailHandler() pegomock.FailHandler { return mock.fail }
func (mock *MockEnvStepRunner) Run(ctx command.ProjectContext, cmd string, value string, path string, envs map[string]string) (string, error) {
func (mock *MockEnvStepRunner) Run(ctx command.ProjectContext, shell *valid.CommandShell, cmd string, value string, path string, envs map[string]string) (string, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockEnvStepRunner().")
}
@@ -81,7 +83,7 @@ type VerifierMockEnvStepRunner struct {
timeout time.Duration
}
func (verifier *VerifierMockEnvStepRunner) Run(ctx command.ProjectContext, cmd string, value string, path string, envs map[string]string) *MockEnvStepRunner_Run_OngoingVerification {
func (verifier *VerifierMockEnvStepRunner) Run(ctx command.ProjectContext, shell *valid.CommandShell, cmd string, value string, path string, envs map[string]string) *MockEnvStepRunner_Run_OngoingVerification {
params := []pegomock.Param{ctx, cmd, value, path, envs}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Run", params, verifier.timeout)
return &MockEnvStepRunner_Run_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}

View File

@@ -65,20 +65,42 @@ type StepRunner interface {
// CustomStepRunner runs custom run steps.
type CustomStepRunner interface {
// Run cmd in path.
Run(ctx command.ProjectContext, cmd string, path string, envs map[string]string, streamOutput bool, postProcessOutput valid.PostProcessRunOutputOption) (string, error)
Run(
ctx command.ProjectContext,
shell *valid.CommandShell,
cmd string,
path string,
envs map[string]string,
streamOutput bool,
postProcessOutput valid.PostProcessRunOutputOption,
) (string, error)
}
//go:generate pegomock generate --package mocks -o mocks/mock_env_step_runner.go EnvStepRunner
// EnvStepRunner runs env steps.
type EnvStepRunner interface {
Run(ctx command.ProjectContext, cmd string, value string, path string, envs map[string]string) (string, error)
Run(
ctx command.ProjectContext,
shell *valid.CommandShell,
cmd string,
value string,
path string,
envs map[string]string,
) (string, error)
}
// MultiEnvStepRunner runs multienv steps.
type MultiEnvStepRunner interface {
// Run cmd in path.
Run(ctx command.ProjectContext, cmd string, path string, envs map[string]string, postProcessOutput valid.PostProcessRunOutputOption) (string, error)
Run(
ctx command.ProjectContext,
shell *valid.CommandShell,
cmd string,
path string,
envs map[string]string,
postProcessOutput valid.PostProcessRunOutputOption,
) (string, error)
}
//go:generate pegomock generate --package mocks -o mocks/mock_webhooks_sender.go WebhooksSender
@@ -790,15 +812,15 @@ func (p *DefaultProjectCommandRunner) runSteps(steps []valid.Step, ctx command.P
case "state_rm":
out, err = p.StateRmStepRunner.Run(ctx, step.ExtraArgs, absPath, envs)
case "run":
out, err = p.RunStepRunner.Run(ctx, step.RunCommand, absPath, envs, true, step.Output)
out, err = p.RunStepRunner.Run(ctx, step.RunShell, step.RunCommand, absPath, envs, true, step.Output)
case "env":
out, err = p.EnvStepRunner.Run(ctx, step.RunCommand, step.EnvVarValue, absPath, envs)
out, err = p.EnvStepRunner.Run(ctx, step.RunShell, step.RunCommand, step.EnvVarValue, absPath, envs)
envs[step.EnvVarName] = out
// We reset out to the empty string because we don't want it to
// be printed to the PR, it's solely to set the environment variable.
out = ""
case "multienv":
out, err = p.MultiEnvStepRunner.Run(ctx, step.RunCommand, absPath, envs, step.Output)
out, err = p.MultiEnvStepRunner.Run(ctx, step.RunShell, step.RunCommand, absPath, envs, step.Output)
}
if out != "" {

View File

@@ -99,7 +99,7 @@ func TestDefaultProjectCommandRunner_Plan(t *testing.T) {
When(mockInit.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("init", nil)
When(mockPlan.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("plan", nil)
When(mockApply.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("apply", nil)
When(mockRun.Run(ctx, "", repoDir, expEnvs, true, "")).ThenReturn("run", nil)
When(mockRun.Run(ctx, nil, "", repoDir, expEnvs, true, "")).ThenReturn("run", nil)
res := runner.Plan(ctx)
Assert(t, res.PlanSuccess != nil, "exp plan success")
@@ -115,7 +115,7 @@ func TestDefaultProjectCommandRunner_Plan(t *testing.T) {
case "apply":
mockApply.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)
case "run":
mockRun.VerifyWasCalledOnce().Run(ctx, "", repoDir, expEnvs, true, "")
mockRun.VerifyWasCalledOnce().Run(ctx, nil, "", repoDir, expEnvs, true, "")
}
}
}
@@ -455,8 +455,8 @@ func TestDefaultProjectCommandRunner_Apply(t *testing.T) {
When(mockInit.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("init", nil)
When(mockPlan.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("plan", nil)
When(mockApply.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("apply", nil)
When(mockRun.Run(ctx, "", repoDir, expEnvs, true, "")).ThenReturn("run", nil)
When(mockEnv.Run(ctx, "", "value", repoDir, make(map[string]string))).ThenReturn("value", nil)
When(mockRun.Run(ctx, nil, "", repoDir, expEnvs, true, "")).ThenReturn("run", nil)
When(mockEnv.Run(ctx, nil, "", "value", repoDir, make(map[string]string))).ThenReturn("value", nil)
res := runner.Apply(ctx)
Equals(t, c.expOut, res.ApplySuccess)
@@ -471,9 +471,9 @@ func TestDefaultProjectCommandRunner_Apply(t *testing.T) {
case "apply":
mockApply.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)
case "run":
mockRun.VerifyWasCalledOnce().Run(ctx, "", repoDir, expEnvs, true, "")
mockRun.VerifyWasCalledOnce().Run(ctx, nil, "", repoDir, expEnvs, true, "")
case "env":
mockEnv.VerifyWasCalledOnce().Run(ctx, "", "value", repoDir, expEnvs)
mockEnv.VerifyWasCalledOnce().Run(ctx, nil, "", "value", repoDir, expEnvs)
}
}
})