Skip to content

Commit

Permalink
feat: set the shellArgs for workflow command steps
Browse files Browse the repository at this point in the history
Signed-off-by: anryko <[email protected]>
  • Loading branch information
anryko committed Oct 30, 2024
1 parent d4e6d38 commit c7cbbe1
Show file tree
Hide file tree
Showing 18 changed files with 290 additions and 127 deletions.
28 changes: 20 additions & 8 deletions runatlantis.io/docs/custom-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -600,14 +600,18 @@ Full
- run:
command: custom-command arg1 arg2
shell: sh
shellArgs:
- "--debug"
- "-c"
output: show
```

| Key | Type | Default | Required | Description |
|-----|--------------------------------------------------------------|---------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 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 (valid values are "sh" and "bash") | |
| 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
Expand Down Expand Up @@ -670,6 +674,9 @@ as the environment variable value.
name: ENV_NAME_3
command: echo ${DIR%$REPO_REL_DIR}
shell: bash
shellArgs:
- "--verbose"
- "-c"
```

| Key | Type | Default | Required | Description |
Expand All @@ -678,7 +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 (valid values are "sh" and "bash"). Cannot be set without `command`| |
| 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

Expand Down Expand Up @@ -707,15 +715,19 @@ Full:
- 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.shell | string | "sh" | no | Name of the shell to use for command execution (valid values are "sh" and "bash") | |
| 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`
Expand Down
195 changes: 124 additions & 71 deletions server/core/config/raw/step.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const (
ImportStepName = "import"
StateRmStepName = "state_rm"
ShellArgKey = "shell"
ShellArgsArgKey = "shellArgs"
)

/*
Expand All @@ -49,9 +50,12 @@ Step represents a single action/command to perform. In YAML, it can be set as
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
Expand All @@ -70,12 +74,12 @@ 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 {
Expand Down Expand Up @@ -152,15 +156,16 @@ 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)
}
}
}
return nil
}

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)
Expand All @@ -179,47 +184,73 @@ 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 && k != ShellArgKey {
return fmt.Errorf("env steps only support keys %q, %q, %q and %q, found key %q",
NameArgKey, ValueArgKey, CommandArgKey, ShellArgKey, 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)
}
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)
}
if utils.SlicesContains(argKeys, ShellArgKey) && !utils.SlicesContains(argKeys, CommandArgKey) {
return fmt.Errorf("env steps only support %q key in combination with %q key",
ShellArgKey, 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 {
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",
Expand All @@ -231,28 +262,22 @@ func (s Step) Validate() error {
OutputArgKey, valid.PostProcessRunOutputShow, valid.PostProcessRunOutputHide)
}
}
delete(args, OutputArgKey)
if v, ok := args[ShellArgKey]; ok {
if !utils.SlicesContains(valid.AllowedRunShellValues, v) {
return fmt.Errorf("run step %q value %q is not supported, supported values are: [%s]",
ShellArgKey, v, strings.Join(valid.AllowedRunShellValues, ", "))
}
}
delete(args, ShellArgKey)
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, %q and %q, found extra keys %q",
stepName, CommandArgKey, OutputArgKey, ShellArgKey, 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
}

Expand Down Expand Up @@ -305,17 +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]),
RunShell: stepArgs[ShellArgKey],
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
}
}
Expand Down Expand Up @@ -369,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]
Expand All @@ -381,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
}

Expand Down
Loading

0 comments on commit c7cbbe1

Please sign in to comment.