diff --git a/runatlantis.io/docs/custom-workflows.md b/runatlantis.io/docs/custom-workflows.md index 85c1330a7..f1c3a043b 100644 --- a/runatlantis.io/docs/custom-workflows.md +++ b/runatlantis.io/docs/custom-workflows.md @@ -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 diff --git a/server/core/config/raw/step.go b/server/core/config/raw/step.go index 68f889971..581be49c6 100644 --- a/server/core/config/raw/step.go +++ b/server/core/config/raw/step.go @@ -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 @@ -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. @@ -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 { @@ -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) @@ -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) @@ -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 { @@ -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) } } @@ -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") } @@ -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], @@ -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 } @@ -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 } diff --git a/server/core/config/raw/step_test.go b/server/core/config/raw/step_test.go index 72003e2c0..f47c497e6 100644 --- a/server/core/config/raw/step_test.go +++ b/server/core/config/raw/step_test.go @@ -81,7 +81,7 @@ env: value: direct_value name: test`, exp: raw.Step{ - EnvOrRun: EnvOrRunType{ + CommandMap: EnvType{ "env": { "value": "direct_value", "name": "test", @@ -96,7 +96,7 @@ env: command: echo 123 name: test`, exp: raw.Step{ - EnvOrRun: EnvOrRunType{ + CommandMap: EnvType{ "env": { "command": "echo 123", "name": "test", @@ -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, }, }, @@ -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", @@ -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, }, @@ -312,7 +312,7 @@ func TestStep_Validate(t *testing.T) { { description: "invalid key in env", input: raw.Step{ - EnvOrRun: EnvOrRunType{ + CommandMap: EnvType{ "invalid": nil, }, }, @@ -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", }, @@ -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": "", @@ -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", @@ -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", @@ -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", @@ -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) { @@ -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 diff --git a/server/core/runtime/multienv_step_runner.go b/server/core/runtime/multienv_step_runner.go index 515eb6689..17e2ae196 100644 --- a/server/core/runtime/multienv_step_runner.go +++ b/server/core/runtime/multienv_step_runner.go @@ -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 } - if len(res) == 0 { - return "No dynamic environment variable added", nil - } - var sb strings.Builder - sb.WriteString("Dynamic environment variables added:\n") + if len(res) == 0 { + sb.WriteString("No dynamic environment variable added") + } else { + 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) { diff --git a/server/core/runtime/multienv_step_runner_test.go b/server/core/runtime/multienv_step_runner_test.go index f7d6b1132..adf51a8b6 100644 --- a/server/core/runtime/multienv_step_runner_test.go +++ b/server/core/runtime/multienv_step_runner_test.go @@ -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" @@ -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 diff --git a/server/events/project_command_runner.go b/server/events/project_command_runner.go index cd1b2e0d1..4d5c0dcf7 100644 --- a/server/events/project_command_runner.go +++ b/server/events/project_command_runner.go @@ -78,7 +78,7 @@ type EnvStepRunner interface { // MultiEnvStepRunner runs multienv steps. type MultiEnvStepRunner interface { // Run cmd in path. - Run(ctx command.ProjectContext, cmd string, path string, envs map[string]string) (string, error) + Run(ctx command.ProjectContext, 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 @@ -795,7 +795,7 @@ func (p *DefaultProjectCommandRunner) runSteps(steps []valid.Step, ctx command.P // 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) + out, err = p.MultiEnvStepRunner.Run(ctx, step.RunCommand, absPath, envs, step.Output) } if out != "" {