From e010803f4fe5f1cb4cf914b912dc24014abcf558 Mon Sep 17 00:00:00 2001 From: Istvan Tapaszto <46001960+tapaszto@users.noreply.github.com> Date: Fri, 13 May 2022 23:30:08 +0200 Subject: [PATCH] feat: MultiEnv step added (#1793) * MultiEnv step added * Values removed from the output * Fixing tests * multienv_step_runner_test added * Documentation of new feature added * Documentatation of new feature modified * multienv_step test file extension changed * Fixed multinev_step_runner test * Enhanced debug logging in multienv_step_runner * Enhanced errorhandling * Test command modified * Test command modified * Test command modified * Test command modified * Errorhandling modified * Test command modified * Test command modified * Create empty map in test * Fixing test ExpOut * Testing, refactoring, eliminating extra debug log * MultiEnv result modified from json to plain string * MultiEnv step Run parameter updated * Import added * project_command_runner updated * project_command_runner updated * multienv_step_runner_test updated * multienv doc modified * Update runatlantis.io/docs/custom-workflows.md Co-authored-by: PePe Amengual Co-authored-by: Istvan Tapaszto Co-authored-by: PePe Amengual --- runatlantis.io/docs/custom-workflows.md | 21 +++++ server/core/config/raw/step.go | 8 +- server/core/runtime/multienv_step_runner.go | 39 +++++++++ .../core/runtime/multienv_step_runner_test.go | 79 +++++++++++++++++++ server/events/project_command_runner.go | 9 +++ server/server.go | 3 + 6 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 server/core/runtime/multienv_step_runner.go create mode 100644 server/core/runtime/multienv_step_runner_test.go diff --git a/runatlantis.io/docs/custom-workflows.md b/runatlantis.io/docs/custom-workflows.md index 40b33782b3..8fb077d01b 100644 --- a/runatlantis.io/docs/custom-workflows.md +++ b/runatlantis.io/docs/custom-workflows.md @@ -407,3 +407,24 @@ as the environment variable value. * `env` `command`'s can use any of the built-in environment variables available to `run` commands. ::: + +#### Multiple Environment Variables `multienv` Command +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. +```yaml +- multienv: custom-command +``` +| Key | Type | Default | Required | Description | +|----------|--------|---------|----------|-----------------------------------------------| +| multienv | string | none | no | Run a custom command and add set | +| | | | | environment variables according to the result | +The result of the executed command must have a fixed 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. + +::: tip Notes +* `multienv` `command`'s can use any of the built-in environment variables available + to `run` commands. +::: + diff --git a/server/core/config/raw/step.go b/server/core/config/raw/step.go index 8d3d0b90be..28e860fd25 100644 --- a/server/core/config/raw/step.go +++ b/server/core/config/raw/step.go @@ -23,6 +23,7 @@ const ( ApplyStepName = "apply" InitStepName = "init" EnvStepName = "env" + MultiEnvStepName = "multienv" ) // Step represents a single action/command to perform. In YAML, it can be set as @@ -81,6 +82,7 @@ func (s Step) validStepName(stepName string) bool { stepName == PlanStepName || stepName == ApplyStepName || stepName == EnvStepName || + stepName == MultiEnvStepName || stepName == ShowStepName || stepName == PolicyCheckStepName } @@ -191,7 +193,7 @@ func (s Step) Validate() error { len(keys), strings.Join(keys, ",")) } for stepName := range elem { - if stepName != RunStepName { + if stepName != RunStepName && stepName != MultiEnvStepName { return fmt.Errorf("%q is not a valid step type", stepName) } } @@ -251,9 +253,9 @@ func (s Step) ToValid() valid.Step { if len(s.StringVal) > 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 _, v := range s.StringVal { + for stepName, v := range s.StringVal { return valid.Step{ - StepName: RunStepName, + StepName: stepName, RunCommand: v, } } diff --git a/server/core/runtime/multienv_step_runner.go b/server/core/runtime/multienv_step_runner.go new file mode 100644 index 0000000000..69a9a2028a --- /dev/null +++ b/server/core/runtime/multienv_step_runner.go @@ -0,0 +1,39 @@ +package runtime + +import ( + "fmt" + "strings" + + "github.com/runatlantis/atlantis/server/events/command" +) + +// EnvStepRunner set environment variables. +type MultiEnvStepRunner struct { + RunStepRunner *RunStepRunner +} + +// 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) + if err == nil { + envVars := strings.Split(res, ",") + if len(envVars) > 0 { + var sb strings.Builder + sb.WriteString("Dynamic environment variables added:\n") + for _, item := range envVars { + nameValue := strings.Split(item, "=") + if len(nameValue) == 2 { + envs[nameValue[0]] = nameValue[1] + sb.WriteString(nameValue[0]) + sb.WriteString("\n") + } else { + return "", fmt.Errorf("Invalid environment variable definition: %s", item) + } + } + return sb.String(), nil + } + return "No dynamic environment variable added", nil + } + return "", err +} diff --git a/server/core/runtime/multienv_step_runner_test.go b/server/core/runtime/multienv_step_runner_test.go new file mode 100644 index 0000000000..7628ea95ea --- /dev/null +++ b/server/core/runtime/multienv_step_runner_test.go @@ -0,0 +1,79 @@ +package runtime_test + +import ( + "testing" + + version "github.com/hashicorp/go-version" + . "github.com/petergtz/pegomock" + "github.com/runatlantis/atlantis/server/core/runtime" + "github.com/runatlantis/atlantis/server/core/terraform/mocks" + "github.com/runatlantis/atlantis/server/events/command" + "github.com/runatlantis/atlantis/server/events/models" + "github.com/runatlantis/atlantis/server/logging" + . "github.com/runatlantis/atlantis/testing" +) + +func TestMultiEnvStepRunner_Run(t *testing.T) { + cases := []struct { + Command string + ProjectName string + ExpOut string + ExpErr string + Version string + }{ + { + Command: `echo 'TF_VAR_REPODEFINEDVARIABLE_ONE=value1'`, + ExpOut: "Dynamic environment variables added:\nTF_VAR_REPODEFINEDVARIABLE_ONE\n", + Version: "v1.2.3", + }, + } + RegisterMockTestingT(t) + tfClient := mocks.NewMockClient() + tfVersion, err := version.NewVersion("0.12.0") + Ok(t, err) + runStepRunner := runtime.RunStepRunner{ + TerraformExecutor: tfClient, + DefaultTFVersion: tfVersion, + } + multiEnvStepRunner := runtime.MultiEnvStepRunner{ + RunStepRunner: &runStepRunner, + } + for _, c := range cases { + t.Run(c.Command, func(t *testing.T) { + tmpDir, cleanup := TempDir(t) + defer cleanup() + ctx := command.ProjectContext{ + BaseRepo: models.Repo{ + Name: "basename", + Owner: "baseowner", + }, + HeadRepo: models.Repo{ + Name: "headname", + Owner: "headowner", + }, + Pull: models.PullRequest{ + Num: 2, + HeadBranch: "add-feat", + BaseBranch: "master", + Author: "acme", + }, + User: models.User{ + Username: "acme-user", + }, + Log: logging.NewNoopLogger(t), + Workspace: "myworkspace", + RepoRelDir: "mydir", + TerraformVersion: tfVersion, + ProjectName: c.ProjectName, + } + envMap := make(map[string]string) + value, err := multiEnvStepRunner.Run(ctx, c.Command, tmpDir, envMap) + if c.ExpErr != "" { + ErrContains(t, c.ExpErr, err) + return + } + Ok(t, err) + Equals(t, c.ExpOut, value) + }) + } +} diff --git a/server/events/project_command_runner.go b/server/events/project_command_runner.go index ead5e85eff..a52c9e2cdb 100644 --- a/server/events/project_command_runner.go +++ b/server/events/project_command_runner.go @@ -72,6 +72,12 @@ type EnvStepRunner interface { Run(ctx command.ProjectContext, cmd string, value string, path string, envs map[string]string) (string, error) } +// 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) +} + //go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_webhooks_sender.go WebhooksSender // WebhooksSender sends webhook. @@ -189,6 +195,7 @@ type DefaultProjectCommandRunner struct { VersionStepRunner StepRunner RunStepRunner CustomStepRunner EnvStepRunner EnvStepRunner + MultiEnvStepRunner MultiEnvStepRunner PullApprovedChecker runtime.PullApprovedChecker WorkingDir WorkingDir Webhooks WebhooksSender @@ -493,6 +500,8 @@ func (p *DefaultProjectCommandRunner) runSteps(steps []valid.Step, ctx command.P // We reset out to the empty string because we don't want it to // 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) } if out != "" { diff --git a/server/server.go b/server/server.go index b9f871e770..a6546a70ca 100644 --- a/server/server.go +++ b/server/server.go @@ -550,6 +550,9 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { EnvStepRunner: &runtime.EnvStepRunner{ RunStepRunner: runStepRunner, }, + MultiEnvStepRunner: &runtime.MultiEnvStepRunner{ + RunStepRunner: runStepRunner, + }, VersionStepRunner: &runtime.VersionStepRunner{ TerraformExecutor: terraformClient, DefaultTFVersion: defaultTfVersion,