Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: optionally hide multienv output #4422

Merged
merged 8 commits into from
May 29, 2024
Merged
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
Loading