Skip to content

Commit

Permalink
feat: Add an output option to filter output for custom run steps (r…
Browse files Browse the repository at this point in the history
…unatlantis#3518)

* feat: Add an `output` option to filter output for custom run steps
  • Loading branch information
mightyguava authored and ijames-gc committed Feb 13, 2024
1 parent daf841c commit 5ef523b
Show file tree
Hide file tree
Showing 12 changed files with 175 additions and 60 deletions.
41 changes: 33 additions & 8 deletions runatlantis.io/docs/custom-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,11 @@ workflows:
myworkflow:
plan:
steps:
- run: terraform init -input=false
# If you want to hide command output from Atlantis's PR comment, use
# the output option on the run step's expanded form.
- run:
command: terraform init -input=false
output: hide
# If you're using workspaces you need to select the workspace using the
# $WORKSPACE environment variable.
Expand Down Expand Up @@ -264,7 +268,9 @@ workflows:
# Reduce Terraform suggestion output
name: TF_IN_AUTOMATION
value: 'true'
- run: terragrunt plan -input=false -out=$PLANFILE
- run:
command: terragrunt plan -input=false -out=$PLANFILE
output: strip_refreshing
- run: terragrunt show -json $PLANFILE > $SHOWFILE
apply:
steps:
Expand Down Expand Up @@ -297,7 +303,9 @@ workflows:
# Reduce Terraform suggestion output
name: TF_IN_AUTOMATION
value: 'true'
- run: terragrunt plan -out $PLANFILE
- run:
command: terragrunt plan -input=false -out=$PLANFILE
output: strip_refreshing
apply:
steps:
- env:
Expand Down Expand Up @@ -451,14 +459,28 @@ A map from string to `extra_args` for a built-in command with extra arguments.
| init/plan/apply/import/state_rm | map[`extra_args` -> array[string]] | none | no | Use a built-in command and append `extra_args`. Only `init`, `plan`, `apply`, `import` and `state_rm` are supported as keys and only `extra_args` is supported as a value |

#### Custom `run` Command
Or a custom command
A custom command can be written in 2 ways

Compact:
```yaml
- run: custom-command
- run: custom-command arg1 arg2
```
| Key | Type | Default | Required | Description |
|-----|--------|---------|----------|----------------------|
| run | string | none | no | Run a custom command |

Full
```yaml
- run:
command: custom-command arg1 arg2
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.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 |

::: tip Notes
* `run` steps in the main `workflow` are executed with the following environment variables:
note: these variables are not available to `pre` or `post` workflows
Expand Down Expand Up @@ -513,9 +535,12 @@ as the environment variable value.
name: ENV_NAME_2
command: 'echo "dynamic-value-$(date)"'
```
| Key | Type | Default | Required | Description |
|-----------------|------------------------------------|---------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------|
| env | map[`name` -> string, `value` -> string, `command` -> string] | none | no | Set environment variables for subsequent steps |
| Key | Type | Default | Required | Description |
|-----------------|-----------------------|---------|----------|-----------------------------------------------------------------------------------------------------------------|
| env | map[string -> string] | none | no | Set environment variables for subsequent steps |
| 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` |

::: tip Notes
* `env` `command`'s can use any of the built-in environment variables available
Expand Down
3 changes: 3 additions & 0 deletions runatlantis.io/docs/repo-level-atlantis-yaml.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ workflows:
plan:
steps:
- run: my-custom-command arg1 arg2
- run:
command: my-custom-command arg1 arg2
output: hide
- init
- plan:
extra_args: ["-lock", "false"]
Expand Down
82 changes: 62 additions & 20 deletions server/core/config/raw/step.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const (
NameArgKey = "name"
CommandArgKey = "command"
ValueArgKey = "value"
OutputArgKey = "output"
RunStepName = "run"
PlanStepName = "plan"
ShowStepName = "show"
Expand All @@ -34,15 +35,18 @@ const (
// - plan
// - policy_check
//
// 2. A map for an env step with name and command or value
// 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
// name: test
// command: echo 312
// value: value
// - 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]
// extra_args: [-var-file=staging.tfvars]
//
// 4. A map for a custom run command:
// - run: my custom command
Expand All @@ -53,8 +57,8 @@ 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
// Env will be set in case #2 above.
Env map[string]map[string]string
// EnvOrRun will be set in case #2 above.
EnvOrRun 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.
Expand Down Expand Up @@ -142,7 +146,7 @@ func (s Step) Validate() error {
return nil
}

envStep := func(value interface{}) error {
envOrRunStep := func(value interface{}) error {
elem := value.(map[string]map[string]string)
var keys []string
for k := range elem {
Expand All @@ -155,10 +159,15 @@ func (s Step) Validate() error {
return fmt.Errorf("step element can only contain a single key, found %d: %s",
len(keys), strings.Join(keys, ","))
}
for stepName, args := range elem {
if stepName != EnvStepName {
return fmt.Errorf("%q is not a valid step type", stepName)
}
if len(keys) == 0 {
return fmt.Errorf("step element must contain at least 1 key")
}

stepName := keys[0]
args := elem[keys[0]]

switch stepName {
case EnvStepName:
var argKeys []string
for k := range args {
argKeys = append(argKeys, k)
Expand All @@ -183,7 +192,35 @@ func (s Step) Validate() error {
return fmt.Errorf("env steps only support one of the %q or %q keys, found both",
ValueArgKey, CommandArgKey)
}
case RunStepName:
argsCopy := make(map[string]string)
for k, v := range args {
argsCopy[k] = v
}
args = argsCopy
if _, ok := args[CommandArgKey]; !ok {
return fmt.Errorf("run step must have a %q key set", CommandArgKey)
}
delete(args, CommandArgKey)
if v, ok := args[OutputArgKey]; ok {
if !(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)
}
}
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("run steps only support keys %q, %q and %q, found extra keys %q", RunStepName, CommandArgKey, OutputArgKey, strings.Join(argKeys, ","))
}
default:
return fmt.Errorf("%q is not a valid step type", stepName)
}

return nil
}

Expand Down Expand Up @@ -214,8 +251,8 @@ func (s Step) Validate() error {
if len(s.Map) > 0 {
return validation.Validate(s.Map, validation.By(extraArgs))
}
if len(s.Env) > 0 {
return validation.Validate(s.Env, validation.By(envStep))
if len(s.EnvOrRun) > 0 {
return validation.Validate(s.EnvOrRun, validation.By(envOrRunStep))
}
if len(s.StringVal) > 0 {
return validation.Validate(s.StringVal, validation.By(runStep))
Expand All @@ -232,16 +269,21 @@ func (s Step) ToValid() valid.Step {
}

// This will trigger in case #2 (see Step docs).
if len(s.Env) > 0 {
if len(s.EnvOrRun) > 0 {
// 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.Env {
return valid.Step{
for stepName, stepArgs := range s.EnvOrRun {
step := valid.Step{
StepName: stepName,
EnvVarName: stepArgs[NameArgKey],
RunCommand: stepArgs[CommandArgKey],
EnvVarValue: stepArgs[ValueArgKey],
Output: valid.PostProcessRunOutputOption(stepArgs[OutputArgKey]),
}
if step.StepName == RunStepName && step.Output == "" {
step.Output = valid.PostProcessRunOutputShow
}
return step
}
}

Expand Down Expand Up @@ -314,7 +356,7 @@ func (s *Step) unmarshalGeneric(unmarshal func(interface{}) error) error {
var envStep map[string]map[string]string
err = unmarshal(&envStep)
if err == nil {
s.Env = envStep
s.EnvOrRun = envStep
return nil
}

Expand All @@ -337,8 +379,8 @@ func (s Step) marshalGeneric() (interface{}, error) {
return s.StringVal, nil
} else if len(s.Map) != 0 {
return s.Map, nil
} else if len(s.Env) != 0 {
return s.Env, nil
} else if len(s.EnvOrRun) != 0 {
return s.EnvOrRun, nil
} else if s.Key != nil {
return s.Key, nil
}
Expand Down
38 changes: 27 additions & 11 deletions server/core/config/raw/step_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ env:
value: direct_value
name: test`,
exp: raw.Step{
Env: EnvType{
EnvOrRun: EnvOrRunType{
"env": {
"value": "direct_value",
"name": "test",
Expand All @@ -96,7 +96,7 @@ env:
command: echo 123
name: test`,
exp: raw.Step{
Env: EnvType{
EnvOrRun: EnvOrRunType{
"env": {
"command": "echo 123",
"name": "test",
Expand Down Expand Up @@ -137,7 +137,7 @@ key: value`,
Key: nil,
Map: nil,
StringVal: nil,
Env: nil,
EnvOrRun: nil,
},
},

Expand Down Expand Up @@ -227,7 +227,7 @@ func TestStep_Validate(t *testing.T) {
{
description: "env",
input: raw.Step{
Env: EnvType{
EnvOrRun: EnvOrRunType{
"env": {
"name": "test",
"command": "echo 123",
Expand Down Expand Up @@ -283,7 +283,7 @@ func TestStep_Validate(t *testing.T) {
{
description: "multiple keys in env",
input: raw.Step{
Env: EnvType{
EnvOrRun: EnvOrRunType{
"key1": nil,
"key2": nil,
},
Expand Down Expand Up @@ -312,7 +312,7 @@ func TestStep_Validate(t *testing.T) {
{
description: "invalid key in env",
input: raw.Step{
Env: EnvType{
EnvOrRun: EnvOrRunType{
"invalid": nil,
},
},
Expand Down Expand Up @@ -353,7 +353,7 @@ func TestStep_Validate(t *testing.T) {
{
description: "env step with no name key set",
input: raw.Step{
Env: EnvType{
EnvOrRun: EnvOrRunType{
"env": {
"value": "value",
},
Expand All @@ -364,7 +364,7 @@ func TestStep_Validate(t *testing.T) {
{
description: "env step with invalid key",
input: raw.Step{
Env: EnvType{
EnvOrRun: EnvOrRunType{
"env": {
"abc": "",
"invalid2": "",
Expand All @@ -376,7 +376,7 @@ func TestStep_Validate(t *testing.T) {
{
description: "env step with both command and value set",
input: raw.Step{
Env: EnvType{
EnvOrRun: EnvOrRunType{
"env": {
"name": "name",
"command": "command",
Expand Down Expand Up @@ -454,7 +454,7 @@ func TestStep_ToValid(t *testing.T) {
{
description: "env step",
input: raw.Step{
Env: EnvType{
EnvOrRun: EnvOrRunType{
"env": {
"name": "test",
"command": "echo 123",
Expand Down Expand Up @@ -558,6 +558,22 @@ func TestStep_ToValid(t *testing.T) {
RunCommand: "my 'run command'",
},
},
{
description: "run step with output",
input: raw.Step{
EnvOrRun: EnvOrRunType{
"run": {
"command": "my 'run command'",
"output": "hide",
},
},
},
exp: valid.Step{
StepName: "run",
RunCommand: "my 'run command'",
Output: "hide",
},
},
}
for _, c := range cases {
t.Run(c.description, func(t *testing.T) {
Expand All @@ -567,4 +583,4 @@ func TestStep_ToValid(t *testing.T) {
}

type MapType map[string]map[string][]string
type EnvType map[string]map[string]string
type EnvOrRunType map[string]map[string]string
11 changes: 11 additions & 0 deletions server/core/config/valid/repo_cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,15 @@ type Autoplan struct {
Enabled bool
}

// PostProcessRunOutputOption is an enum of options for post-processing RunCommand output
type PostProcessRunOutputOption string

const (
PostProcessRunOutputShow = "show"
PostProcessRunOutputHide = "hide"
PostProcessRunOutputStripRefreshing = "strip_refreshing"
)

type Stage struct {
Steps []Step
}
Expand All @@ -160,6 +169,8 @@ type Step struct {
// RunCommand is either a custom run step or the command to run
// during an env step to populate the environment variable dynamically.
RunCommand string
// Output is option for post-processing a RunCommand output
Output PostProcessRunOutputOption
// EnvVarName is the name of the
// environment variable that should be set by this step.
EnvVarName string
Expand Down
Loading

0 comments on commit 5ef523b

Please sign in to comment.