diff --git a/runatlantis.io/docs/custom-workflows.md b/runatlantis.io/docs/custom-workflows.md
index f2edd827ae..af655abf26 100644
--- a/runatlantis.io/docs/custom-workflows.md
+++ b/runatlantis.io/docs/custom-workflows.md
@@ -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
*`show` - preserve the full output
* `hide` - hide output from comment (still visible in the real-time streaming output)
* `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`
diff --git a/server/core/config/raw/step.go b/server/core/config/raw/step.go
index 581be49c64..6ada93488c 100644
--- a/server/core/config/raw/step.go
+++ b/server/core/config/raw/step.go
@@ -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]]
- switch stepName {
- case EnvStepName:
- var argKeys []string
- for k := range args {
- argKeys = append(argKeys, k)
+ 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)
+ }
}
- // Sort so tests can be deterministic.
- sort.Strings(argKeys)
+ 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:
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(args, OutputArgKey)
- if len(args) > 0 {
- var argKeys []string
- for k := range args {
- argKeys = append(argKeys, k)
+ 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)
}
- // 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
}
diff --git a/server/core/config/raw/step_test.go b/server/core/config/raw/step_test.go
index f47c497e6f..f8b9ae8b11 100644
--- a/server/core/config/raw/step_test.go
+++ b/server/core/config/raw/step_test.go
@@ -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{}
diff --git a/server/core/config/valid/repo_cfg.go b/server/core/config/valid/repo_cfg.go
index e5a8378bd7..e42e60158b 100644
--- a/server/core/config/valid/repo_cfg.go
+++ b/server/core/config/valid/repo_cfg.go
@@ -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 {
diff --git a/server/core/runtime/env_step_runner.go b/server/core/runtime/env_step_runner.go
index eb6556c182..5fa865fefd 100644
--- a/server/core/runtime/env_step_runner.go
+++ b/server/core/runtime/env_step_runner.go
@@ -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
diff --git a/server/core/runtime/env_step_runner_test.go b/server/core/runtime/env_step_runner_test.go
index a26b5c1a93..0fe86f77f0 100644
--- a/server/core/runtime/env_step_runner_test.go
+++ b/server/core/runtime/env_step_runner_test.go
@@ -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
diff --git a/server/core/runtime/models/shell_command_runner.go b/server/core/runtime/models/shell_command_runner.go
index 3dcd56dd8a..7271f6789e 100644
--- a/server/core/runtime/models/shell_command_runner.go
+++ b/server/core/runtime/models/shell_command_runner.go
@@ -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)
}
}()
diff --git a/server/core/runtime/models/shell_command_runner_test.go b/server/core/runtime/models/shell_command_runner_test.go
index 0555c7144c..e8edc32fb1 100644
--- a/server/core/runtime/models/shell_command_runner_test.go
+++ b/server/core/runtime/models/shell_command_runner_test.go
@@ -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)
diff --git a/server/core/runtime/multienv_step_runner.go b/server/core/runtime/multienv_step_runner.go
index 17e2ae1963..6e4434111f 100644
--- a/server/core/runtime/multienv_step_runner.go
+++ b/server/core/runtime/multienv_step_runner.go
@@ -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
}
diff --git a/server/core/runtime/multienv_step_runner_test.go b/server/core/runtime/multienv_step_runner_test.go
index adf51a8b60..360adce3f5 100644
--- a/server/core/runtime/multienv_step_runner_test.go
+++ b/server/core/runtime/multienv_step_runner_test.go
@@ -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
diff --git a/server/core/runtime/run_step_runner.go b/server/core/runtime/run_step_runner.go
index 1e3335762c..76629ba460 100644
--- a/server/core/runtime/run_step_runner.go
+++ b/server/core/runtime/run_step_runner.go
@@ -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 {
diff --git a/server/core/runtime/run_step_runner_test.go b/server/core/runtime/run_step_runner_test.go
index d011254a09..4672fa2bb0 100644
--- a/server/core/runtime/run_step_runner_test.go
+++ b/server/core/runtime/run_step_runner_test.go
@@ -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
diff --git a/server/core/terraform/terraform_client.go b/server/core/terraform/terraform_client.go
index 09cf88f564..cd2a0d8ad7 100644
--- a/server/core/terraform/terraform_client.go
+++ b/server/core/terraform/terraform_client.go
@@ -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
}
diff --git a/server/core/terraform/terraform_client_internal_test.go b/server/core/terraform/terraform_client_internal_test.go
index 6dd4c89e85..8c6be3ee43 100644
--- a/server/core/terraform/terraform_client_internal_test.go
+++ b/server/core/terraform/terraform_client_internal_test.go
@@ -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)
diff --git a/server/events/mocks/mock_custom_step_runner.go b/server/events/mocks/mock_custom_step_runner.go
index 8805706322..7662d22ba0 100644
--- a/server/events/mocks/mock_custom_step_runner.go
+++ b/server/events/mocks/mock_custom_step_runner.go
@@ -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}
diff --git a/server/events/mocks/mock_env_step_runner.go b/server/events/mocks/mock_env_step_runner.go
index bfc7f97a57..0d99311987 100644
--- a/server/events/mocks/mock_env_step_runner.go
+++ b/server/events/mocks/mock_env_step_runner.go
@@ -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}
diff --git a/server/events/project_command_runner.go b/server/events/project_command_runner.go
index 153269c7e2..26d4dc2cc2 100644
--- a/server/events/project_command_runner.go
+++ b/server/events/project_command_runner.go
@@ -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 != "" {
diff --git a/server/events/project_command_runner_test.go b/server/events/project_command_runner_test.go
index d241d44569..68548efdd0 100644
--- a/server/events/project_command_runner_test.go
+++ b/server/events/project_command_runner_test.go
@@ -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)
}
}
})