From 33c0a72add47bbad42c88761dc72026c8a6006b9 Mon Sep 17 00:00:00 2001 From: Andrii Nasinnyk Date: Wed, 21 Aug 2019 15:06:49 +0200 Subject: [PATCH 1/2] Add var step to workflows Add new var step for setting environment variable in workflows. --- .../mocks/matchers/map_of_string_to_string.go | 21 +++ .../events/mocks/mock_custom_step_runner.go | 20 +-- server/events/mocks/mock_env_step_runner.go | 133 ++++++++++++++++++ server/events/mocks/mock_step_runner.go | 20 +-- server/events/project_command_runner.go | 26 +++- server/events/project_command_runner_test.go | 55 +++++--- server/events/runtime/apply_step_runner.go | 11 +- .../events/runtime/apply_step_runner_test.go | 30 ++-- server/events/runtime/env_step_runner.go | 26 ++++ server/events/runtime/env_step_runner_test.go | 82 +++++++++++ server/events/runtime/init_step_runner.go | 4 +- .../events/runtime/init_step_runner_test.go | 10 +- server/events/runtime/plan_step_runner.go | 25 ++-- .../events/runtime/plan_step_runner_test.go | 71 ++++++---- server/events/runtime/run_step_runner.go | 5 +- server/events/runtime/run_step_runner_test.go | 2 +- server/events/runtime/runtime.go | 4 +- .../mocks/matchers/map_of_string_to_string.go | 21 +++ .../terraform/mocks/mock_terraform_client.go | 28 ++-- server/events/terraform/terraform_client.go | 16 ++- .../terraform_client_internal_test.go | 14 +- .../events/terraform/terraform_client_test.go | 12 +- server/events/yaml/raw/step.go | 84 ++++++++++- server/events/yaml/raw/step_test.go | 99 +++++++++++++ server/events/yaml/valid/repo_cfg.go | 5 + server/server.go | 4 + 26 files changed, 680 insertions(+), 148 deletions(-) create mode 100644 server/events/mocks/matchers/map_of_string_to_string.go create mode 100644 server/events/mocks/mock_env_step_runner.go create mode 100644 server/events/runtime/env_step_runner.go create mode 100644 server/events/runtime/env_step_runner_test.go create mode 100644 server/events/terraform/mocks/matchers/map_of_string_to_string.go diff --git a/server/events/mocks/matchers/map_of_string_to_string.go b/server/events/mocks/matchers/map_of_string_to_string.go new file mode 100644 index 0000000000..4d969915af --- /dev/null +++ b/server/events/mocks/matchers/map_of_string_to_string.go @@ -0,0 +1,21 @@ +// Code generated by pegomock. DO NOT EDIT. +package matchers + +import ( + "reflect" + "github.com/petergtz/pegomock" + + +) + +func AnyMapOfStringToString() map[string]string { + pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(map[string]string))(nil)).Elem())) + var nullValue map[string]string + return nullValue +} + +func EqMapOfStringToString(value map[string]string) map[string]string { + pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) + var nullValue map[string]string + return nullValue +} diff --git a/server/events/mocks/mock_custom_step_runner.go b/server/events/mocks/mock_custom_step_runner.go index bb47e51fc5..dfe10e8a06 100644 --- a/server/events/mocks/mock_custom_step_runner.go +++ b/server/events/mocks/mock_custom_step_runner.go @@ -25,11 +25,11 @@ func NewMockCustomStepRunner(options ...pegomock.Option) *MockCustomStepRunner { func (mock *MockCustomStepRunner) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } func (mock *MockCustomStepRunner) FailHandler() pegomock.FailHandler { return mock.fail } -func (mock *MockCustomStepRunner) Run(ctx models.ProjectCommandContext, cmd string, path string) (string, error) { +func (mock *MockCustomStepRunner) Run(ctx models.ProjectCommandContext, cmd string, path string, envs map[string]string) (string, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockCustomStepRunner().") } - params := []pegomock.Param{ctx, cmd, path} + params := []pegomock.Param{ctx, cmd, path, envs} result := pegomock.GetGenericMockFrom(mock).Invoke("Run", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) var ret0 string var ret1 error @@ -81,8 +81,8 @@ type VerifierMockCustomStepRunner struct { timeout time.Duration } -func (verifier *VerifierMockCustomStepRunner) Run(ctx models.ProjectCommandContext, cmd string, path string) *MockCustomStepRunner_Run_OngoingVerification { - params := []pegomock.Param{ctx, cmd, path} +func (verifier *VerifierMockCustomStepRunner) Run(ctx models.ProjectCommandContext, cmd string, path string, envs map[string]string) *MockCustomStepRunner_Run_OngoingVerification { + params := []pegomock.Param{ctx, cmd, path, envs} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Run", params, verifier.timeout) return &MockCustomStepRunner_Run_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } @@ -92,12 +92,12 @@ type MockCustomStepRunner_Run_OngoingVerification struct { methodInvocations []pegomock.MethodInvocation } -func (c *MockCustomStepRunner_Run_OngoingVerification) GetCapturedArguments() (models.ProjectCommandContext, string, string) { - ctx, cmd, path := c.GetAllCapturedArguments() - return ctx[len(ctx)-1], cmd[len(cmd)-1], path[len(path)-1] +func (c *MockCustomStepRunner_Run_OngoingVerification) GetCapturedArguments() (models.ProjectCommandContext, string, string, map[string]string) { + ctx, cmd, path, envs := c.GetAllCapturedArguments() + return ctx[len(ctx)-1], cmd[len(cmd)-1], path[len(path)-1], envs[len(envs)-1] } -func (c *MockCustomStepRunner_Run_OngoingVerification) GetAllCapturedArguments() (_param0 []models.ProjectCommandContext, _param1 []string, _param2 []string) { +func (c *MockCustomStepRunner_Run_OngoingVerification) GetAllCapturedArguments() (_param0 []models.ProjectCommandContext, _param1 []string, _param2 []string, _param3 []map[string]string) { params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) if len(params) > 0 { _param0 = make([]models.ProjectCommandContext, len(params[0])) @@ -112,6 +112,10 @@ func (c *MockCustomStepRunner_Run_OngoingVerification) GetAllCapturedArguments() for u, param := range params[2] { _param2[u] = param.(string) } + _param3 = make([]map[string]string, len(params[3])) + for u, param := range params[3] { + _param3[u] = param.(map[string]string) + } } return } diff --git a/server/events/mocks/mock_env_step_runner.go b/server/events/mocks/mock_env_step_runner.go new file mode 100644 index 0000000000..461812701b --- /dev/null +++ b/server/events/mocks/mock_env_step_runner.go @@ -0,0 +1,133 @@ +// Code generated by pegomock. DO NOT EDIT. +// Source: github.com/runatlantis/atlantis/server/events (interfaces: EnvStepRunner) + +package mocks + +import ( + pegomock "github.com/petergtz/pegomock" + models "github.com/runatlantis/atlantis/server/events/models" + "reflect" + "time" +) + +type MockEnvStepRunner struct { + fail func(message string, callerSkip ...int) +} + +func NewMockEnvStepRunner(options ...pegomock.Option) *MockEnvStepRunner { + mock := &MockEnvStepRunner{} + for _, option := range options { + option.Apply(mock) + } + return mock +} + +func (mock *MockEnvStepRunner) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } +func (mock *MockEnvStepRunner) FailHandler() pegomock.FailHandler { return mock.fail } + +func (mock *MockEnvStepRunner) Run(ctx models.ProjectCommandContext, name string, cmd string, value string, path string, envs map[string]string) (string, string, error) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockEnvStepRunner().") + } + params := []pegomock.Param{ctx, name, cmd, value, path, envs} + result := pegomock.GetGenericMockFrom(mock).Invoke("Run", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 string + var ret1 string + var ret2 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(string) + } + if result[1] != nil { + ret1 = result[1].(string) + } + if result[2] != nil { + ret2 = result[2].(error) + } + } + return ret0, ret1, ret2 +} + +func (mock *MockEnvStepRunner) VerifyWasCalledOnce() *VerifierMockEnvStepRunner { + return &VerifierMockEnvStepRunner{ + mock: mock, + invocationCountMatcher: pegomock.Times(1), + } +} + +func (mock *MockEnvStepRunner) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockEnvStepRunner { + return &VerifierMockEnvStepRunner{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + } +} + +func (mock *MockEnvStepRunner) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockEnvStepRunner { + return &VerifierMockEnvStepRunner{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + inOrderContext: inOrderContext, + } +} + +func (mock *MockEnvStepRunner) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockEnvStepRunner { + return &VerifierMockEnvStepRunner{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + timeout: timeout, + } +} + +type VerifierMockEnvStepRunner struct { + mock *MockEnvStepRunner + invocationCountMatcher pegomock.Matcher + inOrderContext *pegomock.InOrderContext + timeout time.Duration +} + +func (verifier *VerifierMockEnvStepRunner) Run(ctx models.ProjectCommandContext, name string, cmd string, value string, path string, envs map[string]string) *MockEnvStepRunner_Run_OngoingVerification { + params := []pegomock.Param{ctx, name, cmd, value, path, envs} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Run", params, verifier.timeout) + return &MockEnvStepRunner_Run_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockEnvStepRunner_Run_OngoingVerification struct { + mock *MockEnvStepRunner + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockEnvStepRunner_Run_OngoingVerification) GetCapturedArguments() (models.ProjectCommandContext, string, string, string, string, map[string]string) { + ctx, name, cmd, value, path, envs := c.GetAllCapturedArguments() + return ctx[len(ctx)-1], name[len(name)-1], cmd[len(cmd)-1], value[len(value)-1], path[len(path)-1], envs[len(envs)-1] +} + +func (c *MockEnvStepRunner_Run_OngoingVerification) GetAllCapturedArguments() (_param0 []models.ProjectCommandContext, _param1 []string, _param2 []string, _param3 []string, _param4 []string, _param5 []map[string]string) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]models.ProjectCommandContext, len(params[0])) + for u, param := range params[0] { + _param0[u] = param.(models.ProjectCommandContext) + } + _param1 = make([]string, len(params[1])) + for u, param := range params[1] { + _param1[u] = param.(string) + } + _param2 = make([]string, len(params[2])) + for u, param := range params[2] { + _param2[u] = param.(string) + } + _param3 = make([]string, len(params[3])) + for u, param := range params[3] { + _param3[u] = param.(string) + } + _param4 = make([]string, len(params[4])) + for u, param := range params[4] { + _param4[u] = param.(string) + } + _param5 = make([]map[string]string, len(params[5])) + for u, param := range params[5] { + _param5[u] = param.(map[string]string) + } + } + return +} diff --git a/server/events/mocks/mock_step_runner.go b/server/events/mocks/mock_step_runner.go index 4f5af225e3..14aff3aeba 100644 --- a/server/events/mocks/mock_step_runner.go +++ b/server/events/mocks/mock_step_runner.go @@ -25,11 +25,11 @@ func NewMockStepRunner(options ...pegomock.Option) *MockStepRunner { func (mock *MockStepRunner) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } func (mock *MockStepRunner) FailHandler() pegomock.FailHandler { return mock.fail } -func (mock *MockStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []string, path string) (string, error) { +func (mock *MockStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []string, path string, envs map[string]string) (string, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockStepRunner().") } - params := []pegomock.Param{ctx, extraArgs, path} + params := []pegomock.Param{ctx, extraArgs, path, envs} result := pegomock.GetGenericMockFrom(mock).Invoke("Run", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) var ret0 string var ret1 error @@ -81,8 +81,8 @@ type VerifierMockStepRunner struct { timeout time.Duration } -func (verifier *VerifierMockStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []string, path string) *MockStepRunner_Run_OngoingVerification { - params := []pegomock.Param{ctx, extraArgs, path} +func (verifier *VerifierMockStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []string, path string, envs map[string]string) *MockStepRunner_Run_OngoingVerification { + params := []pegomock.Param{ctx, extraArgs, path, envs} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Run", params, verifier.timeout) return &MockStepRunner_Run_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } @@ -92,12 +92,12 @@ type MockStepRunner_Run_OngoingVerification struct { methodInvocations []pegomock.MethodInvocation } -func (c *MockStepRunner_Run_OngoingVerification) GetCapturedArguments() (models.ProjectCommandContext, []string, string) { - ctx, extraArgs, path := c.GetAllCapturedArguments() - return ctx[len(ctx)-1], extraArgs[len(extraArgs)-1], path[len(path)-1] +func (c *MockStepRunner_Run_OngoingVerification) GetCapturedArguments() (models.ProjectCommandContext, []string, string, map[string]string) { + ctx, extraArgs, path, envs := c.GetAllCapturedArguments() + return ctx[len(ctx)-1], extraArgs[len(extraArgs)-1], path[len(path)-1], envs[len(envs)-1] } -func (c *MockStepRunner_Run_OngoingVerification) GetAllCapturedArguments() (_param0 []models.ProjectCommandContext, _param1 [][]string, _param2 []string) { +func (c *MockStepRunner_Run_OngoingVerification) GetAllCapturedArguments() (_param0 []models.ProjectCommandContext, _param1 [][]string, _param2 []string, _param3 []map[string]string) { params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) if len(params) > 0 { _param0 = make([]models.ProjectCommandContext, len(params[0])) @@ -112,6 +112,10 @@ func (c *MockStepRunner_Run_OngoingVerification) GetAllCapturedArguments() (_par for u, param := range params[2] { _param2[u] = param.(string) } + _param3 = make([]map[string]string, len(params[3])) + for u, param := range params[3] { + _param3[u] = param.(map[string]string) + } } return } diff --git a/server/events/project_command_runner.go b/server/events/project_command_runner.go index 349bc28baf..b5240910d8 100644 --- a/server/events/project_command_runner.go +++ b/server/events/project_command_runner.go @@ -52,7 +52,7 @@ type LockURLGenerator interface { // `terraform plan`. type StepRunner interface { // Run runs the step. - Run(ctx models.ProjectCommandContext, extraArgs []string, path string) (string, error) + Run(ctx models.ProjectCommandContext, extraArgs []string, path string, envs map[string]string) (string, error) } //go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_custom_step_runner.go CustomStepRunner @@ -60,7 +60,14 @@ type StepRunner interface { // CustomStepRunner runs custom run steps. type CustomStepRunner interface { // Run cmd in path. - Run(ctx models.ProjectCommandContext, cmd string, path string) (string, error) + Run(ctx models.ProjectCommandContext, cmd string, path string, envs map[string]string) (string, error) +} + +//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_env_step_runner.go EnvStepRunner + +// EnvStepRunner runs env steps. +type EnvStepRunner interface { + Run(ctx models.ProjectCommandContext, name string, cmd string, value string, path string, envs map[string]string) (string, string, error) } //go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_webhooks_sender.go WebhooksSender @@ -90,6 +97,7 @@ type DefaultProjectCommandRunner struct { PlanStepRunner StepRunner ApplyStepRunner StepRunner RunStepRunner CustomStepRunner + EnvStepRunner EnvStepRunner PullApprovedChecker runtime.PullApprovedChecker WorkingDir WorkingDir Webhooks WebhooksSender @@ -174,17 +182,23 @@ func (p *DefaultProjectCommandRunner) doPlan(ctx models.ProjectCommandContext) ( func (p *DefaultProjectCommandRunner) runSteps(steps []valid.Step, ctx models.ProjectCommandContext, absPath string) ([]string, error) { var outputs []string for _, step := range steps { + var envs = make(map[string]string) var out string var err error + var name string switch step.StepName { case "init": - out, err = p.InitStepRunner.Run(ctx, step.ExtraArgs, absPath) + out, err = p.InitStepRunner.Run(ctx, step.ExtraArgs, absPath, envs) case "plan": - out, err = p.PlanStepRunner.Run(ctx, step.ExtraArgs, absPath) + out, err = p.PlanStepRunner.Run(ctx, step.ExtraArgs, absPath, envs) case "apply": - out, err = p.ApplyStepRunner.Run(ctx, step.ExtraArgs, absPath) + out, err = p.ApplyStepRunner.Run(ctx, step.ExtraArgs, absPath, envs) case "run": - out, err = p.RunStepRunner.Run(ctx, step.RunCommand, absPath) + out, err = p.RunStepRunner.Run(ctx, step.RunCommand, absPath, envs) + case "env": + name, out, err = p.EnvStepRunner.Run(ctx, step.EnvVarName, step.RunCommand, step.EnvVarValue, absPath, envs) + envs[name] = out + out = "" } if out != "" { diff --git a/server/events/project_command_runner_test.go b/server/events/project_command_runner_test.go index b2ac2766d6..024f446e0a 100644 --- a/server/events/project_command_runner_test.go +++ b/server/events/project_command_runner_test.go @@ -34,6 +34,7 @@ func TestDefaultProjectCommandRunner_Plan(t *testing.T) { mockInit := mocks.NewMockStepRunner() mockPlan := mocks.NewMockStepRunner() mockApply := mocks.NewMockStepRunner() + mockEnv := mocks.NewMockEnvStepRunner() mockRun := mocks.NewMockCustomStepRunner() mockWorkingDir := mocks.NewMockWorkingDir() mockLocker := mocks.NewMockProjectLocker() @@ -45,12 +46,14 @@ func TestDefaultProjectCommandRunner_Plan(t *testing.T) { PlanStepRunner: mockPlan, ApplyStepRunner: mockApply, RunStepRunner: mockRun, + EnvStepRunner: mockEnv, PullApprovedChecker: nil, WorkingDir: mockWorkingDir, Webhooks: nil, WorkingDirLocker: events.NewDefaultWorkingDirLocker(), } + envs := make(map[string]string) repoDir, cleanup := TempDir(t) defer cleanup() When(mockWorkingDir.Clone( @@ -86,33 +89,41 @@ func TestDefaultProjectCommandRunner_Plan(t *testing.T) { { StepName: "init", }, + { + StepName: "env", + EnvVarName: "name", + EnvVarValue: "value", + }, }, Workspace: "default", RepoRelDir: ".", } // Each step will output its step name. - When(mockInit.Run(ctx, nil, repoDir)).ThenReturn("init", nil) - When(mockPlan.Run(ctx, nil, repoDir)).ThenReturn("plan", nil) - When(mockApply.Run(ctx, nil, repoDir)).ThenReturn("apply", nil) - When(mockRun.Run(ctx, "", repoDir)).ThenReturn("run", nil) - + When(mockInit.Run(ctx, nil, repoDir, envs)).ThenReturn("init", nil) + When(mockPlan.Run(ctx, nil, repoDir, envs)).ThenReturn("plan", nil) + When(mockApply.Run(ctx, nil, repoDir, envs)).ThenReturn("apply", nil) + When(mockRun.Run(ctx, "", repoDir, envs)).ThenReturn("run", nil) + When(mockEnv.Run(ctx, "name", "", "value", repoDir, envs)).ThenReturn("name", "value", nil) res := runner.Plan(ctx) Assert(t, res.PlanSuccess != nil, "exp plan success") Equals(t, "https://lock-key", res.PlanSuccess.LockURL) Equals(t, "run\napply\nplan\ninit", res.PlanSuccess.TerraformOutput) - expSteps := []string{"run", "apply", "plan", "init"} + expSteps := []string{"env", "run", "apply", "plan", "init"} + var newEnv = map[string]string{"name": "value"} for _, step := range expSteps { switch step { case "init": - mockInit.VerifyWasCalledOnce().Run(ctx, nil, repoDir) + mockInit.VerifyWasCalledOnce().Run(ctx, nil, repoDir, envs) case "plan": - mockPlan.VerifyWasCalledOnce().Run(ctx, nil, repoDir) + mockPlan.VerifyWasCalledOnce().Run(ctx, nil, repoDir, envs) case "apply": - mockApply.VerifyWasCalledOnce().Run(ctx, nil, repoDir) + mockApply.VerifyWasCalledOnce().Run(ctx, nil, repoDir, envs) case "run": - mockRun.VerifyWasCalledOnce().Run(ctx, "", repoDir) + mockRun.VerifyWasCalledOnce().Run(ctx, "", repoDir, envs) + case "env": + mockEnv.VerifyWasCalledOnce().Run(ctx, "name", "", "value", repoDir, newEnv) } } } @@ -238,8 +249,11 @@ func TestDefaultProjectCommandRunner_Apply(t *testing.T) { { StepName: "init", }, + { + StepName: "env", + }, }, - expSteps: []string{"run", "apply", "plan", "init"}, + expSteps: []string{"run", "apply", "plan", "init", "env"}, expOut: "run\napply\nplan\ninit", }, } @@ -251,6 +265,7 @@ func TestDefaultProjectCommandRunner_Apply(t *testing.T) { mockPlan := mocks.NewMockStepRunner() mockApply := mocks.NewMockStepRunner() mockRun := mocks.NewMockCustomStepRunner() + mockEnv := mocks.NewMockEnvStepRunner() mockApproved := mocks2.NewMockPullApprovedChecker() mockWorkingDir := mocks.NewMockWorkingDir() mockLocker := mocks.NewMockProjectLocker() @@ -263,11 +278,13 @@ func TestDefaultProjectCommandRunner_Apply(t *testing.T) { PlanStepRunner: mockPlan, ApplyStepRunner: mockApply, RunStepRunner: mockRun, + EnvStepRunner: mockEnv, PullApprovedChecker: mockApproved, WorkingDir: mockWorkingDir, Webhooks: mockSender, WorkingDirLocker: events.NewDefaultWorkingDirLocker(), } + envs := make(map[string]string) repoDir, cleanup := TempDir(t) defer cleanup() When(mockWorkingDir.GetWorkingDir( @@ -284,10 +301,10 @@ func TestDefaultProjectCommandRunner_Apply(t *testing.T) { RepoRelDir: ".", PullMergeable: c.pullMergeable, } - When(mockInit.Run(ctx, nil, repoDir)).ThenReturn("init", nil) - When(mockPlan.Run(ctx, nil, repoDir)).ThenReturn("plan", nil) - When(mockApply.Run(ctx, nil, repoDir)).ThenReturn("apply", nil) - When(mockRun.Run(ctx, "", repoDir)).ThenReturn("run", nil) + When(mockInit.Run(ctx, nil, repoDir, envs)).ThenReturn("init", nil) + When(mockPlan.Run(ctx, nil, repoDir, envs)).ThenReturn("plan", nil) + When(mockApply.Run(ctx, nil, repoDir, envs)).ThenReturn("apply", nil) + When(mockRun.Run(ctx, "", repoDir, envs)).ThenReturn("run", nil) When(mockApproved.PullIsApproved(ctx.BaseRepo, ctx.Pull)).ThenReturn(true, nil) res := runner.Apply(ctx) @@ -299,13 +316,13 @@ func TestDefaultProjectCommandRunner_Apply(t *testing.T) { case "approved": mockApproved.VerifyWasCalledOnce().PullIsApproved(ctx.BaseRepo, ctx.Pull) case "init": - mockInit.VerifyWasCalledOnce().Run(ctx, nil, repoDir) + mockInit.VerifyWasCalledOnce().Run(ctx, nil, repoDir, envs) case "plan": - mockPlan.VerifyWasCalledOnce().Run(ctx, nil, repoDir) + mockPlan.VerifyWasCalledOnce().Run(ctx, nil, repoDir, envs) case "apply": - mockApply.VerifyWasCalledOnce().Run(ctx, nil, repoDir) + mockApply.VerifyWasCalledOnce().Run(ctx, nil, repoDir, envs) case "run": - mockRun.VerifyWasCalledOnce().Run(ctx, "", repoDir) + mockRun.VerifyWasCalledOnce().Run(ctx, "", repoDir, envs) } } }) diff --git a/server/events/runtime/apply_step_runner.go b/server/events/runtime/apply_step_runner.go index 2570714ec2..9c5ae97b8f 100644 --- a/server/events/runtime/apply_step_runner.go +++ b/server/events/runtime/apply_step_runner.go @@ -22,7 +22,7 @@ type ApplyStepRunner struct { AsyncTFExec AsyncTFExec } -func (a *ApplyStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []string, path string) (string, error) { +func (a *ApplyStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []string, path string, envs map[string]string) (string, error) { if a.hasTargetFlag(ctx, extraArgs) { return "", errors.New("cannot run apply with -target because we are applying an already generated plan. Instead, run -target with atlantis plan") } @@ -39,7 +39,7 @@ func (a *ApplyStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []stri var out string if a.isRemotePlan(contents) { args := append(append(append([]string{"apply", "-input=false", "-no-color"}, extraArgs...), ctx.EscapedCommentArgs...)) - out, err = a.runRemoteApply(ctx, args, path, planPath, ctx.TerraformVersion) + out, err = a.runRemoteApply(ctx, args, path, planPath, ctx.TerraformVersion, envs) if err == nil { out = a.cleanRemoteApplyOutput(out) } @@ -47,7 +47,7 @@ func (a *ApplyStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []stri // NOTE: we need to quote the plan path because Bitbucket Server can // have spaces in its repo owner names which is part of the path. args := append(append(append([]string{"apply", "-input=false", "-no-color"}, extraArgs...), ctx.EscapedCommentArgs...), fmt.Sprintf("%q", planPath)) - out, err = a.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, args, ctx.TerraformVersion, ctx.Workspace) + out, err = a.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, args, envs, ctx.TerraformVersion, ctx.Workspace) } // If the apply was successful, delete the plan. @@ -120,7 +120,8 @@ func (a *ApplyStepRunner) runRemoteApply( applyArgs []string, path string, absPlanPath string, - tfVersion *version.Version) (string, error) { + tfVersion *version.Version, + envs map[string]string) (string, error) { // The planfile contents are needed to ensure that the plan didn't change // between plan and apply phases. @@ -138,7 +139,7 @@ func (a *ApplyStepRunner) runRemoteApply( // Start the async command execution. ctx.Log.Debug("starting async tf remote operation") - inCh, outCh := a.AsyncTFExec.RunCommandAsync(ctx.Log, filepath.Clean(path), applyArgs, tfVersion, ctx.Workspace) + inCh, outCh := a.AsyncTFExec.RunCommandAsync(ctx.Log, filepath.Clean(path), applyArgs, envs, tfVersion, ctx.Workspace) var lines []string nextLineIsRunURL := false var runURL string diff --git a/server/events/runtime/apply_step_runner_test.go b/server/events/runtime/apply_step_runner_test.go index c43a9b58f1..386857a17a 100644 --- a/server/events/runtime/apply_step_runner_test.go +++ b/server/events/runtime/apply_step_runner_test.go @@ -30,7 +30,7 @@ func TestRun_NoDir(t *testing.T) { _, err := o.Run(models.ProjectCommandContext{ RepoRelDir: ".", Workspace: "workspace", - }, nil, "/nonexistent/path") + }, nil, "/nonexistent/path", map[string]string(nil)) ErrEquals(t, "no plan found at path \".\" and workspace \"workspace\"–did you run plan?", err) } @@ -43,7 +43,7 @@ func TestRun_NoPlanFile(t *testing.T) { _, err := o.Run(models.ProjectCommandContext{ RepoRelDir: ".", Workspace: "workspace", - }, nil, tmpDir) + }, nil, tmpDir, map[string]string(nil)) ErrEquals(t, "no plan found at path \".\" and workspace \"workspace\"–did you run plan?", err) } @@ -60,16 +60,16 @@ func TestRun_Success(t *testing.T) { TerraformExecutor: terraform, } - When(terraform.RunCommandWithVersion(matchers.AnyPtrToLoggingSimpleLogger(), AnyString(), AnyStringSlice(), matchers2.AnyPtrToGoVersionVersion(), AnyString())). + When(terraform.RunCommandWithVersion(matchers.AnyPtrToLoggingSimpleLogger(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())). ThenReturn("output", nil) output, err := o.Run(models.ProjectCommandContext{ Workspace: "workspace", RepoRelDir: ".", EscapedCommentArgs: []string{"comment", "args"}, - }, []string{"extra", "args"}, tmpDir) + }, []string{"extra", "args"}, tmpDir, map[string]string(nil)) Ok(t, err) Equals(t, "output", output) - terraform.VerifyWasCalledOnce().RunCommandWithVersion(nil, tmpDir, []string{"apply", "-input=false", "-no-color", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, nil, "workspace") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(nil, tmpDir, []string{"apply", "-input=false", "-no-color", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, map[string]string(nil), nil, "workspace") _, err = os.Stat(planPath) Assert(t, os.IsNotExist(err), "planfile should be deleted") } @@ -88,17 +88,17 @@ func TestRun_AppliesCorrectProjectPlan(t *testing.T) { TerraformExecutor: terraform, } - When(terraform.RunCommandWithVersion(matchers.AnyPtrToLoggingSimpleLogger(), AnyString(), AnyStringSlice(), matchers2.AnyPtrToGoVersionVersion(), AnyString())). + When(terraform.RunCommandWithVersion(matchers.AnyPtrToLoggingSimpleLogger(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())). ThenReturn("output", nil) output, err := o.Run(models.ProjectCommandContext{ Workspace: "default", RepoRelDir: ".", ProjectName: "projectname", EscapedCommentArgs: []string{"comment", "args"}, - }, []string{"extra", "args"}, tmpDir) + }, []string{"extra", "args"}, tmpDir, map[string]string(nil)) Ok(t, err) Equals(t, "output", output) - terraform.VerifyWasCalledOnce().RunCommandWithVersion(nil, tmpDir, []string{"apply", "-input=false", "-no-color", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, nil, "default") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(nil, tmpDir, []string{"apply", "-input=false", "-no-color", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, map[string]string(nil), nil, "default") _, err = os.Stat(planPath) Assert(t, os.IsNotExist(err), "planfile should be deleted") } @@ -117,17 +117,17 @@ func TestRun_UsesConfiguredTFVersion(t *testing.T) { } tfVersion, _ := version.NewVersion("0.11.0") - When(terraform.RunCommandWithVersion(matchers.AnyPtrToLoggingSimpleLogger(), AnyString(), AnyStringSlice(), matchers2.AnyPtrToGoVersionVersion(), AnyString())). + When(terraform.RunCommandWithVersion(matchers.AnyPtrToLoggingSimpleLogger(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())). ThenReturn("output", nil) output, err := o.Run(models.ProjectCommandContext{ Workspace: "workspace", RepoRelDir: ".", EscapedCommentArgs: []string{"comment", "args"}, TerraformVersion: tfVersion, - }, []string{"extra", "args"}, tmpDir) + }, []string{"extra", "args"}, tmpDir, map[string]string(nil)) Ok(t, err) Equals(t, "output", output) - terraform.VerifyWasCalledOnce().RunCommandWithVersion(nil, tmpDir, []string{"apply", "-input=false", "-no-color", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, tfVersion, "workspace") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(nil, tmpDir, []string{"apply", "-input=false", "-no-color", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, map[string]string(nil), tfVersion, "workspace") _, err = os.Stat(planPath) Assert(t, os.IsNotExist(err), "planfile should be deleted") } @@ -200,7 +200,7 @@ func TestRun_UsingTarget(t *testing.T) { Workspace: "workspace", RepoRelDir: ".", EscapedCommentArgs: c.commentFlags, - }, c.extraArgs, tmpDir) + }, c.extraArgs, tmpDir, map[string]string(nil)) Equals(t, "", output) if c.expErr { ErrEquals(t, "cannot run apply with -target because we are applying an already generated plan. Instead, run -target with atlantis plan", err) @@ -246,7 +246,7 @@ Plan: 0 to add, 0 to change, 1 to destroy.` EscapedCommentArgs: []string{"comment", "args"}, TerraformVersion: tfVersion, } - output, err := o.Run(ctx, []string{"extra", "args"}, tmpDir) + output, err := o.Run(ctx, []string{"extra", "args"}, tmpDir, map[string]string(nil)) <-tfExec.DoneCh Ok(t, err) @@ -307,7 +307,7 @@ Plan: 0 to add, 0 to change, 1 to destroy.` RepoRelDir: ".", EscapedCommentArgs: []string{"comment", "args"}, TerraformVersion: tfVersion, - }, []string{"extra", "args"}, tmpDir) + }, []string{"extra", "args"}, tmpDir, map[string]string(nil)) <-tfExec.DoneCh ErrEquals(t, `Plan generated during apply phase did not match plan generated during plan phase. Aborting apply. @@ -356,7 +356,7 @@ type remoteApplyMock struct { } // RunCommandAsync fakes out running terraform async. -func (r *remoteApplyMock) RunCommandAsync(log *logging.SimpleLogger, path string, args []string, v *version.Version, workspace string) (chan<- string, <-chan terraform.Line) { +func (r *remoteApplyMock) RunCommandAsync(log *logging.SimpleLogger, path string, args []string, envs map[string]string, v *version.Version, workspace string) (chan<- string, <-chan terraform.Line) { r.CalledArgs = args in := make(chan string) diff --git a/server/events/runtime/env_step_runner.go b/server/events/runtime/env_step_runner.go new file mode 100644 index 0000000000..74afe19f48 --- /dev/null +++ b/server/events/runtime/env_step_runner.go @@ -0,0 +1,26 @@ +package runtime + +import ( + "github.com/hashicorp/go-version" + "github.com/runatlantis/atlantis/server/events/models" +) + +// EnvStepRunner set environment variables. +type EnvStepRunner struct { + TerraformExecutor TerraformExec + DefaultTFVersion *version.Version +} + +func (r *EnvStepRunner) Run(ctx models.ProjectCommandContext, name string, command string, value string, path string, envs map[string]string) (string, string, error) { + if value != "" { + return name, value, nil + } + + runStepRunner := RunStepRunner{ + TerraformExecutor: r.TerraformExecutor, + DefaultTFVersion: r.DefaultTFVersion, + } + res, err := runStepRunner.Run(ctx, command, path, envs) + + return name, res, err +} diff --git a/server/events/runtime/env_step_runner_test.go b/server/events/runtime/env_step_runner_test.go new file mode 100644 index 0000000000..fe281e9667 --- /dev/null +++ b/server/events/runtime/env_step_runner_test.go @@ -0,0 +1,82 @@ +package runtime_test + +import ( + "testing" + + "github.com/hashicorp/go-version" + "github.com/runatlantis/atlantis/server/events/models" + "github.com/runatlantis/atlantis/server/events/runtime" + "github.com/runatlantis/atlantis/server/events/terraform/mocks" + "github.com/runatlantis/atlantis/server/logging" + + . "github.com/runatlantis/atlantis/testing" +) + +func TestEnvStepRunner_Run(t *testing.T) { + cases := []struct { + Command string + Value string + ProjectName string + ExpValue string + ExpErr string + }{ + { + Command: "echo 123", + ExpValue: "123\n", + }, + { + Value: "test", + ExpValue: "test", + }, + { + Command: "echo 321", + Value: "test", + ExpValue: "test", + }, + } + terraform := mocks.NewMockClient() + projVersion, err := version.NewVersion("v0.11.0") + Ok(t, err) + defaultVersion, _ := version.NewVersion("0.8") + r := runtime.EnvStepRunner{ + TerraformExecutor: terraform, + DefaultTFVersion: defaultVersion, + } + for _, c := range cases { + t.Run(c.Command, func(t *testing.T) { + tmpDir, cleanup := TempDir(t) + defer cleanup() + ctx := models.ProjectCommandContext{ + 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(), + Workspace: "myworkspace", + RepoRelDir: "mydir", + TerraformVersion: projVersion, + ProjectName: c.ProjectName, + } + _, value, err := r.Run(ctx, "var", c.Command, c.Value, tmpDir, map[string]string(nil)) + if c.ExpErr != "" { + ErrContains(t, c.ExpErr, err) + return + } + Ok(t, err) + Equals(t, c.ExpValue, value) + }) + } +} diff --git a/server/events/runtime/init_step_runner.go b/server/events/runtime/init_step_runner.go index 923f3ea1fd..a49477acd4 100644 --- a/server/events/runtime/init_step_runner.go +++ b/server/events/runtime/init_step_runner.go @@ -11,7 +11,7 @@ type InitStepRunner struct { DefaultTFVersion *version.Version } -func (i *InitStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []string, path string) (string, error) { +func (i *InitStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []string, path string, envs map[string]string) (string, error) { tfVersion := i.DefaultTFVersion if ctx.TerraformVersion != nil { tfVersion = ctx.TerraformVersion @@ -24,7 +24,7 @@ func (i *InitStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []strin terraformInitCmd = append([]string{"get", "-no-color", "-upgrade"}, extraArgs...) } - out, err := i.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, terraformInitCmd, tfVersion, ctx.Workspace) + out, err := i.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, terraformInitCmd, envs, tfVersion, ctx.Workspace) // Only include the init output if there was an error. Otherwise it's // unnecessary and lengthens the comment. if err != nil { diff --git a/server/events/runtime/init_step_runner_test.go b/server/events/runtime/init_step_runner_test.go index e989d89ac3..01e2eafa29 100644 --- a/server/events/runtime/init_step_runner_test.go +++ b/server/events/runtime/init_step_runner_test.go @@ -47,13 +47,13 @@ func TestRun_UsesGetOrInitForRightVersion(t *testing.T) { TerraformExecutor: terraform, DefaultTFVersion: tfVersion, } - When(terraform.RunCommandWithVersion(matchers.AnyPtrToLoggingSimpleLogger(), AnyString(), AnyStringSlice(), matchers2.AnyPtrToGoVersionVersion(), AnyString())). + When(terraform.RunCommandWithVersion(matchers.AnyPtrToLoggingSimpleLogger(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())). ThenReturn("output", nil) output, err := iso.Run(models.ProjectCommandContext{ Workspace: "workspace", RepoRelDir: ".", - }, []string{"extra", "args"}, "/path") + }, []string{"extra", "args"}, "/path", map[string]string(nil)) Ok(t, err) // When there is no error, should not return init output to PR. Equals(t, "", output) @@ -63,7 +63,7 @@ func TestRun_UsesGetOrInitForRightVersion(t *testing.T) { if c.expCmd == "get" { expArgs = []string{c.expCmd, "-no-color", "-upgrade", "extra", "args"} } - terraform.VerifyWasCalledOnce().RunCommandWithVersion(nil, "/path", expArgs, tfVersion, "workspace") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(nil, "/path", expArgs, map[string]string(nil), tfVersion, "workspace") }) } } @@ -72,7 +72,7 @@ func TestRun_ShowInitOutputOnError(t *testing.T) { // If there was an error during init then we want the output to be returned. RegisterMockTestingT(t) tfClient := mocks.NewMockClient() - When(tfClient.RunCommandWithVersion(matchers.AnyPtrToLoggingSimpleLogger(), AnyString(), AnyStringSlice(), matchers2.AnyPtrToGoVersionVersion(), AnyString())). + When(tfClient.RunCommandWithVersion(matchers.AnyPtrToLoggingSimpleLogger(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())). ThenReturn("output", errors.New("error")) tfVersion, _ := version.NewVersion("0.11.0") @@ -84,7 +84,7 @@ func TestRun_ShowInitOutputOnError(t *testing.T) { output, err := iso.Run(models.ProjectCommandContext{ Workspace: "workspace", RepoRelDir: ".", - }, nil, "/path") + }, nil, "/path", map[string]string(nil)) ErrEquals(t, "error", err) Equals(t, "output", output) } diff --git a/server/events/runtime/plan_step_runner.go b/server/events/runtime/plan_step_runner.go index 3a8014e627..0dab24a118 100644 --- a/server/events/runtime/plan_step_runner.go +++ b/server/events/runtime/plan_step_runner.go @@ -33,7 +33,7 @@ type PlanStepRunner struct { AsyncTFExec AsyncTFExec } -func (p *PlanStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []string, path string) (string, error) { +func (p *PlanStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []string, path string, envs map[string]string) (string, error) { tfVersion := p.DefaultTFVersion if ctx.TerraformVersion != nil { tfVersion = ctx.TerraformVersion @@ -41,16 +41,16 @@ func (p *PlanStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []strin // We only need to switch workspaces in version 0.9.*. In older versions, // there is no such thing as a workspace so we don't need to do anything. - if err := p.switchWorkspace(ctx, path, tfVersion); err != nil { + if err := p.switchWorkspace(ctx, path, tfVersion, envs); err != nil { return "", err } planFile := filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectName)) planCmd := p.buildPlanCmd(ctx, extraArgs, path, tfVersion, planFile) - output, err := p.TerraformExecutor.RunCommandWithVersion(ctx.Log, filepath.Clean(path), planCmd, tfVersion, ctx.Workspace) + output, err := p.TerraformExecutor.RunCommandWithVersion(ctx.Log, filepath.Clean(path), planCmd, envs, tfVersion, ctx.Workspace) if p.isRemoteOpsErr(output, err) { ctx.Log.Debug("detected that this project is using TFE remote ops") - return p.remotePlan(ctx, extraArgs, path, tfVersion, planFile) + return p.remotePlan(ctx, extraArgs, path, tfVersion, planFile, envs) } if err != nil { return output, err @@ -69,14 +69,14 @@ func (p *PlanStepRunner) isRemoteOpsErr(output string, err error) bool { // remotePlan runs a terraform plan command compatible with TFE remote // operations. -func (p *PlanStepRunner) remotePlan(ctx models.ProjectCommandContext, extraArgs []string, path string, tfVersion *version.Version, planFile string) (string, error) { +func (p *PlanStepRunner) remotePlan(ctx models.ProjectCommandContext, extraArgs []string, path string, tfVersion *version.Version, planFile string, envs map[string]string) (string, error) { argList := [][]string{ {"plan", "-input=false", "-refresh", "-no-color"}, extraArgs, ctx.EscapedCommentArgs, } args := p.flatten(argList) - output, err := p.runRemotePlan(ctx, args, path, tfVersion) + output, err := p.runRemotePlan(ctx, args, path, tfVersion, envs) if err != nil { return output, err } @@ -107,7 +107,7 @@ func (p *PlanStepRunner) remotePlan(ctx models.ProjectCommandContext, extraArgs // switchWorkspace changes the terraform workspace if necessary and will create // it if it doesn't exist. It handles differences between versions. -func (p *PlanStepRunner) switchWorkspace(ctx models.ProjectCommandContext, path string, tfVersion *version.Version) error { +func (p *PlanStepRunner) switchWorkspace(ctx models.ProjectCommandContext, path string, tfVersion *version.Version, envs map[string]string) error { // In versions less than 0.9 there is no support for workspaces. noWorkspaceSupport := MustConstraint("<0.9").Check(tfVersion) // If the user tried to set a specific workspace in the comment but their @@ -130,7 +130,7 @@ func (p *PlanStepRunner) switchWorkspace(ctx models.ProjectCommandContext, path // already in the right workspace then no need to switch. This will save us // about ten seconds. This command is only available in > 0.10. if !runningZeroPointNine { - workspaceShowOutput, err := p.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, []string{workspaceCmd, "show"}, tfVersion, ctx.Workspace) + workspaceShowOutput, err := p.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, []string{workspaceCmd, "show"}, envs, tfVersion, ctx.Workspace) if err != nil { return err } @@ -145,11 +145,11 @@ func (p *PlanStepRunner) switchWorkspace(ctx models.ProjectCommandContext, path // To do this we can either select and catch the error or use list and then // look for the workspace. Both commands take the same amount of time so // that's why we're running select here. - _, err := p.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, []string{workspaceCmd, "select", "-no-color", ctx.Workspace}, tfVersion, ctx.Workspace) + _, err := p.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, []string{workspaceCmd, "select", "-no-color", ctx.Workspace}, envs, tfVersion, ctx.Workspace) if err != nil { // If terraform workspace select fails we run terraform workspace // new to create a new workspace automatically. - _, err = p.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, []string{workspaceCmd, "new", "-no-color", ctx.Workspace}, tfVersion, ctx.Workspace) + _, err = p.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, []string{workspaceCmd, "new", "-no-color", ctx.Workspace}, envs, tfVersion, ctx.Workspace) return err } return nil @@ -250,7 +250,8 @@ func (p *PlanStepRunner) runRemotePlan( ctx models.ProjectCommandContext, cmdArgs []string, path string, - tfVersion *version.Version) (string, error) { + tfVersion *version.Version, + envs map[string]string) (string, error) { // updateStatusF will update the commit status and log any error. updateStatusF := func(status models.CommitStatus, url string) { @@ -261,7 +262,7 @@ func (p *PlanStepRunner) runRemotePlan( // Start the async command execution. ctx.Log.Debug("starting async tf remote operation") - _, outCh := p.AsyncTFExec.RunCommandAsync(ctx.Log, filepath.Clean(path), cmdArgs, tfVersion, ctx.Workspace) + _, outCh := p.AsyncTFExec.RunCommandAsync(ctx.Log, filepath.Clean(path), cmdArgs, envs, tfVersion, ctx.Workspace) var lines []string nextLineIsRunURL := false var runURL string diff --git a/server/events/runtime/plan_step_runner_test.go b/server/events/runtime/plan_step_runner_test.go index 58a00a5e85..f2acf3dcf2 100644 --- a/server/events/runtime/plan_step_runner_test.go +++ b/server/events/runtime/plan_step_runner_test.go @@ -36,7 +36,7 @@ func TestRun_NoWorkspaceIn08(t *testing.T) { TerraformExecutor: terraform, } - When(terraform.RunCommandWithVersion(matchers.AnyPtrToLoggingSimpleLogger(), AnyString(), AnyStringSlice(), matchers2.AnyPtrToGoVersionVersion(), AnyString())). + When(terraform.RunCommandWithVersion(matchers.AnyPtrToLoggingSimpleLogger(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())). ThenReturn("output", nil) output, err := s.Run(models.ProjectCommandContext{ Log: logger, @@ -52,7 +52,7 @@ func TestRun_NoWorkspaceIn08(t *testing.T) { Owner: "owner", Name: "repo", }, - }, []string{"extra", "args"}, "/path") + }, []string{"extra", "args"}, "/path", map[string]string(nil)) Ok(t, err) Equals(t, "output", output) @@ -79,6 +79,7 @@ func TestRun_NoWorkspaceIn08(t *testing.T) { "args", "comment", "args"}, + map[string]string(nil), tfVersion, workspace) @@ -89,6 +90,7 @@ func TestRun_NoWorkspaceIn08(t *testing.T) { "select", "-no-color", "workspace"}, + map[string]string(nil), tfVersion, workspace) terraform.VerifyWasCalled(Never()).RunCommandWithVersion(logger, @@ -97,6 +99,7 @@ func TestRun_NoWorkspaceIn08(t *testing.T) { "select", "-no-color", "workspace"}, + map[string]string(nil), tfVersion, workspace) } @@ -115,14 +118,14 @@ func TestRun_ErrWorkspaceIn08(t *testing.T) { DefaultTFVersion: tfVersion, } - When(terraform.RunCommandWithVersion(matchers.AnyPtrToLoggingSimpleLogger(), AnyString(), AnyStringSlice(), matchers2.AnyPtrToGoVersionVersion(), AnyString())). + When(terraform.RunCommandWithVersion(matchers.AnyPtrToLoggingSimpleLogger(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())). ThenReturn("output", nil) _, err := s.Run(models.ProjectCommandContext{ Log: logger, Workspace: workspace, RepoRelDir: ".", User: models.User{Username: "username"}, - }, []string{"extra", "args"}, "/path") + }, []string{"extra", "args"}, "/path", map[string]string(nil)) ErrEquals(t, "terraform version 0.8.0 does not support workspaces", err) } @@ -163,7 +166,7 @@ func TestRun_SwitchesWorkspace(t *testing.T) { DefaultTFVersion: tfVersion, } - When(terraform.RunCommandWithVersion(matchers.AnyPtrToLoggingSimpleLogger(), AnyString(), AnyStringSlice(), matchers2.AnyPtrToGoVersionVersion(), AnyString())). + When(terraform.RunCommandWithVersion(matchers.AnyPtrToLoggingSimpleLogger(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())). ThenReturn("output", nil) output, err := s.Run(models.ProjectCommandContext{ Log: logger, @@ -179,7 +182,7 @@ func TestRun_SwitchesWorkspace(t *testing.T) { Owner: "owner", Name: "repo", }, - }, []string{"extra", "args"}, "/path") + }, []string{"extra", "args"}, "/path", map[string]string(nil)) Ok(t, err) Equals(t, "output", output) @@ -190,6 +193,7 @@ func TestRun_SwitchesWorkspace(t *testing.T) { "select", "-no-color", "workspace"}, + map[string]string(nil), tfVersion, "workspace") terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, @@ -214,6 +218,7 @@ func TestRun_SwitchesWorkspace(t *testing.T) { "args", "comment", "args"}, + map[string]string(nil), tfVersion, "workspace") }) @@ -258,10 +263,10 @@ func TestRun_CreatesWorkspace(t *testing.T) { // Ensure that we actually try to switch workspaces by making the // output of `workspace show` to be a different name. - When(terraform.RunCommandWithVersion(logger, "/path", []string{"workspace", "show"}, tfVersion, "workspace")).ThenReturn("diffworkspace\n", nil) + When(terraform.RunCommandWithVersion(logger, "/path", []string{"workspace", "show"}, map[string]string(nil), tfVersion, "workspace")).ThenReturn("diffworkspace\n", nil) expWorkspaceArgs := []string{c.expWorkspaceCommand, "select", "-no-color", "workspace"} - When(terraform.RunCommandWithVersion(logger, "/path", expWorkspaceArgs, tfVersion, "workspace")).ThenReturn("", errors.New("workspace does not exist")) + When(terraform.RunCommandWithVersion(logger, "/path", expWorkspaceArgs, map[string]string(nil), tfVersion, "workspace")).ThenReturn("", errors.New("workspace does not exist")) expPlanArgs := []string{"plan", "-input=false", @@ -283,7 +288,7 @@ func TestRun_CreatesWorkspace(t *testing.T) { "args", "comment", "args"} - When(terraform.RunCommandWithVersion(logger, "/path", expPlanArgs, tfVersion, "workspace")).ThenReturn("output", nil) + When(terraform.RunCommandWithVersion(logger, "/path", expPlanArgs, map[string]string(nil), tfVersion, "workspace")).ThenReturn("output", nil) output, err := s.Run(models.ProjectCommandContext{ Log: logger, @@ -299,13 +304,13 @@ func TestRun_CreatesWorkspace(t *testing.T) { Owner: "owner", Name: "repo", }, - }, []string{"extra", "args"}, "/path") + }, []string{"extra", "args"}, "/path", map[string]string(nil)) Ok(t, err) Equals(t, "output", output) // Verify that env select was called as well as plan. - terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, "/path", expWorkspaceArgs, tfVersion, "workspace") - terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, "/path", expPlanArgs, tfVersion, "workspace") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, "/path", expWorkspaceArgs, map[string]string(nil), tfVersion, "workspace") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, "/path", expPlanArgs, map[string]string(nil), tfVersion, "workspace") }) } } @@ -321,7 +326,7 @@ func TestRun_NoWorkspaceSwitchIfNotNecessary(t *testing.T) { TerraformExecutor: terraform, DefaultTFVersion: tfVersion, } - When(terraform.RunCommandWithVersion(logger, "/path", []string{"workspace", "show"}, tfVersion, "workspace")).ThenReturn("workspace\n", nil) + When(terraform.RunCommandWithVersion(logger, "/path", []string{"workspace", "show"}, map[string]string(nil), tfVersion, "workspace")).ThenReturn("workspace\n", nil) expPlanArgs := []string{"plan", "-input=false", @@ -343,7 +348,7 @@ func TestRun_NoWorkspaceSwitchIfNotNecessary(t *testing.T) { "args", "comment", "args"} - When(terraform.RunCommandWithVersion(logger, "/path", expPlanArgs, tfVersion, "workspace")).ThenReturn("output", nil) + When(terraform.RunCommandWithVersion(logger, "/path", expPlanArgs, map[string]string(nil), tfVersion, "workspace")).ThenReturn("output", nil) output, err := s.Run(models.ProjectCommandContext{ Log: logger, @@ -359,14 +364,14 @@ func TestRun_NoWorkspaceSwitchIfNotNecessary(t *testing.T) { Owner: "owner", Name: "repo", }, - }, []string{"extra", "args"}, "/path") + }, []string{"extra", "args"}, "/path", map[string]string(nil)) Ok(t, err) Equals(t, "output", output) - terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, "/path", expPlanArgs, tfVersion, "workspace") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, "/path", expPlanArgs, map[string]string(nil), tfVersion, "workspace") // Verify that workspace select was never called. - terraform.VerifyWasCalled(Never()).RunCommandWithVersion(logger, "/path", []string{"workspace", "select", "-no-color", "workspace"}, tfVersion, "workspace") + terraform.VerifyWasCalled(Never()).RunCommandWithVersion(logger, "/path", []string{"workspace", "select", "-no-color", "workspace"}, map[string]string(nil), tfVersion, "workspace") } func TestRun_AddsEnvVarFile(t *testing.T) { @@ -414,7 +419,7 @@ func TestRun_AddsEnvVarFile(t *testing.T) { "-var-file", envVarsFile, } - When(terraform.RunCommandWithVersion(logger, tmpDir, expPlanArgs, tfVersion, "workspace")).ThenReturn("output", nil) + When(terraform.RunCommandWithVersion(logger, tmpDir, expPlanArgs, map[string]string(nil), tfVersion, "workspace")).ThenReturn("output", nil) output, err := s.Run(models.ProjectCommandContext{ Log: logger, @@ -430,12 +435,12 @@ func TestRun_AddsEnvVarFile(t *testing.T) { Owner: "owner", Name: "repo", }, - }, []string{"extra", "args"}, tmpDir) + }, []string{"extra", "args"}, tmpDir, map[string]string(nil)) Ok(t, err) // Verify that env select was never called since we're in version >= 0.10 - terraform.VerifyWasCalled(Never()).RunCommandWithVersion(logger, tmpDir, []string{"env", "select", "-no-color", "workspace"}, tfVersion, "workspace") - terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, tmpDir, expPlanArgs, tfVersion, "workspace") + terraform.VerifyWasCalled(Never()).RunCommandWithVersion(logger, tmpDir, []string{"env", "select", "-no-color", "workspace"}, map[string]string(nil), tfVersion, "workspace") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, tmpDir, expPlanArgs, map[string]string(nil), tfVersion, "workspace") Equals(t, "output", output) } @@ -450,7 +455,7 @@ func TestRun_UsesDiffPathForProject(t *testing.T) { TerraformExecutor: terraform, DefaultTFVersion: tfVersion, } - When(terraform.RunCommandWithVersion(logger, "/path", []string{"workspace", "show"}, tfVersion, "workspace")).ThenReturn("workspace\n", nil) + When(terraform.RunCommandWithVersion(logger, "/path", []string{"workspace", "show"}, map[string]string(nil), tfVersion, "workspace")).ThenReturn("workspace\n", nil) expPlanArgs := []string{"plan", "-input=false", @@ -473,7 +478,7 @@ func TestRun_UsesDiffPathForProject(t *testing.T) { "comment", "args", } - When(terraform.RunCommandWithVersion(logger, "/path", expPlanArgs, tfVersion, "default")).ThenReturn("output", nil) + When(terraform.RunCommandWithVersion(logger, "/path", expPlanArgs, map[string]string(nil), tfVersion, "default")).ThenReturn("output", nil) output, err := s.Run(models.ProjectCommandContext{ Log: logger, @@ -490,7 +495,7 @@ func TestRun_UsesDiffPathForProject(t *testing.T) { Owner: "owner", Name: "repo", }, - }, []string{"extra", "args"}, "/path") + }, []string{"extra", "args"}, "/path", map[string]string(nil)) Ok(t, err) Equals(t, "output", output) } @@ -534,6 +539,7 @@ Terraform will perform the following actions: matchers.AnyPtrToLoggingSimpleLogger(), AnyString(), AnyStringSlice(), + matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())). Then(func(params []Param) ReturnValues { @@ -548,7 +554,7 @@ Terraform will perform the following actions: return []ReturnValue{"", errors.New("unexpected call to RunCommandWithVersion")} } }) - actOutput, err := s.Run(models.ProjectCommandContext{Workspace: "default"}, nil, "") + actOutput, err := s.Run(models.ProjectCommandContext{Workspace: "default"}, nil, "", map[string]string(nil)) Ok(t, err) Equals(t, ` An execution plan has been generated and is shown below. @@ -587,6 +593,7 @@ func TestRun_OutputOnErr(t *testing.T) { matchers.AnyPtrToLoggingSimpleLogger(), AnyString(), AnyStringSlice(), + matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())). Then(func(params []Param) ReturnValues { @@ -601,7 +608,7 @@ func TestRun_OutputOnErr(t *testing.T) { return []ReturnValue{"", errors.New("unexpected call to RunCommandWithVersion")} } }) - actOutput, actErr := s.Run(models.ProjectCommandContext{Workspace: "default"}, nil, "") + actOutput, actErr := s.Run(models.ProjectCommandContext{Workspace: "default"}, nil, "", map[string]string(nil)) ErrEquals(t, expErrMsg, actErr) Equals(t, expOutput, actOutput) } @@ -623,6 +630,7 @@ func TestRun_NoOptionalVarsIn012(t *testing.T) { matchers.AnyPtrToLoggingSimpleLogger(), AnyString(), AnyStringSlice(), + matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).ThenReturn("output", nil) @@ -639,7 +647,7 @@ func TestRun_NoOptionalVarsIn012(t *testing.T) { Owner: "owner", Name: "repo", }, - }, []string{"extra", "args"}, "/path") + }, []string{"extra", "args"}, "/path", map[string]string(nil)) Ok(t, err) Equals(t, "output", output) @@ -654,7 +662,7 @@ func TestRun_NoOptionalVarsIn012(t *testing.T) { "comment", "args", } - terraform.VerifyWasCalledOnce().RunCommandWithVersion(nil, "/path", expPlanArgs, tfVersion, "default") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(nil, "/path", expPlanArgs, map[string]string(nil), tfVersion, "default") } // Test plans if using remote ops. @@ -696,6 +704,7 @@ locally at this time. nil, absProjectPath, []string{"workspace", "show"}, + map[string]string(nil), tfVersion, "default")).ThenReturn("default\n", nil) @@ -725,7 +734,7 @@ locally at this time. planErr := errors.New("exit status 1: err") planOutput := "\n" + remoteOpsErr asyncTf.LinesToSend = remotePlanOutput - When(terraform.RunCommandWithVersion(nil, absProjectPath, expPlanArgs, tfVersion, "default")). + When(terraform.RunCommandWithVersion(nil, absProjectPath, expPlanArgs, map[string]string(nil), tfVersion, "default")). ThenReturn(planOutput, planErr) // Now that mocking is set up, we're ready to run the plan. @@ -743,7 +752,7 @@ locally at this time. Name: "repo", }, } - output, err := s.Run(ctx, []string{"extra", "args"}, absProjectPath) + output, err := s.Run(ctx, []string{"extra", "args"}, absProjectPath, map[string]string(nil)) Ok(t, err) Equals(t, ` An execution plan has been generated and is shown below. @@ -791,7 +800,7 @@ type remotePlanMock struct { CalledArgs []string } -func (r *remotePlanMock) RunCommandAsync(log *logging.SimpleLogger, path string, args []string, v *version.Version, workspace string) (chan<- string, <-chan terraform.Line) { +func (r *remotePlanMock) RunCommandAsync(log *logging.SimpleLogger, path string, args []string, envs map[string]string, v *version.Version, workspace string) (chan<- string, <-chan terraform.Line) { r.CalledArgs = args in := make(chan string) out := make(chan terraform.Line) diff --git a/server/events/runtime/run_step_runner.go b/server/events/runtime/run_step_runner.go index 842782e88b..3cec95bb87 100644 --- a/server/events/runtime/run_step_runner.go +++ b/server/events/runtime/run_step_runner.go @@ -19,7 +19,7 @@ type RunStepRunner struct { TerraformBinDir string } -func (r *RunStepRunner) Run(ctx models.ProjectCommandContext, command string, path string) (string, error) { +func (r *RunStepRunner) Run(ctx models.ProjectCommandContext, command string, path string, envs map[string]string) (string, error) { tfVersion := r.DefaultTFVersion if ctx.TerraformVersion != nil { tfVersion = ctx.TerraformVersion @@ -59,6 +59,9 @@ func (r *RunStepRunner) Run(ctx models.ProjectCommandContext, command string, pa for key, val := range customEnvVars { finalEnvVars = append(finalEnvVars, fmt.Sprintf("%s=%s", key, val)) } + for key, val := range envs { + finalEnvVars = append(finalEnvVars, fmt.Sprintf("%s=%s", key, val)) + } cmd.Env = finalEnvVars out, err := cmd.CombinedOutput() diff --git a/server/events/runtime/run_step_runner_test.go b/server/events/runtime/run_step_runner_test.go index 28a35bcd79..7f739e48e2 100644 --- a/server/events/runtime/run_step_runner_test.go +++ b/server/events/runtime/run_step_runner_test.go @@ -137,7 +137,7 @@ func TestRunStepRunner_Run(t *testing.T) { ProjectName: c.ProjectName, EscapedCommentArgs: []string{"-target=resource1", "-target=resource2"}, } - out, err := r.Run(ctx, c.Command, tmpDir) + out, err := r.Run(ctx, c.Command, tmpDir, map[string]string{"test": "var"}) if c.ExpErr != "" { ErrContains(t, c.ExpErr, err) return diff --git a/server/events/runtime/runtime.go b/server/events/runtime/runtime.go index bd53d64880..d671a40694 100644 --- a/server/events/runtime/runtime.go +++ b/server/events/runtime/runtime.go @@ -24,7 +24,7 @@ const ( // TerraformExec brings the interface from TerraformClient into this package // without causing circular imports. type TerraformExec interface { - RunCommandWithVersion(log *logging.SimpleLogger, path string, args []string, v *version.Version, workspace string) (string, error) + RunCommandWithVersion(log *logging.SimpleLogger, path string, args []string, envs map[string]string, v *version.Version, workspace string) (string, error) EnsureVersion(log *logging.SimpleLogger, v *version.Version) error } @@ -39,7 +39,7 @@ type AsyncTFExec interface { // Callers can use the input channel to pass stdin input to the command. // If any error is passed on the out channel, there will be no // further output (so callers are free to exit). - RunCommandAsync(log *logging.SimpleLogger, path string, args []string, v *version.Version, workspace string) (chan<- string, <-chan terraform.Line) + RunCommandAsync(log *logging.SimpleLogger, path string, args []string, envs map[string]string, v *version.Version, workspace string) (chan<- string, <-chan terraform.Line) } // StatusUpdater brings the interface from CommitStatusUpdater into this package diff --git a/server/events/terraform/mocks/matchers/map_of_string_to_string.go b/server/events/terraform/mocks/matchers/map_of_string_to_string.go new file mode 100644 index 0000000000..4d969915af --- /dev/null +++ b/server/events/terraform/mocks/matchers/map_of_string_to_string.go @@ -0,0 +1,21 @@ +// Code generated by pegomock. DO NOT EDIT. +package matchers + +import ( + "reflect" + "github.com/petergtz/pegomock" + + +) + +func AnyMapOfStringToString() map[string]string { + pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(map[string]string))(nil)).Elem())) + var nullValue map[string]string + return nullValue +} + +func EqMapOfStringToString(value map[string]string) map[string]string { + pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) + var nullValue map[string]string + return nullValue +} diff --git a/server/events/terraform/mocks/mock_terraform_client.go b/server/events/terraform/mocks/mock_terraform_client.go index 39c3eca559..5e4b2b1802 100644 --- a/server/events/terraform/mocks/mock_terraform_client.go +++ b/server/events/terraform/mocks/mock_terraform_client.go @@ -26,11 +26,11 @@ func NewMockClient(options ...pegomock.Option) *MockClient { func (mock *MockClient) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } func (mock *MockClient) FailHandler() pegomock.FailHandler { return mock.fail } -func (mock *MockClient) RunCommandWithVersion(log *logging.SimpleLogger, path string, args []string, v *go_version.Version, workspace string) (string, error) { +func (mock *MockClient) RunCommandWithVersion(log *logging.SimpleLogger, path string, args []string, envs map[string]string, v *go_version.Version, workspace string) (string, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockClient().") } - params := []pegomock.Param{log, path, args, v, workspace} + params := []pegomock.Param{log, path, args, envs, v, workspace} result := pegomock.GetGenericMockFrom(mock).Invoke("RunCommandWithVersion", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) var ret0 string var ret1 error @@ -97,8 +97,8 @@ type VerifierMockClient struct { timeout time.Duration } -func (verifier *VerifierMockClient) RunCommandWithVersion(log *logging.SimpleLogger, path string, args []string, v *go_version.Version, workspace string) *MockClient_RunCommandWithVersion_OngoingVerification { - params := []pegomock.Param{log, path, args, v, workspace} +func (verifier *VerifierMockClient) RunCommandWithVersion(log *logging.SimpleLogger, path string, args []string, envs map[string]string, v *go_version.Version, workspace string) *MockClient_RunCommandWithVersion_OngoingVerification { + params := []pegomock.Param{log, path, args, envs, v, workspace} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "RunCommandWithVersion", params, verifier.timeout) return &MockClient_RunCommandWithVersion_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } @@ -108,12 +108,12 @@ type MockClient_RunCommandWithVersion_OngoingVerification struct { methodInvocations []pegomock.MethodInvocation } -func (c *MockClient_RunCommandWithVersion_OngoingVerification) GetCapturedArguments() (*logging.SimpleLogger, string, []string, *go_version.Version, string) { - log, path, args, v, workspace := c.GetAllCapturedArguments() - return log[len(log)-1], path[len(path)-1], args[len(args)-1], v[len(v)-1], workspace[len(workspace)-1] +func (c *MockClient_RunCommandWithVersion_OngoingVerification) GetCapturedArguments() (*logging.SimpleLogger, string, []string, map[string]string, *go_version.Version, string) { + log, path, args, envs, v, workspace := c.GetAllCapturedArguments() + return log[len(log)-1], path[len(path)-1], args[len(args)-1], envs[len(envs)-1], v[len(v)-1], workspace[len(workspace)-1] } -func (c *MockClient_RunCommandWithVersion_OngoingVerification) GetAllCapturedArguments() (_param0 []*logging.SimpleLogger, _param1 []string, _param2 [][]string, _param3 []*go_version.Version, _param4 []string) { +func (c *MockClient_RunCommandWithVersion_OngoingVerification) GetAllCapturedArguments() (_param0 []*logging.SimpleLogger, _param1 []string, _param2 [][]string, _param3 []map[string]string, _param4 []*go_version.Version, _param5 []string) { params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) if len(params) > 0 { _param0 = make([]*logging.SimpleLogger, len(params[0])) @@ -128,13 +128,17 @@ func (c *MockClient_RunCommandWithVersion_OngoingVerification) GetAllCapturedArg for u, param := range params[2] { _param2[u] = param.([]string) } - _param3 = make([]*go_version.Version, len(params[3])) + _param3 = make([]map[string]string, len(params[3])) for u, param := range params[3] { - _param3[u] = param.(*go_version.Version) + _param3[u] = param.(map[string]string) } - _param4 = make([]string, len(params[4])) + _param4 = make([]*go_version.Version, len(params[4])) for u, param := range params[4] { - _param4[u] = param.(string) + _param4[u] = param.(*go_version.Version) + } + _param5 = make([]string, len(params[5])) + for u, param := range params[5] { + _param5[u] = param.(string) } } return diff --git a/server/events/terraform/terraform_client.go b/server/events/terraform/terraform_client.go index 4e5fad850b..8d7b448205 100644 --- a/server/events/terraform/terraform_client.go +++ b/server/events/terraform/terraform_client.go @@ -40,7 +40,7 @@ type Client interface { // RunCommandWithVersion executes terraform with args in path. If v is nil, // it will use the default Terraform version. workspace is the Terraform // workspace which should be set as an environment variable. - RunCommandWithVersion(log *logging.SimpleLogger, path string, args []string, v *version.Version, workspace string) (string, error) + RunCommandWithVersion(log *logging.SimpleLogger, path string, args []string, envs map[string]string, v *version.Version, workspace string) (string, error) // EnsureVersion makes sure that terraform version `v` is available to use EnsureVersion(log *logging.SimpleLogger, v *version.Version) error @@ -210,11 +210,16 @@ func (c *DefaultClient) EnsureVersion(log *logging.SimpleLogger, v *version.Vers } // See Client.RunCommandWithVersion. -func (c *DefaultClient) RunCommandWithVersion(log *logging.SimpleLogger, path string, args []string, v *version.Version, workspace string) (string, error) { +func (c *DefaultClient) RunCommandWithVersion(log *logging.SimpleLogger, path string, args []string, customEnvVars map[string]string, v *version.Version, workspace string) (string, error) { tfCmd, cmd, err := c.prepCmd(log, v, workspace, path, args) if err != nil { return "", err } + envVars := cmd.Env + for key, val := range customEnvVars { + envVars = append(envVars, fmt.Sprintf("%s=%s", key, val)) + } + cmd.Env = envVars out, err := cmd.CombinedOutput() if err != nil { err = errors.Wrapf(err, "running %q in %q", tfCmd, path) @@ -282,7 +287,7 @@ type Line struct { // Callers can use the input channel to pass stdin input to the command. // If any error is passed on the out channel, there will be no // further output (so callers are free to exit). -func (c *DefaultClient) RunCommandAsync(log *logging.SimpleLogger, path string, args []string, v *version.Version, workspace string) (chan<- string, <-chan Line) { +func (c *DefaultClient) RunCommandAsync(log *logging.SimpleLogger, path string, args []string, customEnvVars map[string]string, v *version.Version, workspace string) (chan<- string, <-chan Line) { outCh := make(chan Line) inCh := make(chan string) @@ -305,6 +310,11 @@ func (c *DefaultClient) RunCommandAsync(log *logging.SimpleLogger, path string, stdout, _ := cmd.StdoutPipe() stderr, _ := cmd.StderrPipe() stdin, _ := cmd.StdinPipe() + envVars := cmd.Env + for key, val := range customEnvVars { + envVars = append(envVars, fmt.Sprintf("%s=%s", key, val)) + } + cmd.Env = envVars log.Debug("starting %q in %q", tfCmd, path) err = cmd.Start() diff --git a/server/events/terraform/terraform_client_internal_test.go b/server/events/terraform/terraform_client_internal_test.go index ca58664bff..97c40795cd 100644 --- a/server/events/terraform/terraform_client_internal_test.go +++ b/server/events/terraform/terraform_client_internal_test.go @@ -103,7 +103,7 @@ func TestDefaultClient_RunCommandWithVersion_EnvVars(t *testing.T) { "ATLANTIS_TERRAFORM_VERSION=$ATLANTIS_TERRAFORM_VERSION", "DIR=$DIR", } - out, err := client.RunCommandWithVersion(nil, tmp, args, nil, "workspace") + out, err := client.RunCommandWithVersion(nil, tmp, args, map[string]string{}, nil, "workspace") Ok(t, err) exp := fmt.Sprintf("TF_IN_AUTOMATION=true TF_PLUGIN_CACHE_DIR=%s WORKSPACE=workspace ATLANTIS_TERRAFORM_VERSION=0.11.11 DIR=%s\n", tmp, tmp) Equals(t, exp, out) @@ -128,7 +128,7 @@ func TestDefaultClient_RunCommandWithVersion_Error(t *testing.T) { "1", } log := logging.NewSimpleLogger("test", false, logging.Debug) - out, err := client.RunCommandWithVersion(log, tmp, args, nil, "workspace") + out, err := client.RunCommandWithVersion(log, tmp, args, map[string]string{}, nil, "workspace") ErrEquals(t, fmt.Sprintf(`running "echo dying && exit 1" in %q: exit status 1`, tmp), err) // Test that we still get our output. Equals(t, "dying\n", out) @@ -152,7 +152,7 @@ func TestDefaultClient_RunCommandAsync_Success(t *testing.T) { "ATLANTIS_TERRAFORM_VERSION=$ATLANTIS_TERRAFORM_VERSION", "DIR=$DIR", } - _, outCh := client.RunCommandAsync(nil, tmp, args, nil, "workspace") + _, outCh := client.RunCommandAsync(nil, tmp, args, map[string]string{}, nil, "workspace") out, err := waitCh(outCh) Ok(t, err) @@ -181,7 +181,7 @@ func TestDefaultClient_RunCommandAsync_BigOutput(t *testing.T) { _, err = f.WriteString(s) Ok(t, err) } - _, outCh := client.RunCommandAsync(nil, tmp, []string{filename}, nil, "workspace") + _, outCh := client.RunCommandAsync(nil, tmp, []string{filename}, map[string]string{}, nil, "workspace") out, err := waitCh(outCh) Ok(t, err) @@ -199,7 +199,7 @@ func TestDefaultClient_RunCommandAsync_StderrOutput(t *testing.T) { overrideTF: "echo", } log := logging.NewSimpleLogger("test", false, logging.Debug) - _, outCh := client.RunCommandAsync(log, tmp, []string{"stderr", ">&2"}, nil, "workspace") + _, outCh := client.RunCommandAsync(log, tmp, []string{"stderr", ">&2"}, map[string]string{}, nil, "workspace") out, err := waitCh(outCh) Ok(t, err) @@ -217,7 +217,7 @@ func TestDefaultClient_RunCommandAsync_ExitOne(t *testing.T) { overrideTF: "echo", } log := logging.NewSimpleLogger("test", false, logging.Debug) - _, outCh := client.RunCommandAsync(log, tmp, []string{"dying", "&&", "exit", "1"}, nil, "workspace") + _, outCh := client.RunCommandAsync(log, tmp, []string{"dying", "&&", "exit", "1"}, map[string]string{}, nil, "workspace") out, err := waitCh(outCh) ErrEquals(t, fmt.Sprintf(`running "echo dying && exit 1" in %q: exit status 1`, tmp), err) @@ -236,7 +236,7 @@ func TestDefaultClient_RunCommandAsync_Input(t *testing.T) { overrideTF: "read", } log := logging.NewSimpleLogger("test", false, logging.Debug) - inCh, outCh := client.RunCommandAsync(log, tmp, []string{"a", "&&", "echo", "$a"}, nil, "workspace") + inCh, outCh := client.RunCommandAsync(log, tmp, []string{"a", "&&", "echo", "$a"}, map[string]string{}, nil, "workspace") inCh <- "echo me\n" out, err := waitCh(outCh) diff --git a/server/events/terraform/terraform_client_test.go b/server/events/terraform/terraform_client_test.go index a38d7c9110..39bcb85940 100644 --- a/server/events/terraform/terraform_client_test.go +++ b/server/events/terraform/terraform_client_test.go @@ -74,7 +74,7 @@ is 0.11.13. You can update by downloading from www.terraform.io/downloads.html Ok(t, err) Equals(t, "0.11.10", c.DefaultVersion().String()) - output, err := c.RunCommandWithVersion(nil, tmp, nil, nil, "") + output, err := c.RunCommandWithVersion(nil, tmp, nil, map[string]string{"test": "123"}, nil, "") Ok(t, err) Equals(t, fakeBinOut+"\n", output) } @@ -102,7 +102,7 @@ is 0.11.13. You can update by downloading from www.terraform.io/downloads.html Ok(t, err) Equals(t, "0.11.10", c.DefaultVersion().String()) - output, err := c.RunCommandWithVersion(nil, tmp, nil, nil, "") + output, err := c.RunCommandWithVersion(nil, tmp, nil, map[string]string{}, nil, "") Ok(t, err) Equals(t, fakeBinOut+"\n", output) } @@ -139,7 +139,7 @@ func TestNewClient_DefaultTFFlagInPath(t *testing.T) { Ok(t, err) Equals(t, "0.11.10", c.DefaultVersion().String()) - output, err := c.RunCommandWithVersion(nil, tmp, nil, nil, "") + output, err := c.RunCommandWithVersion(nil, tmp, nil, map[string]string{}, nil, "") Ok(t, err) Equals(t, fakeBinOut+"\n", output) } @@ -163,7 +163,7 @@ func TestNewClient_DefaultTFFlagInBinDir(t *testing.T) { Ok(t, err) Equals(t, "0.11.10", c.DefaultVersion().String()) - output, err := c.RunCommandWithVersion(nil, tmp, nil, nil, "") + output, err := c.RunCommandWithVersion(nil, tmp, nil, map[string]string{}, nil, "") Ok(t, err) Equals(t, fakeBinOut+"\n", output) } @@ -198,7 +198,7 @@ func TestNewClient_DefaultTFFlagDownload(t *testing.T) { // Reset PATH so that it has sh. Ok(t, os.Setenv("PATH", orig)) - output, err := c.RunCommandWithVersion(nil, tmp, nil, nil, "") + output, err := c.RunCommandWithVersion(nil, tmp, nil, map[string]string{}, nil, "") Ok(t, err) Equals(t, "\nTerraform v0.11.10\n\n", output) } @@ -236,7 +236,7 @@ func TestRunCommandWithVersion_DLsTF(t *testing.T) { v, err := version.NewVersion("99.99.99") Ok(t, err) - output, err := c.RunCommandWithVersion(nil, tmp, nil, v, "") + output, err := c.RunCommandWithVersion(nil, tmp, nil, map[string]string{}, v, "") Assert(t, err == nil, "err: %s: %s", err, output) Equals(t, "\nTerraform v99.99.99\n\n", output) } diff --git a/server/events/yaml/raw/step.go b/server/events/yaml/raw/step.go index 12e22a5d6a..b4026ae7b9 100644 --- a/server/events/yaml/raw/step.go +++ b/server/events/yaml/raw/step.go @@ -13,20 +13,28 @@ import ( const ( ExtraArgsKey = "extra_args" + NameArgKey = "name" + CommandArgKey = "command" + ValueArgKey = "value" RunStepName = "run" PlanStepName = "plan" ApplyStepName = "apply" InitStepName = "init" + EnvStepName = "env" ) // Step represents a single action/command to perform. In YAML, it can be set as // 1. A single string for a built-in command: // - init // - plan -// 2. A map for a built-in command and extra_args: +// 2. A map for a built-in env with name and command +// - env: +// name: test +// command: echo 312 +// 3. A map for a built-in command and extra_args: // - plan: // extra_args: [-var-file=staging.tfvars] -// 3. A map for a custom run command: +// 4. A map for a custom run command: // - run: my custom command // Here we parse step in the most generic fashion possible. See fields for more // details. @@ -35,8 +43,10 @@ type Step struct { // could be multiple keys (since the element is a map) so we don't set Key. Key *string // Map will be set in case #2 above. + Env 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 #3 above. + // StringVal will be set in case #4 above. StringVal map[string]string } @@ -52,7 +62,7 @@ func (s *Step) UnmarshalJSON(data []byte) error { func (s Step) Validate() error { validStep := func(value interface{}) error { str := *value.(*string) - if str != InitStepName && str != PlanStepName && str != ApplyStepName { + if str != InitStepName && str != PlanStepName && str != ApplyStepName && str != EnvStepName { return fmt.Errorf("%q is not a valid step type, maybe you omitted the 'run' key", str) } return nil @@ -94,6 +104,40 @@ func (s Step) Validate() error { return nil } + envStep := func(value interface{}) error { + elem := value.(map[string]map[string]string) + var keys []string + for k := range elem { + keys = append(keys, k) + } + // Sort so tests can be deterministic. + sort.Strings(keys) + + if len(keys) > 1 { + 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) + } + var argKeys []string + for k := range args { + argKeys = append(argKeys, k) + } + if len(argKeys) != 2 { + return fmt.Errorf("built-in steps only support two keys %s and %s or %s, found %d: %s", + NameArgKey, CommandArgKey, ValueArgKey, len(argKeys), strings.Join(argKeys, ",")) + } + for k := range args { + if k != NameArgKey && k != CommandArgKey && k != ValueArgKey { + return fmt.Errorf("built-in steps only support two keys %s and %s or %s, found %q in step %s", NameArgKey, CommandArgKey, ValueArgKey, k, stepName) + } + } + } + return nil + } + runStep := func(value interface{}) error { elem := value.(map[string]string) var keys []string @@ -121,6 +165,9 @@ 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.StringVal) > 0 { return validation.Validate(s.StringVal, validation.By(runStep)) } @@ -136,6 +183,20 @@ func (s Step) ToValid() valid.Step { } // This will trigger in case #2 (see Step docs). + if len(s.Env) > 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{ + StepName: stepName, + EnvVarName: stepArgs[NameArgKey], + RunCommand: stepArgs[CommandArgKey], + EnvVarValue: stepArgs[ValueArgKey], + } + } + } + + // This will trigger in case #3 (see Step docs). if len(s.Map) > 0 { // After validation we assume there's only one key and it's a valid // step name so we just use the first one. @@ -147,7 +208,7 @@ func (s Step) ToValid() valid.Step { } } - // This will trigger in case #3 (see Step docs). + // This will trigger in case #4 (see Step docs). 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. @@ -196,6 +257,19 @@ func (s *Step) unmarshalGeneric(unmarshal func(interface{}) error) error { return nil } + // This represents a step with extra_args, ex: + // env: + // name: k + // value: hi //optional + // command: exec + // We validate if the key env + var envStep map[string]map[string]string + err = unmarshal(&envStep) + if err == nil { + s.Env = envStep + return nil + } + // Try to unmarshal as a custom run step, ex. // steps: // - run: my command diff --git a/server/events/yaml/raw/step_test.go b/server/events/yaml/raw/step_test.go index fd0cbe3c93..001136c184 100644 --- a/server/events/yaml/raw/step_test.go +++ b/server/events/yaml/raw/step_test.go @@ -73,6 +73,21 @@ key2: }, }, }, + { + description: "env step", + input: ` +env: + command: echo 123 + name: test`, + exp: raw.Step{ + Env: EnvType{ + "env": { + "command": "echo 123", + "name": "test", + }, + }, + }, + }, // Run-step style { @@ -106,6 +121,7 @@ key: value`, Key: nil, Map: nil, StringVal: nil, + Env: nil, }, }, @@ -184,6 +200,18 @@ func TestStep_Validate(t *testing.T) { }, expErr: "", }, + { + description: "env", + input: raw.Step{ + Env: EnvType{ + "env": { + "name": "test", + "command": "echo 123", + }, + }, + }, + expErr: "", + }, { description: "apply extra_args", input: raw.Step{ @@ -228,6 +256,16 @@ func TestStep_Validate(t *testing.T) { }, expErr: "step element can only contain a single key, found 2: key1,key2", }, + { + description: "multiple keys in env", + input: raw.Step{ + Env: EnvType{ + "key1": nil, + "key2": nil, + }, + }, + expErr: "step element can only contain a single key, found 2: key1,key2", + }, { description: "multiple keys in string val", input: raw.Step{ @@ -247,6 +285,15 @@ func TestStep_Validate(t *testing.T) { }, expErr: "\"invalid\" is not a valid step type", }, + { + description: "invalid key in env", + input: raw.Step{ + Env: EnvType{ + "invalid": nil, + }, + }, + expErr: "\"invalid\" is not a valid step type", + }, { description: "invalid key in string val", input: raw.Step{ @@ -267,6 +314,41 @@ func TestStep_Validate(t *testing.T) { }, expErr: "built-in steps only support a single extra_args key, found \"invalid\" in step init", }, + // { + // description: "non extra_arg key", + // input: raw.Step{ + // Map: MapType{ + // "init": { + // "invalid": nil, + // "zzzzzzz": nil, + // }, + // }, + // }, + // expErr: "built-in steps only support a single extra_args key, found 2: invalid,zzzzzzz", + // }, + { + description: "incorrect keys in env", + input: raw.Step{ + Env: EnvType{ + "env": { + "abc": "", + "invalid2": "", + }, + }, + }, + expErr: "built-in steps only support two keys name and command or value, found \"abc\" in step env", + }, + { + description: "non two keys in env", + input: raw.Step{ + Env: EnvType{ + "env": { + "invalid": "", + }, + }, + }, + expErr: "built-in steps only support two keys name and command or value, found 1: invalid", + }, { // For atlantis.yaml v2, this wouldn't parse, but now there should // be no error. @@ -323,6 +405,22 @@ func TestStep_ToValid(t *testing.T) { StepName: "apply", }, }, + { + description: "env step", + input: raw.Step{ + Env: EnvType{ + "env": { + "name": "test", + "command": "echo 123", + }, + }, + }, + exp: valid.Step{ + StepName: "env", + RunCommand: "echo 123", + EnvVarName: "test", + }, + }, { description: "init extra_args", input: raw.Step{ @@ -386,3 +484,4 @@ func TestStep_ToValid(t *testing.T) { } type MapType map[string]map[string][]string +type EnvType map[string]map[string]string diff --git a/server/events/yaml/valid/repo_cfg.go b/server/events/yaml/valid/repo_cfg.go index e5aaa4c136..626b7879f5 100644 --- a/server/events/yaml/valid/repo_cfg.go +++ b/server/events/yaml/valid/repo_cfg.go @@ -75,6 +75,11 @@ type Step struct { StepName string ExtraArgs []string RunCommand string + // EnvVarName is the name of the + // environment variable that should be set by this step. + EnvVarName string + // EnvVarValue is the value to set EnvVarName to. + EnvVarValue string } type Workflow struct { diff --git a/server/server.go b/server/server.go index 568189525c..2eb3db823f 100644 --- a/server/server.go +++ b/server/server.go @@ -316,6 +316,10 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { DefaultTFVersion: defaultTfVersion, TerraformBinDir: terraformClient.TerraformBinDir(), }, + EnvStepRunner: &runtime.EnvStepRunner{ + TerraformExecutor: terraformClient, + DefaultTFVersion: defaultTfVersion, + }, PullApprovedChecker: vcsClient, WorkingDir: workingDir, Webhooks: webhooksManager, From c646f20c3467fbdf18e3afbed3cd44092d6a244b Mon Sep 17 00:00:00 2001 From: Luke Kysow <1034429+lkysow@users.noreply.github.com> Date: Wed, 21 Aug 2019 14:57:58 +0200 Subject: [PATCH 2/2] Actually pass env vars between steps Also some refactoring and testing. --- server/events/mocks/mock_env_step_runner.go | 36 ++-- server/events/project_command_runner.go | 11 +- server/events/project_command_runner_test.go | 172 ++++++++++++++---- server/events/runtime/env_step_runner.go | 26 +-- server/events/runtime/env_step_runner_test.go | 22 ++- server/events/yaml/parser_validator_test.go | 56 ++++++ server/events/yaml/raw/step.go | 33 +++- server/events/yaml/raw/step_test.go | 67 +++++-- server/events/yaml/valid/repo_cfg.go | 6 +- server/server.go | 14 +- 10 files changed, 322 insertions(+), 121 deletions(-) diff --git a/server/events/mocks/mock_env_step_runner.go b/server/events/mocks/mock_env_step_runner.go index 461812701b..c05782666e 100644 --- a/server/events/mocks/mock_env_step_runner.go +++ b/server/events/mocks/mock_env_step_runner.go @@ -25,27 +25,23 @@ func NewMockEnvStepRunner(options ...pegomock.Option) *MockEnvStepRunner { func (mock *MockEnvStepRunner) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } func (mock *MockEnvStepRunner) FailHandler() pegomock.FailHandler { return mock.fail } -func (mock *MockEnvStepRunner) Run(ctx models.ProjectCommandContext, name string, cmd string, value string, path string, envs map[string]string) (string, string, error) { +func (mock *MockEnvStepRunner) Run(ctx models.ProjectCommandContext, cmd string, value string, path string, envs map[string]string) (string, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockEnvStepRunner().") } - params := []pegomock.Param{ctx, name, cmd, value, path, envs} - result := pegomock.GetGenericMockFrom(mock).Invoke("Run", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + params := []pegomock.Param{ctx, cmd, value, path, envs} + result := pegomock.GetGenericMockFrom(mock).Invoke("Run", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) var ret0 string - var ret1 string - var ret2 error + var ret1 error if len(result) != 0 { if result[0] != nil { ret0 = result[0].(string) } if result[1] != nil { - ret1 = result[1].(string) - } - if result[2] != nil { - ret2 = result[2].(error) + ret1 = result[1].(error) } } - return ret0, ret1, ret2 + return ret0, ret1 } func (mock *MockEnvStepRunner) VerifyWasCalledOnce() *VerifierMockEnvStepRunner { @@ -85,8 +81,8 @@ type VerifierMockEnvStepRunner struct { timeout time.Duration } -func (verifier *VerifierMockEnvStepRunner) Run(ctx models.ProjectCommandContext, name string, cmd string, value string, path string, envs map[string]string) *MockEnvStepRunner_Run_OngoingVerification { - params := []pegomock.Param{ctx, name, cmd, value, path, envs} +func (verifier *VerifierMockEnvStepRunner) Run(ctx models.ProjectCommandContext, cmd string, value string, path string, envs map[string]string) *MockEnvStepRunner_Run_OngoingVerification { + params := []pegomock.Param{ctx, cmd, value, path, envs} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Run", params, verifier.timeout) return &MockEnvStepRunner_Run_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } @@ -96,12 +92,12 @@ type MockEnvStepRunner_Run_OngoingVerification struct { methodInvocations []pegomock.MethodInvocation } -func (c *MockEnvStepRunner_Run_OngoingVerification) GetCapturedArguments() (models.ProjectCommandContext, string, string, string, string, map[string]string) { - ctx, name, cmd, value, path, envs := c.GetAllCapturedArguments() - return ctx[len(ctx)-1], name[len(name)-1], cmd[len(cmd)-1], value[len(value)-1], path[len(path)-1], envs[len(envs)-1] +func (c *MockEnvStepRunner_Run_OngoingVerification) GetCapturedArguments() (models.ProjectCommandContext, string, string, string, map[string]string) { + ctx, cmd, value, path, envs := c.GetAllCapturedArguments() + return ctx[len(ctx)-1], cmd[len(cmd)-1], value[len(value)-1], path[len(path)-1], envs[len(envs)-1] } -func (c *MockEnvStepRunner_Run_OngoingVerification) GetAllCapturedArguments() (_param0 []models.ProjectCommandContext, _param1 []string, _param2 []string, _param3 []string, _param4 []string, _param5 []map[string]string) { +func (c *MockEnvStepRunner_Run_OngoingVerification) GetAllCapturedArguments() (_param0 []models.ProjectCommandContext, _param1 []string, _param2 []string, _param3 []string, _param4 []map[string]string) { params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) if len(params) > 0 { _param0 = make([]models.ProjectCommandContext, len(params[0])) @@ -120,13 +116,9 @@ func (c *MockEnvStepRunner_Run_OngoingVerification) GetAllCapturedArguments() (_ for u, param := range params[3] { _param3[u] = param.(string) } - _param4 = make([]string, len(params[4])) + _param4 = make([]map[string]string, len(params[4])) for u, param := range params[4] { - _param4[u] = param.(string) - } - _param5 = make([]map[string]string, len(params[5])) - for u, param := range params[5] { - _param5[u] = param.(map[string]string) + _param4[u] = param.(map[string]string) } } return diff --git a/server/events/project_command_runner.go b/server/events/project_command_runner.go index b5240910d8..3bd813052a 100644 --- a/server/events/project_command_runner.go +++ b/server/events/project_command_runner.go @@ -67,7 +67,7 @@ type CustomStepRunner interface { // EnvStepRunner runs env steps. type EnvStepRunner interface { - Run(ctx models.ProjectCommandContext, name string, cmd string, value string, path string, envs map[string]string) (string, string, error) + Run(ctx models.ProjectCommandContext, cmd string, value 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 @@ -181,11 +181,10 @@ func (p *DefaultProjectCommandRunner) doPlan(ctx models.ProjectCommandContext) ( func (p *DefaultProjectCommandRunner) runSteps(steps []valid.Step, ctx models.ProjectCommandContext, absPath string) ([]string, error) { var outputs []string + envs := make(map[string]string) for _, step := range steps { - var envs = make(map[string]string) var out string var err error - var name string switch step.StepName { case "init": out, err = p.InitStepRunner.Run(ctx, step.ExtraArgs, absPath, envs) @@ -196,8 +195,10 @@ func (p *DefaultProjectCommandRunner) runSteps(steps []valid.Step, ctx models.Pr case "run": out, err = p.RunStepRunner.Run(ctx, step.RunCommand, absPath, envs) case "env": - name, out, err = p.EnvStepRunner.Run(ctx, step.EnvVarName, step.RunCommand, step.EnvVarValue, absPath, envs) - envs[name] = out + out, err = p.EnvStepRunner.Run(ctx, step.RunCommand, step.EnvVarValue, absPath, envs) + envs[step.EnvVarName] = out + // 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 = "" } diff --git a/server/events/project_command_runner_test.go b/server/events/project_command_runner_test.go index 024f446e0a..04be59abbf 100644 --- a/server/events/project_command_runner_test.go +++ b/server/events/project_command_runner_test.go @@ -14,6 +14,8 @@ package events_test import ( + "github.com/hashicorp/go-version" + "github.com/runatlantis/atlantis/server/events/runtime" "os" "testing" @@ -23,6 +25,7 @@ import ( "github.com/runatlantis/atlantis/server/events/mocks/matchers" "github.com/runatlantis/atlantis/server/events/models" mocks2 "github.com/runatlantis/atlantis/server/events/runtime/mocks" + tmocks "github.com/runatlantis/atlantis/server/events/terraform/mocks" "github.com/runatlantis/atlantis/server/events/yaml/valid" "github.com/runatlantis/atlantis/server/logging" . "github.com/runatlantis/atlantis/testing" @@ -34,8 +37,8 @@ func TestDefaultProjectCommandRunner_Plan(t *testing.T) { mockInit := mocks.NewMockStepRunner() mockPlan := mocks.NewMockStepRunner() mockApply := mocks.NewMockStepRunner() - mockEnv := mocks.NewMockEnvStepRunner() mockRun := mocks.NewMockCustomStepRunner() + realEnv := runtime.EnvStepRunner{} mockWorkingDir := mocks.NewMockWorkingDir() mockLocker := mocks.NewMockProjectLocker() @@ -46,14 +49,13 @@ func TestDefaultProjectCommandRunner_Plan(t *testing.T) { PlanStepRunner: mockPlan, ApplyStepRunner: mockApply, RunStepRunner: mockRun, - EnvStepRunner: mockEnv, + EnvStepRunner: &realEnv, PullApprovedChecker: nil, WorkingDir: mockWorkingDir, Webhooks: nil, WorkingDirLocker: events.NewDefaultWorkingDirLocker(), } - envs := make(map[string]string) repoDir, cleanup := TempDir(t) defer cleanup() When(mockWorkingDir.Clone( @@ -74,9 +76,17 @@ func TestDefaultProjectCommandRunner_Plan(t *testing.T) { LockKey: "lock-key", }, nil) + expEnvs := map[string]string{ + "name": "value", + } ctx := models.ProjectCommandContext{ Log: logging.NewNoopLogger(), Steps: []valid.Step{ + { + StepName: "env", + EnvVarName: "name", + EnvVarValue: "value", + }, { StepName: "run", }, @@ -89,41 +99,32 @@ func TestDefaultProjectCommandRunner_Plan(t *testing.T) { { StepName: "init", }, - { - StepName: "env", - EnvVarName: "name", - EnvVarValue: "value", - }, }, Workspace: "default", RepoRelDir: ".", } // Each step will output its step name. - When(mockInit.Run(ctx, nil, repoDir, envs)).ThenReturn("init", nil) - When(mockPlan.Run(ctx, nil, repoDir, envs)).ThenReturn("plan", nil) - When(mockApply.Run(ctx, nil, repoDir, envs)).ThenReturn("apply", nil) - When(mockRun.Run(ctx, "", repoDir, envs)).ThenReturn("run", nil) - When(mockEnv.Run(ctx, "name", "", "value", repoDir, envs)).ThenReturn("name", "value", nil) + When(mockInit.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("init", nil) + When(mockPlan.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("plan", nil) + When(mockApply.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("apply", nil) + When(mockRun.Run(ctx, "", repoDir, expEnvs)).ThenReturn("run", nil) res := runner.Plan(ctx) Assert(t, res.PlanSuccess != nil, "exp plan success") Equals(t, "https://lock-key", res.PlanSuccess.LockURL) Equals(t, "run\napply\nplan\ninit", res.PlanSuccess.TerraformOutput) - expSteps := []string{"env", "run", "apply", "plan", "init"} - var newEnv = map[string]string{"name": "value"} + expSteps := []string{"run", "apply", "plan", "init", "env"} for _, step := range expSteps { switch step { case "init": - mockInit.VerifyWasCalledOnce().Run(ctx, nil, repoDir, envs) + mockInit.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs) case "plan": - mockPlan.VerifyWasCalledOnce().Run(ctx, nil, repoDir, envs) + mockPlan.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs) case "apply": - mockApply.VerifyWasCalledOnce().Run(ctx, nil, repoDir, envs) + mockApply.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs) case "run": - mockRun.VerifyWasCalledOnce().Run(ctx, "", repoDir, envs) - case "env": - mockEnv.VerifyWasCalledOnce().Run(ctx, "name", "", "value", repoDir, newEnv) + mockRun.VerifyWasCalledOnce().Run(ctx, "", repoDir, expEnvs) } } } @@ -237,6 +238,11 @@ func TestDefaultProjectCommandRunner_Apply(t *testing.T) { { description: "workflow with custom apply stage", steps: []valid.Step{ + { + StepName: "env", + EnvVarName: "key", + EnvVarValue: "value", + }, { StepName: "run", }, @@ -249,16 +255,16 @@ func TestDefaultProjectCommandRunner_Apply(t *testing.T) { { StepName: "init", }, - { - StepName: "env", - }, }, - expSteps: []string{"run", "apply", "plan", "init", "env"}, + expSteps: []string{"env", "run", "apply", "plan", "init"}, expOut: "run\napply\nplan\ninit", }, } for _, c := range cases { + if c.description != "workflow with custom apply stage" { + continue + } t.Run(c.description, func(t *testing.T) { RegisterMockTestingT(t) mockInit := mocks.NewMockStepRunner() @@ -284,7 +290,6 @@ func TestDefaultProjectCommandRunner_Apply(t *testing.T) { Webhooks: mockSender, WorkingDirLocker: events.NewDefaultWorkingDirLocker(), } - envs := make(map[string]string) repoDir, cleanup := TempDir(t) defer cleanup() When(mockWorkingDir.GetWorkingDir( @@ -301,10 +306,14 @@ func TestDefaultProjectCommandRunner_Apply(t *testing.T) { RepoRelDir: ".", PullMergeable: c.pullMergeable, } - When(mockInit.Run(ctx, nil, repoDir, envs)).ThenReturn("init", nil) - When(mockPlan.Run(ctx, nil, repoDir, envs)).ThenReturn("plan", nil) - When(mockApply.Run(ctx, nil, repoDir, envs)).ThenReturn("apply", nil) - When(mockRun.Run(ctx, "", repoDir, envs)).ThenReturn("run", nil) + expEnvs := map[string]string{ + "key": "value", + } + When(mockInit.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("init", nil) + When(mockPlan.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("plan", nil) + When(mockApply.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("apply", nil) + When(mockRun.Run(ctx, "", repoDir, expEnvs)).ThenReturn("run", nil) + When(mockEnv.Run(ctx, "", "value", repoDir, make(map[string]string))).ThenReturn("value", nil) When(mockApproved.PullIsApproved(ctx.BaseRepo, ctx.Pull)).ThenReturn(true, nil) res := runner.Apply(ctx) @@ -316,19 +325,114 @@ func TestDefaultProjectCommandRunner_Apply(t *testing.T) { case "approved": mockApproved.VerifyWasCalledOnce().PullIsApproved(ctx.BaseRepo, ctx.Pull) case "init": - mockInit.VerifyWasCalledOnce().Run(ctx, nil, repoDir, envs) + mockInit.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs) case "plan": - mockPlan.VerifyWasCalledOnce().Run(ctx, nil, repoDir, envs) + mockPlan.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs) case "apply": - mockApply.VerifyWasCalledOnce().Run(ctx, nil, repoDir, envs) + mockApply.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs) case "run": - mockRun.VerifyWasCalledOnce().Run(ctx, "", repoDir, envs) + mockRun.VerifyWasCalledOnce().Run(ctx, "", repoDir, expEnvs) + case "env": + mockEnv.VerifyWasCalledOnce().Run(ctx, "", "value", repoDir, expEnvs) } } }) } } +// Test run and env steps. We don't use mocks for this test since we're +// not running any Terraform. +func TestDefaultProjectCommandRunner_RunEnvSteps(t *testing.T) { + RegisterMockTestingT(t) + tfClient := tmocks.NewMockClient() + tfVersion, err := version.NewVersion("0.12.0") + Ok(t, err) + run := runtime.RunStepRunner{ + TerraformExecutor: tfClient, + DefaultTFVersion: tfVersion, + } + env := runtime.EnvStepRunner{ + RunStepRunner: &run, + } + mockWorkingDir := mocks.NewMockWorkingDir() + mockLocker := mocks.NewMockProjectLocker() + + runner := events.DefaultProjectCommandRunner{ + Locker: mockLocker, + LockURLGenerator: mockURLGenerator{}, + RunStepRunner: &run, + EnvStepRunner: &env, + PullApprovedChecker: nil, + WorkingDir: mockWorkingDir, + Webhooks: nil, + WorkingDirLocker: events.NewDefaultWorkingDirLocker(), + } + + repoDir, cleanup := TempDir(t) + defer cleanup() + When(mockWorkingDir.Clone( + matchers.AnyPtrToLoggingSimpleLogger(), + matchers.AnyModelsRepo(), + matchers.AnyModelsRepo(), + matchers.AnyModelsPullRequest(), + AnyString(), + )).ThenReturn(repoDir, nil) + When(mockLocker.TryLock( + matchers.AnyPtrToLoggingSimpleLogger(), + matchers.AnyModelsPullRequest(), + matchers.AnyModelsUser(), + AnyString(), + matchers.AnyModelsProject(), + )).ThenReturn(&events.TryLockResponse{ + LockAcquired: true, + LockKey: "lock-key", + }, nil) + + ctx := models.ProjectCommandContext{ + Log: logging.NewNoopLogger(), + Steps: []valid.Step{ + { + StepName: "run", + RunCommand: "echo var=$var", + }, + { + StepName: "env", + EnvVarName: "var", + EnvVarValue: "value", + }, + { + StepName: "run", + RunCommand: "echo var=$var", + }, + { + StepName: "env", + EnvVarName: "dynamic_var", + RunCommand: "echo dynamic_value", + }, + { + StepName: "run", + RunCommand: "echo dynamic_var=$dynamic_var", + }, + // Test overriding the variable + { + StepName: "env", + EnvVarName: "dynamic_var", + EnvVarValue: "overridden", + }, + { + StepName: "run", + RunCommand: "echo dynamic_var=$dynamic_var", + }, + }, + Workspace: "default", + RepoRelDir: ".", + } + res := runner.Plan(ctx) + Assert(t, res.PlanSuccess != nil, "exp plan success") + Equals(t, "https://lock-key", res.PlanSuccess.LockURL) + Equals(t, "var=\n\nvar=value\n\ndynamic_var=dynamic_value\n\ndynamic_var=overridden\n", res.PlanSuccess.TerraformOutput) +} + type mockURLGenerator struct{} func (m mockURLGenerator) GenerateLockURL(lockID string) string { diff --git a/server/events/runtime/env_step_runner.go b/server/events/runtime/env_step_runner.go index 74afe19f48..28a8d481f2 100644 --- a/server/events/runtime/env_step_runner.go +++ b/server/events/runtime/env_step_runner.go @@ -1,26 +1,26 @@ package runtime import ( - "github.com/hashicorp/go-version" "github.com/runatlantis/atlantis/server/events/models" + "strings" ) // EnvStepRunner set environment variables. type EnvStepRunner struct { - TerraformExecutor TerraformExec - DefaultTFVersion *version.Version + RunStepRunner *RunStepRunner } -func (r *EnvStepRunner) Run(ctx models.ProjectCommandContext, name string, command string, value string, path string, envs map[string]string) (string, string, error) { +// Run runs the env step command. +// value is the value for the environment variable. If set this is returned as +// the value. Otherwise command is run and its output is the value returned. +func (r *EnvStepRunner) Run(ctx models.ProjectCommandContext, command string, value string, path string, envs map[string]string) (string, error) { if value != "" { - return name, value, nil + return value, nil } - - runStepRunner := RunStepRunner{ - TerraformExecutor: r.TerraformExecutor, - DefaultTFVersion: r.DefaultTFVersion, - } - res, err := runStepRunner.Run(ctx, command, path, envs) - - return name, res, err + res, err := r.RunStepRunner.Run(ctx, command, path, envs) + // Trim newline from res to support running `echo env_value` which has + // a newline. We don't recommend users run echo -n env_value to remove the + // newline because -n doesn't work in the sh shell which is what we use + // to run commands. + return strings.TrimSuffix(res, "\n"), err } diff --git a/server/events/runtime/env_step_runner_test.go b/server/events/runtime/env_step_runner_test.go index fe281e9667..62b6f255fc 100644 --- a/server/events/runtime/env_step_runner_test.go +++ b/server/events/runtime/env_step_runner_test.go @@ -9,6 +9,7 @@ import ( "github.com/runatlantis/atlantis/server/events/terraform/mocks" "github.com/runatlantis/atlantis/server/logging" + . "github.com/petergtz/pegomock" . "github.com/runatlantis/atlantis/testing" ) @@ -22,7 +23,7 @@ func TestEnvStepRunner_Run(t *testing.T) { }{ { Command: "echo 123", - ExpValue: "123\n", + ExpValue: "123", }, { Value: "test", @@ -34,13 +35,16 @@ func TestEnvStepRunner_Run(t *testing.T) { ExpValue: "test", }, } - terraform := mocks.NewMockClient() - projVersion, err := version.NewVersion("v0.11.0") + RegisterMockTestingT(t) + tfClient := mocks.NewMockClient() + tfVersion, err := version.NewVersion("0.12.0") Ok(t, err) - defaultVersion, _ := version.NewVersion("0.8") - r := runtime.EnvStepRunner{ - TerraformExecutor: terraform, - DefaultTFVersion: defaultVersion, + runStepRunner := runtime.RunStepRunner{ + TerraformExecutor: tfClient, + DefaultTFVersion: tfVersion, + } + envRunner := runtime.EnvStepRunner{ + RunStepRunner: &runStepRunner, } for _, c := range cases { t.Run(c.Command, func(t *testing.T) { @@ -67,10 +71,10 @@ func TestEnvStepRunner_Run(t *testing.T) { Log: logging.NewNoopLogger(), Workspace: "myworkspace", RepoRelDir: "mydir", - TerraformVersion: projVersion, + TerraformVersion: tfVersion, ProjectName: c.ProjectName, } - _, value, err := r.Run(ctx, "var", c.Command, c.Value, tmpDir, map[string]string(nil)) + value, err := envRunner.Run(ctx, c.Command, c.Value, tmpDir, map[string]string(nil)) if c.ExpErr != "" { ErrContains(t, c.ExpErr, err) return diff --git a/server/events/yaml/parser_validator_test.go b/server/events/yaml/parser_validator_test.go index 9d3c1ace91..cdd4149402 100644 --- a/server/events/yaml/parser_validator_test.go +++ b/server/events/yaml/parser_validator_test.go @@ -745,6 +745,62 @@ workflows: }, }, }, + { + description: "env steps", + input: ` +version: 3 +projects: +- dir: "." +workflows: + default: + plan: + steps: + - env: + name: env_name + value: env_value + apply: + steps: + - env: + name: env_name + command: command and args +`, + exp: valid.RepoCfg{ + Version: 3, + Projects: []valid.Project{ + { + Dir: ".", + Workspace: "default", + Autoplan: valid.Autoplan{ + WhenModified: []string{"**/*.tf*"}, + Enabled: true, + }, + }, + }, + Workflows: map[string]valid.Workflow{ + "default": { + Name: "default", + Plan: valid.Stage{ + Steps: []valid.Step{ + { + StepName: "env", + EnvVarName: "env_name", + EnvVarValue: "env_value", + }, + }, + }, + Apply: valid.Stage{ + Steps: []valid.Step{ + { + StepName: "env", + EnvVarName: "env_name", + RunCommand: "command and args", + }, + }, + }, + }, + }, + }, + }, } tmpDir, cleanup := TempDir(t) diff --git a/server/events/yaml/raw/step.go b/server/events/yaml/raw/step.go index b4026ae7b9..09b5f0f015 100644 --- a/server/events/yaml/raw/step.go +++ b/server/events/yaml/raw/step.go @@ -27,10 +27,11 @@ const ( // 1. A single string for a built-in command: // - init // - plan -// 2. A map for a built-in env with name and command +// 2. A map for an env step with name and command or value // - env: // name: test // command: echo 312 +// value: value // 3. A map for a built-in command and extra_args: // - plan: // extra_args: [-var-file=staging.tfvars] @@ -42,7 +43,7 @@ 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 - // Map will be set in case #2 above. + // Env will be set in case #2 above. Env map[string]map[string]string // Map will be set in case #3 above. Map map[string]map[string][]string @@ -89,6 +90,8 @@ func (s Step) Validate() error { for k := range args { argKeys = append(argKeys, k) } + // Sort so tests can be deterministic. + sort.Strings(argKeys) // args should contain a single 'extra_args' key. if len(argKeys) > 1 { @@ -125,15 +128,26 @@ func (s Step) Validate() error { for k := range args { argKeys = append(argKeys, k) } - if len(argKeys) != 2 { - return fmt.Errorf("built-in steps only support two keys %s and %s or %s, found %d: %s", - NameArgKey, CommandArgKey, ValueArgKey, len(argKeys), strings.Join(argKeys, ",")) - } - for k := range args { + // Sort so tests can be deterministic. + sort.Strings(argKeys) + + foundNameKey := false + for _, k := range argKeys { if k != NameArgKey && k != CommandArgKey && k != ValueArgKey { - return fmt.Errorf("built-in steps only support two keys %s and %s or %s, found %q in step %s", NameArgKey, CommandArgKey, ValueArgKey, k, stepName) + return fmt.Errorf("env steps only support keys %q, %q and %q, found key %q", NameArgKey, ValueArgKey, CommandArgKey, k) + } + if k == NameArgKey { + foundNameKey = true } } + if !foundNameKey { + return fmt.Errorf("env steps must have a %q key set", NameArgKey) + } + // If we have 3 keys at this point then they've set both command and value. + if len(argKeys) != 2 { + return fmt.Errorf("env steps only support one of the %q or %q keys, found both", + ValueArgKey, CommandArgKey) + } } return nil } @@ -257,12 +271,11 @@ func (s *Step) unmarshalGeneric(unmarshal func(interface{}) error) error { return nil } - // This represents a step with extra_args, ex: + // This represents an env step, ex: // env: // name: k // value: hi //optional // command: exec - // We validate if the key env var envStep map[string]map[string]string err = unmarshal(&envStep) if err == nil { diff --git a/server/events/yaml/raw/step_test.go b/server/events/yaml/raw/step_test.go index 001136c184..c99a49596c 100644 --- a/server/events/yaml/raw/step_test.go +++ b/server/events/yaml/raw/step_test.go @@ -73,8 +73,24 @@ key2: }, }, }, + // Env steps { - description: "env step", + description: "env step value", + input: ` +env: + value: direct_value + name: test`, + exp: raw.Step{ + Env: EnvType{ + "env": { + "value": "direct_value", + "name": "test", + }, + }, + }, + }, + { + description: "env step command", input: ` env: command: echo 123 @@ -314,20 +330,31 @@ func TestStep_Validate(t *testing.T) { }, expErr: "built-in steps only support a single extra_args key, found \"invalid\" in step init", }, - // { - // description: "non extra_arg key", - // input: raw.Step{ - // Map: MapType{ - // "init": { - // "invalid": nil, - // "zzzzzzz": nil, - // }, - // }, - // }, - // expErr: "built-in steps only support a single extra_args key, found 2: invalid,zzzzzzz", - // }, - { - description: "incorrect keys in env", + { + description: "non extra_arg key", + input: raw.Step{ + Map: MapType{ + "init": { + "invalid": nil, + "zzzzzzz": nil, + }, + }, + }, + expErr: "built-in steps only support a single extra_args key, found 2: invalid,zzzzzzz", + }, + { + description: "env step with no name key set", + input: raw.Step{ + Env: EnvType{ + "env": { + "value": "value", + }, + }, + }, + expErr: "env steps must have a \"name\" key set", + }, + { + description: "env step with invalid key", input: raw.Step{ Env: EnvType{ "env": { @@ -336,18 +363,20 @@ func TestStep_Validate(t *testing.T) { }, }, }, - expErr: "built-in steps only support two keys name and command or value, found \"abc\" in step env", + expErr: "env steps only support keys \"name\", \"value\" and \"command\", found key \"abc\"", }, { - description: "non two keys in env", + description: "env step with both command and value set", input: raw.Step{ Env: EnvType{ "env": { - "invalid": "", + "name": "name", + "command": "command", + "value": "value", }, }, }, - expErr: "built-in steps only support two keys name and command or value, found 1: invalid", + expErr: "env steps only support one of the \"value\" or \"command\" keys, found both", }, { // For atlantis.yaml v2, this wouldn't parse, but now there should diff --git a/server/events/yaml/valid/repo_cfg.go b/server/events/yaml/valid/repo_cfg.go index 626b7879f5..8fa20aebaf 100644 --- a/server/events/yaml/valid/repo_cfg.go +++ b/server/events/yaml/valid/repo_cfg.go @@ -72,8 +72,10 @@ type Stage struct { } type Step struct { - StepName string - ExtraArgs []string + StepName string + ExtraArgs []string + // RunCommand is either a custom run step or the command to run + // during an env step to populate the environment variable dynamically. RunCommand string // EnvVarName is the name of the // environment variable that should be set by this step. diff --git a/server/server.go b/server/server.go index 2eb3db823f..1274afb24c 100644 --- a/server/server.go +++ b/server/server.go @@ -272,6 +272,11 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { } defaultTfVersion := terraformClient.DefaultVersion() pendingPlanFinder := &events.DefaultPendingPlanFinder{} + runStepRunner := &runtime.RunStepRunner{ + TerraformExecutor: terraformClient, + DefaultTFVersion: defaultTfVersion, + TerraformBinDir: terraformClient.TerraformBinDir(), + } commandRunner := &events.DefaultCommandRunner{ VCSClient: vcsClient, GithubPullGetter: githubClient, @@ -311,14 +316,9 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { CommitStatusUpdater: commitStatusUpdater, AsyncTFExec: terraformClient, }, - RunStepRunner: &runtime.RunStepRunner{ - TerraformExecutor: terraformClient, - DefaultTFVersion: defaultTfVersion, - TerraformBinDir: terraformClient.TerraformBinDir(), - }, + RunStepRunner: runStepRunner, EnvStepRunner: &runtime.EnvStepRunner{ - TerraformExecutor: terraformClient, - DefaultTFVersion: defaultTfVersion, + RunStepRunner: runStepRunner, }, PullApprovedChecker: vcsClient, WorkingDir: workingDir,