Skip to content

Commit

Permalink
feat: optionally hide multienv output (runatlantis#4422)
Browse files Browse the repository at this point in the history
Co-authored-by: PePe Amengual <[email protected]>
Co-authored-by: Rui Chen <[email protected]>
  • Loading branch information
3 people authored and terakoya76 committed Dec 31, 2024
1 parent b0e122f commit 8bc06f4
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 57 deletions.
24 changes: 18 additions & 6 deletions runatlantis.io/docs/custom-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -625,18 +625,30 @@ as the environment variable value.
The `multienv` command allows you to set dynamic number of multiple environment variables that will be available
to all steps defined **below** the `multienv` step.

Compact:
```yaml
- multienv: custom-command
```
| Key | Type | Default | Required | Description |
|----------|--------|---------|----------|------------------------------------------------------------|
| multienv | string | none | no | Run a custom command and add printed environment variables |

| Key | Type | Default | Required | Description |
|----------|--------|---------|----------|--------------------------------------------------------------------------------|
| multienv | string | none | no | Run a custom command and add set environment variables according to the result |
Full:
```yaml
- multienv:
command: custom-command
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 |

The result of the executed command must have a fixed format:
EnvVar1Name=value1,EnvVar2Name=value2,EnvVar3Name=value3
The output of the command execution must have the following format:
`EnvVar1Name=value1,EnvVar2Name=value2,EnvVar3Name=value3`

The name-value pairs in the result are added as environment variables if success is true otherwise the workflow execution stops with error and the errorMessage is getting displayed.
The name-value pairs in the output are added as environment variables if command execution is successful, otherwise the workflow execution is interrupted with an error and the errorMessage is returned.

::: tip Notes

Expand Down
39 changes: 22 additions & 17 deletions server/core/config/raw/step.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ const (
// name: test
// command: echo 312
// value: value
// - multienv:
// command: envs.sh
// outpiut: hide
// - run:
// command: my custom command
// output: hide
Expand All @@ -57,8 +60,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
// EnvOrRun will be set in case #2 above.
EnvOrRun map[string]map[string]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.
Expand Down Expand Up @@ -146,7 +149,7 @@ func (s Step) Validate() error {
return nil
}

envOrRunStep := func(value interface{}) error {
envOrRunOrMultiEnvStep := func(value interface{}) error {
elem := value.(map[string]map[string]string)
var keys []string
for k := range elem {
Expand Down Expand Up @@ -192,19 +195,21 @@ 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:
case RunStepName, MultiEnvStepName:
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)
return fmt.Errorf("%q step must have a %q key set", stepName, CommandArgKey)
}
delete(args, CommandArgKey)
if v, ok := args[OutputArgKey]; ok {
if !(v == valid.PostProcessRunOutputShow || v == valid.PostProcessRunOutputHide || v == valid.PostProcessRunOutputStripRefreshing) {
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)
Expand All @@ -215,7 +220,7 @@ func (s Step) Validate() error {
}
// 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, ","))
return fmt.Errorf("%q steps only support keys %q and %q, found extra keys %q", stepName, CommandArgKey, OutputArgKey, strings.Join(argKeys, ","))
}
default:
return fmt.Errorf("%q is not a valid step type", stepName)
Expand All @@ -224,7 +229,7 @@ func (s Step) Validate() error {
return nil
}

runStep := func(value interface{}) error {
runOrMultiEnvStep := func(value interface{}) error {
elem := value.(map[string]string)
var keys []string
for k := range elem {
Expand All @@ -238,7 +243,7 @@ func (s Step) Validate() error {
len(keys), strings.Join(keys, ","))
}
for stepName := range elem {
if stepName != RunStepName && stepName != MultiEnvStepName {
if !(stepName == RunStepName || stepName == MultiEnvStepName) {
return fmt.Errorf("%q is not a valid step type", stepName)
}
}
Expand All @@ -251,11 +256,11 @@ func (s Step) Validate() error {
if len(s.Map) > 0 {
return validation.Validate(s.Map, validation.By(extraArgs))
}
if len(s.EnvOrRun) > 0 {
return validation.Validate(s.EnvOrRun, validation.By(envOrRunStep))
if len(s.CommandMap) > 0 {
return validation.Validate(s.CommandMap, validation.By(envOrRunOrMultiEnvStep))
}
if len(s.StringVal) > 0 {
return validation.Validate(s.StringVal, validation.By(runStep))
return validation.Validate(s.StringVal, validation.By(runOrMultiEnvStep))
}
return errors.New("step element is empty")
}
Expand All @@ -269,10 +274,10 @@ func (s Step) ToValid() valid.Step {
}

// This will trigger in case #2 (see Step docs).
if len(s.EnvOrRun) > 0 {
if len(s.CommandMap) > 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.EnvOrRun {
for stepName, stepArgs := range s.CommandMap {
step := valid.Step{
StepName: stepName,
EnvVarName: stepArgs[NameArgKey],
Expand Down Expand Up @@ -356,7 +361,7 @@ func (s *Step) unmarshalGeneric(unmarshal func(interface{}) error) error {
var envStep map[string]map[string]string
err = unmarshal(&envStep)
if err == nil {
s.EnvOrRun = envStep
s.CommandMap = envStep
return nil
}

Expand All @@ -379,8 +384,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.EnvOrRun) != 0 {
return s.EnvOrRun, nil
} else if len(s.CommandMap) != 0 {
return s.CommandMap, nil
} else if s.Key != nil {
return s.Key, nil
}
Expand Down
60 changes: 45 additions & 15 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{
EnvOrRun: EnvOrRunType{
CommandMap: EnvType{
"env": {
"value": "direct_value",
"name": "test",
Expand All @@ -96,7 +96,7 @@ env:
command: echo 123
name: test`,
exp: raw.Step{
EnvOrRun: EnvOrRunType{
CommandMap: EnvType{
"env": {
"command": "echo 123",
"name": "test",
Expand Down Expand Up @@ -134,10 +134,10 @@ key: value`,
description: "empty",
input: "",
exp: raw.Step{
Key: nil,
Map: nil,
StringVal: nil,
EnvOrRun: nil,
Key: nil,
Map: nil,
StringVal: nil,
CommandMap: nil,
},
},

Expand Down Expand Up @@ -227,7 +227,7 @@ func TestStep_Validate(t *testing.T) {
{
description: "env",
input: raw.Step{
EnvOrRun: EnvOrRunType{
CommandMap: EnvType{
"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{
EnvOrRun: EnvOrRunType{
CommandMap: EnvType{
"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{
EnvOrRun: EnvOrRunType{
CommandMap: EnvType{
"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{
EnvOrRun: EnvOrRunType{
CommandMap: EnvType{
"env": {
"value": "value",
},
Expand All @@ -364,7 +364,7 @@ func TestStep_Validate(t *testing.T) {
{
description: "env step with invalid key",
input: raw.Step{
EnvOrRun: EnvOrRunType{
CommandMap: EnvType{
"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{
EnvOrRun: EnvOrRunType{
CommandMap: EnvType{
"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{
EnvOrRun: EnvOrRunType{
CommandMap: EnvType{
"env": {
"name": "test",
"command": "echo 123",
Expand Down Expand Up @@ -561,7 +561,7 @@ func TestStep_ToValid(t *testing.T) {
{
description: "run step with output",
input: raw.Step{
EnvOrRun: EnvOrRunType{
CommandMap: RunType{
"run": {
"command": "my 'run command'",
"output": "hide",
Expand All @@ -574,6 +574,34 @@ func TestStep_ToValid(t *testing.T) {
Output: "hide",
},
},
{
description: "multienv step",
input: raw.Step{
StringVal: map[string]string{
"multienv": "envs.sh",
},
},
exp: valid.Step{
StepName: "multienv",
RunCommand: "envs.sh",
},
},
{
description: "multienv step with output",
input: raw.Step{
CommandMap: MultiEnvType{
"multienv": {
"command": "envs.sh",
"output": "hide",
},
},
},
exp: valid.Step{
StepName: "multienv",
RunCommand: "envs.sh",
Output: "hide",
},
},
}
for _, c := range cases {
t.Run(c.description, func(t *testing.T) {
Expand All @@ -583,4 +611,6 @@ func TestStep_ToValid(t *testing.T) {
}

type MapType map[string]map[string][]string
type EnvOrRunType 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
39 changes: 23 additions & 16 deletions server/core/runtime/multienv_step_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,39 @@ 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) (string, error) {
res, err := r.RunStepRunner.Run(ctx, command, path, envs, false, valid.PostProcessRunOutputShow)
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)
if err != nil {
return "", err
}

var sb strings.Builder
if len(res) == 0 {
return "No dynamic environment variable added", nil
}
sb.WriteString("No dynamic environment variable added")
} else {
sb.WriteString("Dynamic environment variables added:\n")

var sb strings.Builder
sb.WriteString("Dynamic environment variables added:\n")
vars, err := parseMultienvLine(res)
if err != nil {
return "", fmt.Errorf("Invalid environment variable definition: %s (%w)", res, err)
}

vars, err := parseMultienvLine(res)
if err != nil {
return "", fmt.Errorf("Invalid environment variable definition: %s (%w)", res, err)
for i := 0; i < len(vars); i += 2 {
key := vars[i]
envs[key] = vars[i+1]
sb.WriteString(key)
sb.WriteRune('\n')
}
}

for i := 0; i < len(vars); i += 2 {
key := vars[i]
envs[key] = vars[i+1]
sb.WriteString(key)
sb.WriteRune('\n')
switch postProcessOutput {
case valid.PostProcessRunOutputHide:
return "", nil
case valid.PostProcessRunOutputShow:
return sb.String(), nil
default:
return sb.String(), nil
}

return sb.String(), nil
}

func parseMultienvLine(in string) ([]string, error) {
Expand Down
3 changes: 2 additions & 1 deletion server/core/runtime/multienv_step_runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

version "github.com/hashicorp/go-version"
. "github.com/petergtz/pegomock/v4"
"github.com/runatlantis/atlantis/server/core/config/valid"
"github.com/runatlantis/atlantis/server/core/runtime"
"github.com/runatlantis/atlantis/server/core/terraform/mocks"
"github.com/runatlantis/atlantis/server/events/command"
Expand Down Expand Up @@ -84,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)
value, err := multiEnvStepRunner.Run(ctx, c.Command, tmpDir, envMap, valid.PostProcessRunOutputShow)
if c.ExpErr != "" {
ErrContains(t, c.ExpErr, err)
return
Expand Down
Loading

0 comments on commit 8bc06f4

Please sign in to comment.