From 113728f7e9145340e321542ea6e885e50d3008d4 Mon Sep 17 00:00:00 2001 From: Gerald Barker Date: Sat, 26 Jun 2021 18:32:34 +0100 Subject: [PATCH 1/2] Add post-workflow hooks --- go.sum | 2 + runatlantis.io/.vuepress/config.js | 1 + runatlantis.io/docs/post-workflow-hooks.md | 60 +++++ .../docs/server-side-repo-config.md | 19 ++ .../events/events_controller_e2e_test.go | 48 ++-- .../test-repos/server-side-cfg/repos.yaml | 2 + server/events/command_runner.go | 23 +- server/events/command_runner_test.go | 30 ++- ...ock_post_workflows_hooks_command_runner.go | 107 +++++++++ server/events/models/models.go | 4 +- .../post_workflow_hooks_command_runner.go | 94 ++++++++ ...post_workflow_hooks_command_runner_test.go | 217 ++++++++++++++++++ .../pre_workflow_hooks_command_runner.go | 8 +- .../pre_workflow_hooks_command_runner_test.go | 72 +++--- .../models_preworkflowhookcommandcontext.go | 33 --- .../models_workflowhookcommandcontext.go | 33 +++ .../mocks/mock_post_workflows_hook_runner.go | 117 ++++++++++ .../mocks/mock_pre_workflows_hook_runner.go | 12 +- .../runtime/post_workflow_hook_runner.go | 52 +++++ .../runtime/post_workflow_hook_runner_test.go | 111 +++++++++ .../runtime/pre_workflow_hook_runner.go | 4 +- .../runtime/pre_workflow_hook_runner_test.go | 2 +- server/events/yaml/parser_validator_test.go | 22 +- server/events/yaml/raw/global_cfg.go | 29 ++- ...{pre_workflow_step.go => workflow_step.go} | 24 +- ...low_step_test.go => workflow_step_test.go} | 30 +-- server/events/yaml/valid/global_cfg.go | 25 +- server/server.go | 112 +++++---- 28 files changed, 1076 insertions(+), 217 deletions(-) create mode 100644 runatlantis.io/docs/post-workflow-hooks.md create mode 100644 server/events/mocks/mock_post_workflows_hooks_command_runner.go create mode 100644 server/events/post_workflow_hooks_command_runner.go create mode 100644 server/events/post_workflow_hooks_command_runner_test.go delete mode 100644 server/events/runtime/mocks/matchers/models_preworkflowhookcommandcontext.go create mode 100644 server/events/runtime/mocks/matchers/models_workflowhookcommandcontext.go create mode 100644 server/events/runtime/mocks/mock_post_workflows_hook_runner.go create mode 100644 server/events/runtime/post_workflow_hook_runner.go create mode 100644 server/events/runtime/post_workflow_hook_runner_test.go rename server/events/yaml/raw/{pre_workflow_step.go => workflow_step.go} (76%) rename server/events/yaml/raw/{pre_workflow_step_test.go => workflow_step_test.go} (82%) diff --git a/go.sum b/go.sum index 6113e4e70a..120fa38886 100644 --- a/go.sum +++ b/go.sum @@ -660,6 +660,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2 h1:kRBLX7v7Af8W7Gdbbc908OJcdgtK8bOz9Uaj8/F1ACA= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4 h1:cVngSRcfgyZCzys3KYOpCFa+4dqX/Oub9tAq00ttGVs= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/runatlantis.io/.vuepress/config.js b/runatlantis.io/.vuepress/config.js index ecb708105b..0fed27f067 100644 --- a/runatlantis.io/.vuepress/config.js +++ b/runatlantis.io/.vuepress/config.js @@ -65,6 +65,7 @@ module.exports = { 'server-configuration', 'server-side-repo-config', 'pre-workflow-hooks', + 'post-workflow-hooks', 'policy-checking', 'custom-workflows', 'repo-level-atlantis-yaml', diff --git a/runatlantis.io/docs/post-workflow-hooks.md b/runatlantis.io/docs/post-workflow-hooks.md new file mode 100644 index 0000000000..6258e89491 --- /dev/null +++ b/runatlantis.io/docs/post-workflow-hooks.md @@ -0,0 +1,60 @@ +# Post Workflow Hooks + +Post workflow hooks can be defined to run scripts after default or custom +workflows are executed. Pre workflow hooks differ from [custom +workflows](custom-workflows.html#custom-run-command) in that they are ran +outside of Atlantis commands. Which means they do not surface their output +back to the PR as a comment. + +[[toc]] + +## Usage + +Post workflow hooks can only be specified in the Server-Side Repo Config under +`repos` key. + +## Use Cases + +### Deleting files that hold credenatils + +If you generate files that hold credentials needed by your Terraform as part of either a +[pre workflow hooks](pre-workflow-hooks.html) or as part of a [custom workflow](custom-workflows.html) +you may want to make sure it gets deleted even if your main workflow has failed + +```yaml +repos: + - id: /.*/ + post_workflow_hooks: + - run: rm $DIR/asupersecretfile +``` + +### Reference + +#### Custom `run` Command + +This is very similar to [custom workflow run +command](custom-workflows.html#custom-run-command). + +```yaml +- run: custom-command +``` + +| Key | Type | Default | Required | Description | +| --- | ------ | ------- | -------- | -------------------- | +| run | string | none | no | Run a custom command | + +::: tip Notes + +* `run` commands are executed with the following environment variables: + * `BASE_REPO_NAME` - Name of the repository that the pull request will be merged into, ex. `atlantis`. + * `BASE_REPO_OWNER` - Owner of the repository that the pull request will be merged into, ex. `runatlantis`. + * `HEAD_REPO_NAME` - Name of the repository that is getting merged into the base repository, ex. `atlantis`. + * `HEAD_REPO_OWNER` - Owner of the repository that is getting merged into the base repository, ex. `acme-corp`. + * `HEAD_BRANCH_NAME` - Name of the head branch of the pull request (the branch that is getting merged into the base) + * `HEAD_COMMIT` - The sha256 that points to the head of the branch that is being pull requested into the base. If the pull request is from Bitbucket Cloud the string will only be 12 characters long because Bitbucket Cloud truncates its commit IDs. + * `BASE_BRANCH_NAME` - Name of the base branch of the pull request (the branch that the pull request is getting merged into) + * `PULL_NUM` - Pull request number or ID, ex. `2`. + * `PULL_AUTHOR` - Username of the pull request author, ex. `acme-user`. + * `DIR` - The absolute path to the root of the cloned repository. + * `USER_NAME` - Username of the VCS user running command, ex. `acme-user`. During an autoplan, the user will be the Atlantis API user, ex. `atlantis`. +::: diff --git a/runatlantis.io/docs/server-side-repo-config.md b/runatlantis.io/docs/server-side-repo-config.md index 0e2b3d4d44..fa0f95aaa8 100644 --- a/runatlantis.io/docs/server-side-repo-config.md +++ b/runatlantis.io/docs/server-side-repo-config.md @@ -60,6 +60,10 @@ repos: # pre_workflow_hooks defines arbitrary list of scripts to execute before workflow execution. pre_workflow_hooks: - run: my-pre-workflow-hook-command arg1 + + # post_workflow_hooks defines arbitrary list of scripts to execute after workflow execution. + post_workflow_hooks: + - run: my-post-workflow-hook-command arg1 # id can also be an exact match. - id: github.com/myorg/specific-repo @@ -180,6 +184,21 @@ repos: See [Pre Workflow Hooks](pre-workflow-hooks.html) for more details on writing pre workflow hooks. +### Running Scripts After Atlantis Workflows +If you want to run scripts that would execute after Atlantis runs default or +custom workflows, you can create a `post-workflow-hooks`: + +```yaml +repos: + - id: /.*/ + post_workflow_hooks: + - run: my custom command + - run: | + my bash script inline +``` +See [Post Workflow Hooks](post-workflow-hooks.html) for more details on writing +post workflow hooks. + ### Change The Default Atlantis Workflow If you want to change the default commands that Atlantis runs during `plan` and `apply` phases, you can create a new `workflow`. diff --git a/server/controllers/events/events_controller_e2e_test.go b/server/controllers/events/events_controller_e2e_test.go index eabfda2c38..5143662e50 100644 --- a/server/controllers/events/events_controller_e2e_test.go +++ b/server/controllers/events/events_controller_e2e_test.go @@ -47,6 +47,8 @@ type NoopTFDownloader struct{} var mockPreWorkflowHookRunner *runtimemocks.MockPreWorkflowHookRunner +var mockPostWorkflowHookRunner *runtimemocks.MockPostWorkflowHookRunner + func (m *NoopTFDownloader) GetFile(dst, src string, opts ...getter.ClientOption) error { return nil } @@ -424,7 +426,10 @@ func TestGitHubWorkflow(t *testing.T) { ResponseContains(t, w, 200, "Pull request cleaned successfully") // Let's verify the pre-workflow hook was called for each comment including the pull request opened event - mockPreWorkflowHookRunner.VerifyWasCalled(Times(len(c.Comments)+1)).Run(runtimematchers.AnyModelsPreWorkflowHookCommandContext(), EqString("some dummy command"), AnyString()) + mockPreWorkflowHookRunner.VerifyWasCalled(Times(len(c.Comments)+1)).Run(runtimematchers.AnyModelsWorkflowHookCommandContext(), EqString("some dummy command"), AnyString()) + + // Let's verify the post-workflow hook was called for each comment including the pull request opened event + mockPostWorkflowHookRunner.VerifyWasCalled(Times(len(c.Comments)+1)).Run(runtimematchers.AnyModelsWorkflowHookCommandContext(), EqString("some post dummy command"), AnyString()) // Now we're ready to verify Atlantis made all the comments back (or // replies) that we expect. We expect each plan to have 1 comment, @@ -697,12 +702,18 @@ func setupE2E(t *testing.T, repoDir string) (events_controllers.VCSEventsControl AllowRepoCfg: true, MergeableReq: false, ApprovedReq: false, - PreWorkflowHooks: []*valid.PreWorkflowHook{ + PreWorkflowHooks: []*valid.WorkflowHook{ { StepName: "global_hook", RunCommand: "some dummy command", }, }, + PostWorkflowHooks: []*valid.WorkflowHook{ + { + StepName: "global_hook", + RunCommand: "some post dummy command", + }, + }, PolicyCheckEnabled: userConfig.EnablePolicyChecksFlag, } globalCfg := valid.NewGlobalCfgFromArgs(globalCfgArgs) @@ -724,6 +735,16 @@ func setupE2E(t *testing.T, repoDir string) (events_controllers.VCSEventsControl WorkingDir: workingDir, PreWorkflowHookRunner: mockPreWorkflowHookRunner, } + + mockPostWorkflowHookRunner = runtimemocks.NewMockPostWorkflowHookRunner() + postWorkflowHooksCommandRunner := &events.DefaultPostWorkflowHooksCommandRunner{ + VCSClient: e2eVCSClient, + GlobalCfg: globalCfg, + WorkingDirLocker: locker, + WorkingDir: workingDir, + PostWorkflowHookRunner: mockPostWorkflowHookRunner, + } + projectCommandBuilder := events.NewProjectCommandBuilder( userConfig.EnablePolicyChecksFlag, parser, @@ -866,17 +887,18 @@ func setupE2E(t *testing.T, repoDir string) (events_controllers.VCSEventsControl } commandRunner := &events.DefaultCommandRunner{ - EventParser: eventParser, - VCSClient: e2eVCSClient, - GithubPullGetter: e2eGithubGetter, - GitlabMergeRequestGetter: e2eGitlabGetter, - Logger: logger, - AllowForkPRs: allowForkPRs, - AllowForkPRsFlag: "allow-fork-prs", - CommentCommandRunnerByCmd: commentCommandRunnerByCmd, - Drainer: drainer, - PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner, - PullStatusFetcher: boltdb, + EventParser: eventParser, + VCSClient: e2eVCSClient, + GithubPullGetter: e2eGithubGetter, + GitlabMergeRequestGetter: e2eGitlabGetter, + Logger: logger, + AllowForkPRs: allowForkPRs, + AllowForkPRsFlag: "allow-fork-prs", + CommentCommandRunnerByCmd: commentCommandRunnerByCmd, + Drainer: drainer, + PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner, + PostWorkflowHooksCommandRunner: postWorkflowHooksCommandRunner, + PullStatusFetcher: boltdb, } repoAllowlistChecker, err := events.NewRepoAllowlistChecker("*") diff --git a/server/controllers/events/testfixtures/test-repos/server-side-cfg/repos.yaml b/server/controllers/events/testfixtures/test-repos/server-side-cfg/repos.yaml index 5c550ba60e..01ee03dfea 100644 --- a/server/controllers/events/testfixtures/test-repos/server-side-cfg/repos.yaml +++ b/server/controllers/events/testfixtures/test-repos/server-side-cfg/repos.yaml @@ -3,6 +3,8 @@ repos: pre_workflow_hooks: - run: echo "hello" workflow: custom + post_workflow_hooks: + - run: echo "hello" allowed_overrides: [workflow] workflows: custom: diff --git a/server/events/command_runner.go b/server/events/command_runner.go index 24c717a01d..12be8c48f4 100644 --- a/server/events/command_runner.go +++ b/server/events/command_runner.go @@ -109,11 +109,12 @@ type DefaultCommandRunner struct { // SilenceForkPRErrorsFlag is the name of the flag that controls fork PR's. We use // this in our error message back to the user on a forked PR so they know // how to disable error comment - SilenceForkPRErrorsFlag string - CommentCommandRunnerByCmd map[models.CommandName]CommentCommandRunner - Drainer *Drainer - PreWorkflowHooksCommandRunner PreWorkflowHooksCommandRunner - PullStatusFetcher PullStatusFetcher + SilenceForkPRErrorsFlag string + CommentCommandRunnerByCmd map[models.CommandName]CommentCommandRunner + Drainer *Drainer + PreWorkflowHooksCommandRunner PreWorkflowHooksCommandRunner + PostWorkflowHooksCommandRunner PostWorkflowHooksCommandRunner + PullStatusFetcher PullStatusFetcher } // RunAutoplanCommand runs plan and policy_checks when a pull request is opened or updated. @@ -158,6 +159,12 @@ func (c *DefaultCommandRunner) RunAutoplanCommand(baseRepo models.Repo, headRepo autoPlanRunner := buildCommentCommandRunner(c, models.PlanCommand) autoPlanRunner.Run(ctx, nil) + + err = c.PostWorkflowHooksCommandRunner.RunPostHooks(ctx) + + if err != nil { + ctx.Log.Err("Error running post-workflow hooks %s.", err) + } } // RunCommentCommand executes the command. @@ -210,6 +217,12 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead cmdRunner := buildCommentCommandRunner(c, cmd.CommandName()) cmdRunner.Run(ctx, cmd) + + err = c.PostWorkflowHooksCommandRunner.RunPostHooks(ctx) + + if err != nil { + ctx.Log.Err("Error running post-workflow hooks %s.", err) + } } func (c *DefaultCommandRunner) getGithubData(baseRepo models.Repo, pullNum int) (models.PullRequest, models.Repo, error) { diff --git a/server/events/command_runner_test.go b/server/events/command_runner_test.go index 1254e96d03..8efd1ceea1 100644 --- a/server/events/command_runner_test.go +++ b/server/events/command_runner_test.go @@ -62,6 +62,7 @@ var applyLockChecker *lockingmocks.MockApplyLockChecker var applyCommandRunner *events.ApplyCommandRunner var unlockCommandRunner *events.UnlockCommandRunner var preWorkflowHooksCommandRunner events.PreWorkflowHooksCommandRunner +var postWorkflowHooksCommandRunner events.PostWorkflowHooksCommandRunner func setup(t *testing.T) *vcsmocks.MockClient { RegisterMockTestingT(t) @@ -173,19 +174,24 @@ func setup(t *testing.T) *vcsmocks.MockClient { When(preWorkflowHooksCommandRunner.RunPreHooks(matchers.AnyPtrToEventsCommandContext())).ThenReturn(nil) + postWorkflowHooksCommandRunner = mocks.NewMockPostWorkflowHooksCommandRunner() + + When(postWorkflowHooksCommandRunner.RunPostHooks(matchers.AnyPtrToEventsCommandContext())).ThenReturn(nil) + ch = events.DefaultCommandRunner{ - VCSClient: vcsClient, - CommentCommandRunnerByCmd: commentCommandRunnerByCmd, - EventParser: eventParsing, - GithubPullGetter: githubGetter, - GitlabMergeRequestGetter: gitlabGetter, - AzureDevopsPullGetter: azuredevopsGetter, - Logger: logger, - AllowForkPRs: false, - AllowForkPRsFlag: "allow-fork-prs-flag", - Drainer: drainer, - PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner, - PullStatusFetcher: defaultBoltDB, + VCSClient: vcsClient, + CommentCommandRunnerByCmd: commentCommandRunnerByCmd, + EventParser: eventParsing, + GithubPullGetter: githubGetter, + GitlabMergeRequestGetter: gitlabGetter, + AzureDevopsPullGetter: azuredevopsGetter, + Logger: logger, + AllowForkPRs: false, + AllowForkPRsFlag: "allow-fork-prs-flag", + Drainer: drainer, + PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner, + PostWorkflowHooksCommandRunner: postWorkflowHooksCommandRunner, + PullStatusFetcher: defaultBoltDB, } return vcsClient } diff --git a/server/events/mocks/mock_post_workflows_hooks_command_runner.go b/server/events/mocks/mock_post_workflows_hooks_command_runner.go new file mode 100644 index 0000000000..fd42ae3ce6 --- /dev/null +++ b/server/events/mocks/mock_post_workflows_hooks_command_runner.go @@ -0,0 +1,107 @@ +// Code generated by pegomock. DO NOT EDIT. +// Source: github.com/runatlantis/atlantis/server/events (interfaces: PostWorkflowHooksCommandRunner) + +package mocks + +import ( + pegomock "github.com/petergtz/pegomock" + events "github.com/runatlantis/atlantis/server/events" + "reflect" + "time" +) + +type MockPostWorkflowHooksCommandRunner struct { + fail func(message string, callerSkip ...int) +} + +func NewMockPostWorkflowHooksCommandRunner(options ...pegomock.Option) *MockPostWorkflowHooksCommandRunner { + mock := &MockPostWorkflowHooksCommandRunner{} + for _, option := range options { + option.Apply(mock) + } + return mock +} + +func (mock *MockPostWorkflowHooksCommandRunner) SetFailHandler(fh pegomock.FailHandler) { + mock.fail = fh +} +func (mock *MockPostWorkflowHooksCommandRunner) FailHandler() pegomock.FailHandler { return mock.fail } + +func (mock *MockPostWorkflowHooksCommandRunner) RunPostHooks(ctx *events.CommandContext) error { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockPostWorkflowHooksCommandRunner().") + } + params := []pegomock.Param{ctx} + result := pegomock.GetGenericMockFrom(mock).Invoke("RunPostHooks", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(error) + } + } + return ret0 +} + +func (mock *MockPostWorkflowHooksCommandRunner) VerifyWasCalledOnce() *VerifierMockPostWorkflowHooksCommandRunner { + return &VerifierMockPostWorkflowHooksCommandRunner{ + mock: mock, + invocationCountMatcher: pegomock.Times(1), + } +} + +func (mock *MockPostWorkflowHooksCommandRunner) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockPostWorkflowHooksCommandRunner { + return &VerifierMockPostWorkflowHooksCommandRunner{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + } +} + +func (mock *MockPostWorkflowHooksCommandRunner) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockPostWorkflowHooksCommandRunner { + return &VerifierMockPostWorkflowHooksCommandRunner{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + inOrderContext: inOrderContext, + } +} + +func (mock *MockPostWorkflowHooksCommandRunner) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockPostWorkflowHooksCommandRunner { + return &VerifierMockPostWorkflowHooksCommandRunner{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + timeout: timeout, + } +} + +type VerifierMockPostWorkflowHooksCommandRunner struct { + mock *MockPostWorkflowHooksCommandRunner + invocationCountMatcher pegomock.InvocationCountMatcher + inOrderContext *pegomock.InOrderContext + timeout time.Duration +} + +func (verifier *VerifierMockPostWorkflowHooksCommandRunner) RunPostHooks(ctx *events.CommandContext) *MockPostWorkflowHooksCommandRunner_RunPostHooks_OngoingVerification { + params := []pegomock.Param{ctx} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "RunPostHooks", params, verifier.timeout) + return &MockPostWorkflowHooksCommandRunner_RunPostHooks_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockPostWorkflowHooksCommandRunner_RunPostHooks_OngoingVerification struct { + mock *MockPostWorkflowHooksCommandRunner + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockPostWorkflowHooksCommandRunner_RunPostHooks_OngoingVerification) GetCapturedArguments() *events.CommandContext { + ctx := c.GetAllCapturedArguments() + return ctx[len(ctx)-1] +} + +func (c *MockPostWorkflowHooksCommandRunner_RunPostHooks_OngoingVerification) GetAllCapturedArguments() (_param0 []*events.CommandContext) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]*events.CommandContext, len(c.methodInvocations)) + for u, param := range params[0] { + _param0[u] = param.(*events.CommandContext) + } + } + return +} diff --git a/server/events/models/models.go b/server/events/models/models.go index 459b67d3e7..a8da6e3b5b 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -653,9 +653,9 @@ func (c CommandName) String() string { return "" } -// PreWorkflowHookCommandContext defines the context for a pre_worklfow_hooks that will +// WorkflowHookCommandContext defines the context for a pre and post worklfow_hooks that will // be executed before workflows. -type PreWorkflowHookCommandContext struct { +type WorkflowHookCommandContext struct { // BaseRepo is the repository that the pull request will be merged into. BaseRepo Repo // HeadRepo is the repository that is getting merged into the BaseRepo. diff --git a/server/events/post_workflow_hooks_command_runner.go b/server/events/post_workflow_hooks_command_runner.go new file mode 100644 index 0000000000..1307defe3f --- /dev/null +++ b/server/events/post_workflow_hooks_command_runner.go @@ -0,0 +1,94 @@ +package events + +import ( + "github.com/runatlantis/atlantis/server/events/models" + "github.com/runatlantis/atlantis/server/events/runtime" + "github.com/runatlantis/atlantis/server/events/vcs" + "github.com/runatlantis/atlantis/server/events/yaml/valid" +) + +//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_post_workflows_hooks_command_runner.go PostWorkflowHooksCommandRunner + +type PostWorkflowHooksCommandRunner interface { + RunPostHooks(ctx *CommandContext) error +} + +// DefaultPostWorkflowHooksCommandRunner is the first step when processing a workflow hook commands. +type DefaultPostWorkflowHooksCommandRunner struct { + VCSClient vcs.Client + WorkingDirLocker WorkingDirLocker + WorkingDir WorkingDir + GlobalCfg valid.GlobalCfg + PostWorkflowHookRunner runtime.PostWorkflowHookRunner +} + +// RunPostHooks runs post_workflow_hooks after a plan/apply has completed +func (w *DefaultPostWorkflowHooksCommandRunner) RunPostHooks( + ctx *CommandContext, +) error { + pull := ctx.Pull + baseRepo := pull.BaseRepo + headRepo := ctx.HeadRepo + user := ctx.User + log := ctx.Log + + postWorkflowHooks := make([]*valid.WorkflowHook, 0) + for _, repo := range w.GlobalCfg.Repos { + if repo.IDMatches(baseRepo.ID()) && repo.BranchMatches(pull.BaseBranch) && len(repo.PostWorkflowHooks) > 0 { + postWorkflowHooks = append(postWorkflowHooks, repo.PostWorkflowHooks...) + } + } + + // short circuit any other calls if there are no post-hooks configured + if len(postWorkflowHooks) == 0 { + return nil + } + + log.Debug("post-hooks configured, running...") + + unlockFn, err := w.WorkingDirLocker.TryLock(baseRepo.FullName, pull.Num, DefaultWorkspace) + if err != nil { + return err + } + log.Debug("got workspace lock") + defer unlockFn() + + repoDir, _, err := w.WorkingDir.Clone(log, headRepo, pull, DefaultWorkspace) + if err != nil { + return err + } + + err = w.runHooks( + models.WorkflowHookCommandContext{ + BaseRepo: baseRepo, + HeadRepo: headRepo, + Log: log, + Pull: pull, + User: user, + Verbose: false, + }, + postWorkflowHooks, repoDir) + + if err != nil { + return err + } + + return nil +} + +func (w *DefaultPostWorkflowHooksCommandRunner) runHooks( + ctx models.WorkflowHookCommandContext, + postWorkflowHooks []*valid.WorkflowHook, + repoDir string, +) error { + + for _, hook := range postWorkflowHooks { + _, err := w.PostWorkflowHookRunner.Run(ctx, hook.RunCommand, repoDir) + + if err != nil { + return err + } + } + + return nil +} diff --git a/server/events/post_workflow_hooks_command_runner_test.go b/server/events/post_workflow_hooks_command_runner_test.go new file mode 100644 index 0000000000..3aef3ca9ab --- /dev/null +++ b/server/events/post_workflow_hooks_command_runner_test.go @@ -0,0 +1,217 @@ +package events_test + +import ( + "errors" + "testing" + + . "github.com/petergtz/pegomock" + "github.com/runatlantis/atlantis/server/events" + "github.com/runatlantis/atlantis/server/events/mocks" + "github.com/runatlantis/atlantis/server/events/models" + "github.com/runatlantis/atlantis/server/events/models/fixtures" + runtime_mocks "github.com/runatlantis/atlantis/server/events/runtime/mocks" + vcsmocks "github.com/runatlantis/atlantis/server/events/vcs/mocks" + "github.com/runatlantis/atlantis/server/events/yaml/valid" + "github.com/runatlantis/atlantis/server/logging" + . "github.com/runatlantis/atlantis/testing" +) + +var postWh events.DefaultPostWorkflowHooksCommandRunner +var postWhWorkingDir *mocks.MockWorkingDir +var postWhWorkingDirLocker *mocks.MockWorkingDirLocker +var whPostWorkflowHookRunner *runtime_mocks.MockPostWorkflowHookRunner + +func postWorkflowHooksSetup(t *testing.T) { + RegisterMockTestingT(t) + vcsClient := vcsmocks.NewMockClient() + postWhWorkingDir = mocks.NewMockWorkingDir() + postWhWorkingDirLocker = mocks.NewMockWorkingDirLocker() + whPostWorkflowHookRunner = runtime_mocks.NewMockPostWorkflowHookRunner() + + postWh = events.DefaultPostWorkflowHooksCommandRunner{ + VCSClient: vcsClient, + WorkingDirLocker: postWhWorkingDirLocker, + WorkingDir: postWhWorkingDir, + PostWorkflowHookRunner: whPostWorkflowHookRunner, + } +} + +func TestRunPostHooks_Clone(t *testing.T) { + + log := logging.NewNoopLogger(t) + + var newPull = fixtures.Pull + newPull.BaseRepo = fixtures.GithubRepo + + ctx := &events.CommandContext{ + Pull: newPull, + HeadRepo: fixtures.GithubRepo, + User: fixtures.User, + Log: log, + } + + testHook := valid.WorkflowHook{ + StepName: "test", + RunCommand: "some command", + } + + pCtx := models.WorkflowHookCommandContext{ + BaseRepo: fixtures.GithubRepo, + HeadRepo: fixtures.GithubRepo, + Pull: newPull, + Log: log, + User: fixtures.User, + Verbose: false, + } + + repoDir := "path/to/repo" + result := "some result" + + t.Run("success hooks in cfg", func(t *testing.T) { + postWorkflowHooksSetup(t) + + var unlockCalled *bool = newBool(false) + unlockFn := func() { + unlockCalled = newBool(true) + } + + globalCfg := valid.GlobalCfg{ + Repos: []valid.Repo{ + { + ID: fixtures.GithubRepo.ID(), + PostWorkflowHooks: []*valid.WorkflowHook{ + &testHook, + }, + }, + }, + } + + postWh.GlobalCfg = globalCfg + + When(postWhWorkingDirLocker.TryLock(fixtures.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace)).ThenReturn(unlockFn, nil) + When(postWhWorkingDir.Clone(log, fixtures.GithubRepo, newPull, events.DefaultWorkspace)).ThenReturn(repoDir, false, nil) + When(whPostWorkflowHookRunner.Run(pCtx, testHook.RunCommand, repoDir)).ThenReturn(result, nil) + + err := postWh.RunPostHooks(ctx) + + Ok(t, err) + whPostWorkflowHookRunner.VerifyWasCalledOnce().Run(pCtx, testHook.RunCommand, repoDir) + Assert(t, *unlockCalled == true, "unlock function called") + }) + t.Run("success hooks not in cfg", func(t *testing.T) { + postWorkflowHooksSetup(t) + globalCfg := valid.GlobalCfg{ + Repos: []valid.Repo{ + // one with hooks but mismatched id + { + ID: "id1", + PostWorkflowHooks: []*valid.WorkflowHook{ + &testHook, + }, + }, + // one with the correct id but no hooks + { + ID: fixtures.GithubRepo.ID(), + PostWorkflowHooks: []*valid.WorkflowHook{}, + }, + }, + } + + postWh.GlobalCfg = globalCfg + + err := postWh.RunPostHooks(ctx) + + Ok(t, err) + + whPostWorkflowHookRunner.VerifyWasCalled(Never()).Run(pCtx, testHook.RunCommand, repoDir) + postWhWorkingDirLocker.VerifyWasCalled(Never()).TryLock(fixtures.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace) + postWhWorkingDir.VerifyWasCalled(Never()).Clone(log, fixtures.GithubRepo, newPull, events.DefaultWorkspace) + }) + t.Run("error locking work dir", func(t *testing.T) { + postWorkflowHooksSetup(t) + + globalCfg := valid.GlobalCfg{ + Repos: []valid.Repo{ + { + ID: fixtures.GithubRepo.ID(), + PostWorkflowHooks: []*valid.WorkflowHook{ + &testHook, + }, + }, + }, + } + + postWh.GlobalCfg = globalCfg + + When(postWhWorkingDirLocker.TryLock(fixtures.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace)).ThenReturn(func() {}, errors.New("some error")) + + err := postWh.RunPostHooks(ctx) + + Assert(t, err != nil, "error not nil") + postWhWorkingDir.VerifyWasCalled(Never()).Clone(log, fixtures.GithubRepo, newPull, events.DefaultWorkspace) + whPostWorkflowHookRunner.VerifyWasCalled(Never()).Run(pCtx, testHook.RunCommand, repoDir) + }) + + t.Run("error cloning", func(t *testing.T) { + postWorkflowHooksSetup(t) + + var unlockCalled *bool = newBool(false) + unlockFn := func() { + unlockCalled = newBool(true) + } + + globalCfg := valid.GlobalCfg{ + Repos: []valid.Repo{ + { + ID: fixtures.GithubRepo.ID(), + PostWorkflowHooks: []*valid.WorkflowHook{ + &testHook, + }, + }, + }, + } + + postWh.GlobalCfg = globalCfg + + When(postWhWorkingDirLocker.TryLock(fixtures.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace)).ThenReturn(unlockFn, nil) + When(postWhWorkingDir.Clone(log, fixtures.GithubRepo, newPull, events.DefaultWorkspace)).ThenReturn(repoDir, false, errors.New("some error")) + + err := postWh.RunPostHooks(ctx) + + Assert(t, err != nil, "error not nil") + + whPostWorkflowHookRunner.VerifyWasCalled(Never()).Run(pCtx, testHook.RunCommand, repoDir) + Assert(t, *unlockCalled == true, "unlock function called") + }) + + t.Run("error running post hook", func(t *testing.T) { + postWorkflowHooksSetup(t) + + var unlockCalled *bool = newBool(false) + unlockFn := func() { + unlockCalled = newBool(true) + } + + globalCfg := valid.GlobalCfg{ + Repos: []valid.Repo{ + { + ID: fixtures.GithubRepo.ID(), + PostWorkflowHooks: []*valid.WorkflowHook{ + &testHook, + }, + }, + }, + } + + postWh.GlobalCfg = globalCfg + + When(postWhWorkingDirLocker.TryLock(fixtures.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace)).ThenReturn(unlockFn, nil) + When(postWhWorkingDir.Clone(log, fixtures.GithubRepo, newPull, events.DefaultWorkspace)).ThenReturn(repoDir, false, nil) + When(whPostWorkflowHookRunner.Run(pCtx, testHook.RunCommand, repoDir)).ThenReturn(result, errors.New("some error")) + + err := postWh.RunPostHooks(ctx) + + Assert(t, err != nil, "error not nil") + Assert(t, *unlockCalled == true, "unlock function called") + }) +} diff --git a/server/events/pre_workflow_hooks_command_runner.go b/server/events/pre_workflow_hooks_command_runner.go index cd5ae6eed1..e7df2bc89b 100644 --- a/server/events/pre_workflow_hooks_command_runner.go +++ b/server/events/pre_workflow_hooks_command_runner.go @@ -32,7 +32,7 @@ func (w *DefaultPreWorkflowHooksCommandRunner) RunPreHooks( user := ctx.User log := ctx.Log - preWorkflowHooks := make([]*valid.PreWorkflowHook, 0) + preWorkflowHooks := make([]*valid.WorkflowHook, 0) for _, repo := range w.GlobalCfg.Repos { if repo.IDMatches(baseRepo.ID()) && repo.BranchMatches(pull.BaseBranch) && len(repo.PreWorkflowHooks) > 0 { preWorkflowHooks = append(preWorkflowHooks, repo.PreWorkflowHooks...) @@ -59,7 +59,7 @@ func (w *DefaultPreWorkflowHooksCommandRunner) RunPreHooks( } err = w.runHooks( - models.PreWorkflowHookCommandContext{ + models.WorkflowHookCommandContext{ BaseRepo: baseRepo, HeadRepo: headRepo, Log: log, @@ -77,8 +77,8 @@ func (w *DefaultPreWorkflowHooksCommandRunner) RunPreHooks( } func (w *DefaultPreWorkflowHooksCommandRunner) runHooks( - ctx models.PreWorkflowHookCommandContext, - preWorkflowHooks []*valid.PreWorkflowHook, + ctx models.WorkflowHookCommandContext, + preWorkflowHooks []*valid.WorkflowHook, repoDir string, ) error { diff --git a/server/events/pre_workflow_hooks_command_runner_test.go b/server/events/pre_workflow_hooks_command_runner_test.go index 0b9fd48cfc..811fc91ec0 100644 --- a/server/events/pre_workflow_hooks_command_runner_test.go +++ b/server/events/pre_workflow_hooks_command_runner_test.go @@ -16,22 +16,22 @@ import ( . "github.com/runatlantis/atlantis/testing" ) -var wh events.DefaultPreWorkflowHooksCommandRunner -var whWorkingDir *mocks.MockWorkingDir -var whWorkingDirLocker *mocks.MockWorkingDirLocker +var preWh events.DefaultPreWorkflowHooksCommandRunner +var preWhWorkingDir *mocks.MockWorkingDir +var preWhWorkingDirLocker *mocks.MockWorkingDirLocker var whPreWorkflowHookRunner *runtime_mocks.MockPreWorkflowHookRunner func preWorkflowHooksSetup(t *testing.T) { RegisterMockTestingT(t) vcsClient := vcsmocks.NewMockClient() - whWorkingDir = mocks.NewMockWorkingDir() - whWorkingDirLocker = mocks.NewMockWorkingDirLocker() + preWhWorkingDir = mocks.NewMockWorkingDir() + preWhWorkingDirLocker = mocks.NewMockWorkingDirLocker() whPreWorkflowHookRunner = runtime_mocks.NewMockPreWorkflowHookRunner() - wh = events.DefaultPreWorkflowHooksCommandRunner{ + preWh = events.DefaultPreWorkflowHooksCommandRunner{ VCSClient: vcsClient, - WorkingDirLocker: whWorkingDirLocker, - WorkingDir: whWorkingDir, + WorkingDirLocker: preWhWorkingDirLocker, + WorkingDir: preWhWorkingDir, PreWorkflowHookRunner: whPreWorkflowHookRunner, } } @@ -54,12 +54,12 @@ func TestRunPreHooks_Clone(t *testing.T) { Log: log, } - testHook := valid.PreWorkflowHook{ + testHook := valid.WorkflowHook{ StepName: "test", RunCommand: "some command", } - pCtx := models.PreWorkflowHookCommandContext{ + pCtx := models.WorkflowHookCommandContext{ BaseRepo: fixtures.GithubRepo, HeadRepo: fixtures.GithubRepo, Pull: newPull, @@ -83,20 +83,20 @@ func TestRunPreHooks_Clone(t *testing.T) { Repos: []valid.Repo{ { ID: fixtures.GithubRepo.ID(), - PreWorkflowHooks: []*valid.PreWorkflowHook{ + PreWorkflowHooks: []*valid.WorkflowHook{ &testHook, }, }, }, } - wh.GlobalCfg = globalCfg + preWh.GlobalCfg = globalCfg - When(whWorkingDirLocker.TryLock(fixtures.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace)).ThenReturn(unlockFn, nil) - When(whWorkingDir.Clone(log, fixtures.GithubRepo, newPull, events.DefaultWorkspace)).ThenReturn(repoDir, false, nil) + When(preWhWorkingDirLocker.TryLock(fixtures.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace)).ThenReturn(unlockFn, nil) + When(preWhWorkingDir.Clone(log, fixtures.GithubRepo, newPull, events.DefaultWorkspace)).ThenReturn(repoDir, false, nil) When(whPreWorkflowHookRunner.Run(pCtx, testHook.RunCommand, repoDir)).ThenReturn(result, nil) - err := wh.RunPreHooks(ctx) + err := preWh.RunPreHooks(ctx) Ok(t, err) whPreWorkflowHookRunner.VerifyWasCalledOnce().Run(pCtx, testHook.RunCommand, repoDir) @@ -109,27 +109,27 @@ func TestRunPreHooks_Clone(t *testing.T) { // one with hooks but mismatched id { ID: "id1", - PreWorkflowHooks: []*valid.PreWorkflowHook{ + PreWorkflowHooks: []*valid.WorkflowHook{ &testHook, }, }, // one with the correct id but no hooks { ID: fixtures.GithubRepo.ID(), - PreWorkflowHooks: []*valid.PreWorkflowHook{}, + PreWorkflowHooks: []*valid.WorkflowHook{}, }, }, } - wh.GlobalCfg = globalCfg + preWh.GlobalCfg = globalCfg - err := wh.RunPreHooks(ctx) + err := preWh.RunPreHooks(ctx) Ok(t, err) whPreWorkflowHookRunner.VerifyWasCalled(Never()).Run(pCtx, testHook.RunCommand, repoDir) - whWorkingDirLocker.VerifyWasCalled(Never()).TryLock(fixtures.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace) - whWorkingDir.VerifyWasCalled(Never()).Clone(log, fixtures.GithubRepo, newPull, events.DefaultWorkspace) + preWhWorkingDirLocker.VerifyWasCalled(Never()).TryLock(fixtures.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace) + preWhWorkingDir.VerifyWasCalled(Never()).Clone(log, fixtures.GithubRepo, newPull, events.DefaultWorkspace) }) t.Run("error locking work dir", func(t *testing.T) { preWorkflowHooksSetup(t) @@ -138,21 +138,21 @@ func TestRunPreHooks_Clone(t *testing.T) { Repos: []valid.Repo{ { ID: fixtures.GithubRepo.ID(), - PreWorkflowHooks: []*valid.PreWorkflowHook{ + PreWorkflowHooks: []*valid.WorkflowHook{ &testHook, }, }, }, } - wh.GlobalCfg = globalCfg + preWh.GlobalCfg = globalCfg - When(whWorkingDirLocker.TryLock(fixtures.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace)).ThenReturn(func() {}, errors.New("some error")) + When(preWhWorkingDirLocker.TryLock(fixtures.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace)).ThenReturn(func() {}, errors.New("some error")) - err := wh.RunPreHooks(ctx) + err := preWh.RunPreHooks(ctx) Assert(t, err != nil, "error not nil") - whWorkingDir.VerifyWasCalled(Never()).Clone(log, fixtures.GithubRepo, newPull, events.DefaultWorkspace) + preWhWorkingDir.VerifyWasCalled(Never()).Clone(log, fixtures.GithubRepo, newPull, events.DefaultWorkspace) whPreWorkflowHookRunner.VerifyWasCalled(Never()).Run(pCtx, testHook.RunCommand, repoDir) }) @@ -168,19 +168,19 @@ func TestRunPreHooks_Clone(t *testing.T) { Repos: []valid.Repo{ { ID: fixtures.GithubRepo.ID(), - PreWorkflowHooks: []*valid.PreWorkflowHook{ + PreWorkflowHooks: []*valid.WorkflowHook{ &testHook, }, }, }, } - wh.GlobalCfg = globalCfg + preWh.GlobalCfg = globalCfg - When(whWorkingDirLocker.TryLock(fixtures.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace)).ThenReturn(unlockFn, nil) - When(whWorkingDir.Clone(log, fixtures.GithubRepo, newPull, events.DefaultWorkspace)).ThenReturn(repoDir, false, errors.New("some error")) + When(preWhWorkingDirLocker.TryLock(fixtures.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace)).ThenReturn(unlockFn, nil) + When(preWhWorkingDir.Clone(log, fixtures.GithubRepo, newPull, events.DefaultWorkspace)).ThenReturn(repoDir, false, errors.New("some error")) - err := wh.RunPreHooks(ctx) + err := preWh.RunPreHooks(ctx) Assert(t, err != nil, "error not nil") @@ -200,20 +200,20 @@ func TestRunPreHooks_Clone(t *testing.T) { Repos: []valid.Repo{ { ID: fixtures.GithubRepo.ID(), - PreWorkflowHooks: []*valid.PreWorkflowHook{ + PreWorkflowHooks: []*valid.WorkflowHook{ &testHook, }, }, }, } - wh.GlobalCfg = globalCfg + preWh.GlobalCfg = globalCfg - When(whWorkingDirLocker.TryLock(fixtures.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace)).ThenReturn(unlockFn, nil) - When(whWorkingDir.Clone(log, fixtures.GithubRepo, newPull, events.DefaultWorkspace)).ThenReturn(repoDir, false, nil) + When(preWhWorkingDirLocker.TryLock(fixtures.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace)).ThenReturn(unlockFn, nil) + When(preWhWorkingDir.Clone(log, fixtures.GithubRepo, newPull, events.DefaultWorkspace)).ThenReturn(repoDir, false, nil) When(whPreWorkflowHookRunner.Run(pCtx, testHook.RunCommand, repoDir)).ThenReturn(result, errors.New("some error")) - err := wh.RunPreHooks(ctx) + err := preWh.RunPreHooks(ctx) Assert(t, err != nil, "error not nil") Assert(t, *unlockCalled == true, "unlock function called") diff --git a/server/events/runtime/mocks/matchers/models_preworkflowhookcommandcontext.go b/server/events/runtime/mocks/matchers/models_preworkflowhookcommandcontext.go deleted file mode 100644 index 8a57120c7c..0000000000 --- a/server/events/runtime/mocks/matchers/models_preworkflowhookcommandcontext.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code generated by pegomock. DO NOT EDIT. -package matchers - -import ( - "github.com/petergtz/pegomock" - "reflect" - - models "github.com/runatlantis/atlantis/server/events/models" -) - -func AnyModelsPreWorkflowHookCommandContext() models.PreWorkflowHookCommandContext { - pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(models.PreWorkflowHookCommandContext))(nil)).Elem())) - var nullValue models.PreWorkflowHookCommandContext - return nullValue -} - -func EqModelsPreWorkflowHookCommandContext(value models.PreWorkflowHookCommandContext) models.PreWorkflowHookCommandContext { - pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) - var nullValue models.PreWorkflowHookCommandContext - return nullValue -} - -func NotEqModelsPreWorkflowHookCommandContext(value models.PreWorkflowHookCommandContext) models.PreWorkflowHookCommandContext { - pegomock.RegisterMatcher(&pegomock.NotEqMatcher{Value: value}) - var nullValue models.PreWorkflowHookCommandContext - return nullValue -} - -func ModelsPreWorkflowHookCommandContextThat(matcher pegomock.ArgumentMatcher) models.PreWorkflowHookCommandContext { - pegomock.RegisterMatcher(matcher) - var nullValue models.PreWorkflowHookCommandContext - return nullValue -} diff --git a/server/events/runtime/mocks/matchers/models_workflowhookcommandcontext.go b/server/events/runtime/mocks/matchers/models_workflowhookcommandcontext.go new file mode 100644 index 0000000000..18a5a5bae9 --- /dev/null +++ b/server/events/runtime/mocks/matchers/models_workflowhookcommandcontext.go @@ -0,0 +1,33 @@ +// Code generated by pegomock. DO NOT EDIT. +package matchers + +import ( + "github.com/petergtz/pegomock" + "reflect" + + models "github.com/runatlantis/atlantis/server/events/models" +) + +func AnyModelsWorkflowHookCommandContext() models.WorkflowHookCommandContext { + pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(models.WorkflowHookCommandContext))(nil)).Elem())) + var nullValue models.WorkflowHookCommandContext + return nullValue +} + +func EqModelsWorkflowHookCommandContext(value models.WorkflowHookCommandContext) models.WorkflowHookCommandContext { + pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) + var nullValue models.WorkflowHookCommandContext + return nullValue +} + +func NotEqModelsWorkflowHookCommandContext(value models.WorkflowHookCommandContext) models.WorkflowHookCommandContext { + pegomock.RegisterMatcher(&pegomock.NotEqMatcher{Value: value}) + var nullValue models.WorkflowHookCommandContext + return nullValue +} + +func ModelsWorkflowHookCommandContextThat(matcher pegomock.ArgumentMatcher) models.WorkflowHookCommandContext { + pegomock.RegisterMatcher(matcher) + var nullValue models.WorkflowHookCommandContext + return nullValue +} diff --git a/server/events/runtime/mocks/mock_post_workflows_hook_runner.go b/server/events/runtime/mocks/mock_post_workflows_hook_runner.go new file mode 100644 index 0000000000..9dcdc738b1 --- /dev/null +++ b/server/events/runtime/mocks/mock_post_workflows_hook_runner.go @@ -0,0 +1,117 @@ +// Code generated by pegomock. DO NOT EDIT. +// Source: github.com/runatlantis/atlantis/server/events/runtime (interfaces: PostWorkflowHookRunner) + +package mocks + +import ( + pegomock "github.com/petergtz/pegomock" + models "github.com/runatlantis/atlantis/server/events/models" + "reflect" + "time" +) + +type MockPostWorkflowHookRunner struct { + fail func(message string, callerSkip ...int) +} + +func NewMockPostWorkflowHookRunner(options ...pegomock.Option) *MockPostWorkflowHookRunner { + mock := &MockPostWorkflowHookRunner{} + for _, option := range options { + option.Apply(mock) + } + return mock +} + +func (mock *MockPostWorkflowHookRunner) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } +func (mock *MockPostWorkflowHookRunner) FailHandler() pegomock.FailHandler { return mock.fail } + +func (mock *MockPostWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, path string) (string, error) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockPostWorkflowHookRunner().") + } + params := []pegomock.Param{ctx, command, path} + 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 + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(string) + } + if result[1] != nil { + ret1 = result[1].(error) + } + } + return ret0, ret1 +} + +func (mock *MockPostWorkflowHookRunner) VerifyWasCalledOnce() *VerifierMockPostWorkflowHookRunner { + return &VerifierMockPostWorkflowHookRunner{ + mock: mock, + invocationCountMatcher: pegomock.Times(1), + } +} + +func (mock *MockPostWorkflowHookRunner) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockPostWorkflowHookRunner { + return &VerifierMockPostWorkflowHookRunner{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + } +} + +func (mock *MockPostWorkflowHookRunner) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockPostWorkflowHookRunner { + return &VerifierMockPostWorkflowHookRunner{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + inOrderContext: inOrderContext, + } +} + +func (mock *MockPostWorkflowHookRunner) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockPostWorkflowHookRunner { + return &VerifierMockPostWorkflowHookRunner{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + timeout: timeout, + } +} + +type VerifierMockPostWorkflowHookRunner struct { + mock *MockPostWorkflowHookRunner + invocationCountMatcher pegomock.InvocationCountMatcher + inOrderContext *pegomock.InOrderContext + timeout time.Duration +} + +func (verifier *VerifierMockPostWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, path string) *MockPostWorkflowHookRunner_Run_OngoingVerification { + params := []pegomock.Param{ctx, command, path} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Run", params, verifier.timeout) + return &MockPostWorkflowHookRunner_Run_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockPostWorkflowHookRunner_Run_OngoingVerification struct { + mock *MockPostWorkflowHookRunner + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockPostWorkflowHookRunner_Run_OngoingVerification) GetCapturedArguments() (models.WorkflowHookCommandContext, string, string) { + ctx, command, path := c.GetAllCapturedArguments() + return ctx[len(ctx)-1], command[len(command)-1], path[len(path)-1] +} + +func (c *MockPostWorkflowHookRunner_Run_OngoingVerification) GetAllCapturedArguments() (_param0 []models.WorkflowHookCommandContext, _param1 []string, _param2 []string) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]models.WorkflowHookCommandContext, len(c.methodInvocations)) + for u, param := range params[0] { + _param0[u] = param.(models.WorkflowHookCommandContext) + } + _param1 = make([]string, len(c.methodInvocations)) + for u, param := range params[1] { + _param1[u] = param.(string) + } + _param2 = make([]string, len(c.methodInvocations)) + for u, param := range params[2] { + _param2[u] = param.(string) + } + } + return +} diff --git a/server/events/runtime/mocks/mock_pre_workflows_hook_runner.go b/server/events/runtime/mocks/mock_pre_workflows_hook_runner.go index dcb5c32935..6fbe387407 100644 --- a/server/events/runtime/mocks/mock_pre_workflows_hook_runner.go +++ b/server/events/runtime/mocks/mock_pre_workflows_hook_runner.go @@ -25,7 +25,7 @@ func NewMockPreWorkflowHookRunner(options ...pegomock.Option) *MockPreWorkflowHo func (mock *MockPreWorkflowHookRunner) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } func (mock *MockPreWorkflowHookRunner) FailHandler() pegomock.FailHandler { return mock.fail } -func (mock *MockPreWorkflowHookRunner) Run(ctx models.PreWorkflowHookCommandContext, command string, path string) (string, error) { +func (mock *MockPreWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, path string) (string, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockPreWorkflowHookRunner().") } @@ -81,7 +81,7 @@ type VerifierMockPreWorkflowHookRunner struct { timeout time.Duration } -func (verifier *VerifierMockPreWorkflowHookRunner) Run(ctx models.PreWorkflowHookCommandContext, command string, path string) *MockPreWorkflowHookRunner_Run_OngoingVerification { +func (verifier *VerifierMockPreWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, path string) *MockPreWorkflowHookRunner_Run_OngoingVerification { params := []pegomock.Param{ctx, command, path} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Run", params, verifier.timeout) return &MockPreWorkflowHookRunner_Run_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} @@ -92,17 +92,17 @@ type MockPreWorkflowHookRunner_Run_OngoingVerification struct { methodInvocations []pegomock.MethodInvocation } -func (c *MockPreWorkflowHookRunner_Run_OngoingVerification) GetCapturedArguments() (models.PreWorkflowHookCommandContext, string, string) { +func (c *MockPreWorkflowHookRunner_Run_OngoingVerification) GetCapturedArguments() (models.WorkflowHookCommandContext, string, string) { ctx, command, path := c.GetAllCapturedArguments() return ctx[len(ctx)-1], command[len(command)-1], path[len(path)-1] } -func (c *MockPreWorkflowHookRunner_Run_OngoingVerification) GetAllCapturedArguments() (_param0 []models.PreWorkflowHookCommandContext, _param1 []string, _param2 []string) { +func (c *MockPreWorkflowHookRunner_Run_OngoingVerification) GetAllCapturedArguments() (_param0 []models.WorkflowHookCommandContext, _param1 []string, _param2 []string) { params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) if len(params) > 0 { - _param0 = make([]models.PreWorkflowHookCommandContext, len(c.methodInvocations)) + _param0 = make([]models.WorkflowHookCommandContext, len(c.methodInvocations)) for u, param := range params[0] { - _param0[u] = param.(models.PreWorkflowHookCommandContext) + _param0[u] = param.(models.WorkflowHookCommandContext) } _param1 = make([]string, len(c.methodInvocations)) for u, param := range params[1] { diff --git a/server/events/runtime/post_workflow_hook_runner.go b/server/events/runtime/post_workflow_hook_runner.go new file mode 100644 index 0000000000..8f2df1d2db --- /dev/null +++ b/server/events/runtime/post_workflow_hook_runner.go @@ -0,0 +1,52 @@ +package runtime + +import ( + "fmt" + "os" + "os/exec" + + "github.com/runatlantis/atlantis/server/events/models" +) + +//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_post_workflows_hook_runner.go PostWorkflowHookRunner +type PostWorkflowHookRunner interface { + Run(ctx models.WorkflowHookCommandContext, command string, path string) (string, error) +} + +type DefaultPostWorkflowHookRunner struct{} + +func (wh DefaultPostWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, path string) (string, error) { + cmd := exec.Command("sh", "-c", command) // #nosec + cmd.Dir = path + + baseEnvVars := os.Environ() + customEnvVars := map[string]string{ + "BASE_BRANCH_NAME": ctx.Pull.BaseBranch, + "BASE_REPO_NAME": ctx.BaseRepo.Name, + "BASE_REPO_OWNER": ctx.BaseRepo.Owner, + "DIR": path, + "HEAD_BRANCH_NAME": ctx.Pull.HeadBranch, + "HEAD_COMMIT": ctx.Pull.HeadCommit, + "HEAD_REPO_NAME": ctx.HeadRepo.Name, + "HEAD_REPO_OWNER": ctx.HeadRepo.Owner, + "PULL_AUTHOR": ctx.Pull.Author, + "PULL_NUM": fmt.Sprintf("%d", ctx.Pull.Num), + "USER_NAME": ctx.User.Username, + } + + finalEnvVars := baseEnvVars + for key, val := range customEnvVars { + finalEnvVars = append(finalEnvVars, fmt.Sprintf("%s=%s", key, val)) + } + + cmd.Env = finalEnvVars + out, err := cmd.CombinedOutput() + + if err != nil { + err = fmt.Errorf("%s: running %q in %q: \n%s", err, command, path, out) + ctx.Log.Debug("error: %s", err) + return "", err + } + ctx.Log.Info("successfully ran %q in %q", command, path) + return string(out), nil +} diff --git a/server/events/runtime/post_workflow_hook_runner_test.go b/server/events/runtime/post_workflow_hook_runner_test.go new file mode 100644 index 0000000000..8926df28b3 --- /dev/null +++ b/server/events/runtime/post_workflow_hook_runner_test.go @@ -0,0 +1,111 @@ +package runtime_test + +import ( + "strings" + "testing" + + . "github.com/petergtz/pegomock" + "github.com/runatlantis/atlantis/server/events/mocks/matchers" + "github.com/runatlantis/atlantis/server/events/models" + "github.com/runatlantis/atlantis/server/events/runtime" + "github.com/runatlantis/atlantis/server/events/terraform/mocks" + matchers2 "github.com/runatlantis/atlantis/server/events/terraform/mocks/matchers" + "github.com/runatlantis/atlantis/server/logging" + . "github.com/runatlantis/atlantis/testing" +) + +func TestPostWorkflowHookRunner_Run(t *testing.T) { + cases := []struct { + Command string + ExpOut string + ExpErr string + }{ + { + Command: "", + ExpOut: "", + }, + { + Command: "echo hi", + ExpOut: "hi\n", + }, + { + Command: `printf \'your main.tf file does not provide default region.\\ncheck\'`, + ExpOut: `'your`, + }, + { + Command: `printf 'your main.tf file does not provide default region.\ncheck'`, + ExpOut: "your main.tf file does not provide default region.\ncheck", + }, + { + Command: "echo 'a", + ExpErr: "exit status 2: running \"echo 'a\" in", + }, + { + Command: "echo hi >> file && cat file", + ExpOut: "hi\n", + }, + { + Command: "lkjlkj", + ExpErr: "exit status 127: running \"lkjlkj\" in", + }, + { + Command: "echo base_repo_name=$BASE_REPO_NAME base_repo_owner=$BASE_REPO_OWNER head_repo_name=$HEAD_REPO_NAME head_repo_owner=$HEAD_REPO_OWNER head_branch_name=$HEAD_BRANCH_NAME head_commit=$HEAD_COMMIT base_branch_name=$BASE_BRANCH_NAME pull_num=$PULL_NUM pull_author=$PULL_AUTHOR", + ExpOut: "base_repo_name=basename base_repo_owner=baseowner head_repo_name=headname head_repo_owner=headowner head_branch_name=add-feat head_commit=12345abcdef base_branch_name=master pull_num=2 pull_author=acme\n", + }, + { + Command: "echo user_name=$USER_NAME", + ExpOut: "user_name=acme-user\n", + }, + } + + for _, c := range cases { + var err error + + Ok(t, err) + + RegisterMockTestingT(t) + terraform := mocks.NewMockClient() + When(terraform.EnsureVersion(matchers.AnyPtrToLoggingSimpleLogger(), matchers2.AnyPtrToGoVersionVersion())). + ThenReturn(nil) + + logger := logging.NewNoopLogger(t) + + r := runtime.DefaultPostWorkflowHookRunner{} + t.Run(c.Command, func(t *testing.T) { + tmpDir, cleanup := TempDir(t) + defer cleanup() + ctx := models.WorkflowHookCommandContext{ + BaseRepo: models.Repo{ + Name: "basename", + Owner: "baseowner", + }, + HeadRepo: models.Repo{ + Name: "headname", + Owner: "headowner", + }, + Pull: models.PullRequest{ + Num: 2, + HeadBranch: "add-feat", + HeadCommit: "12345abcdef", + BaseBranch: "master", + Author: "acme", + }, + User: models.User{ + Username: "acme-user", + }, + Log: logger, + } + out, err := r.Run(ctx, c.Command, tmpDir) + if c.ExpErr != "" { + ErrContains(t, c.ExpErr, err) + return + } + Ok(t, err) + // Replace $DIR in the exp with the actual temp dir. We do this + // here because when constructing the cases we don't yet know the + // temp dir. + expOut := strings.Replace(c.ExpOut, "$DIR", tmpDir, -1) + Equals(t, expOut, out) + }) + } +} diff --git a/server/events/runtime/pre_workflow_hook_runner.go b/server/events/runtime/pre_workflow_hook_runner.go index df0c694463..d3004c84eb 100644 --- a/server/events/runtime/pre_workflow_hook_runner.go +++ b/server/events/runtime/pre_workflow_hook_runner.go @@ -10,12 +10,12 @@ import ( //go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_pre_workflows_hook_runner.go PreWorkflowHookRunner type PreWorkflowHookRunner interface { - Run(ctx models.PreWorkflowHookCommandContext, command string, path string) (string, error) + Run(ctx models.WorkflowHookCommandContext, command string, path string) (string, error) } type DefaultPreWorkflowHookRunner struct{} -func (wh DefaultPreWorkflowHookRunner) Run(ctx models.PreWorkflowHookCommandContext, command string, path string) (string, error) { +func (wh DefaultPreWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, path string) (string, error) { cmd := exec.Command("sh", "-c", command) // #nosec cmd.Dir = path diff --git a/server/events/runtime/pre_workflow_hook_runner_test.go b/server/events/runtime/pre_workflow_hook_runner_test.go index 75eb4a2b11..fabffa7cea 100644 --- a/server/events/runtime/pre_workflow_hook_runner_test.go +++ b/server/events/runtime/pre_workflow_hook_runner_test.go @@ -74,7 +74,7 @@ func TestPreWorkflowHookRunner_Run(t *testing.T) { t.Run(c.Command, func(t *testing.T) { tmpDir, cleanup := TempDir(t) defer cleanup() - ctx := models.PreWorkflowHookCommandContext{ + ctx := models.WorkflowHookCommandContext{ BaseRepo: models.Repo{ Name: "basename", Owner: "baseowner", diff --git a/server/events/yaml/parser_validator_test.go b/server/events/yaml/parser_validator_test.go index a722585e07..da54f10196 100644 --- a/server/events/yaml/parser_validator_test.go +++ b/server/events/yaml/parser_validator_test.go @@ -1132,11 +1132,17 @@ func TestParseGlobalCfg(t *testing.T) { } defaultCfg := valid.NewGlobalCfgFromArgs(globalCfgArgs) - preWorkflowHook := &valid.PreWorkflowHook{ + preWorkflowHook := &valid.WorkflowHook{ StepName: "run", RunCommand: "custom workflow command", } - preWorkflowHooks := []*valid.PreWorkflowHook{preWorkflowHook} + preWorkflowHooks := []*valid.WorkflowHook{preWorkflowHook} + + postWorkflowHook := &valid.WorkflowHook{ + StepName: "run", + RunCommand: "custom workflow command", + } + postWorkflowHooks := []*valid.WorkflowHook{postWorkflowHook} customWorkflow1 := valid.Workflow{ Name: "custom1", @@ -1307,12 +1313,16 @@ repos: pre_workflow_hooks: - run: custom workflow command workflow: custom1 + post_workflow_hooks: + - run: custom workflow command allowed_overrides: [apply_requirements, workflow, delete_source_branch_on_merge] allow_custom_workflows: true - id: /.*/ branch: /(master|main)/ pre_workflow_hooks: - run: custom workflow command + post_workflow_hooks: + - run: custom workflow command workflows: custom1: plan: @@ -1346,13 +1356,15 @@ policies: ApplyRequirements: []string{"approved", "mergeable"}, PreWorkflowHooks: preWorkflowHooks, Workflow: &customWorkflow1, + PostWorkflowHooks: postWorkflowHooks, AllowedOverrides: []string{"apply_requirements", "workflow", "delete_source_branch_on_merge"}, AllowCustomWorkflows: Bool(true), }, { - IDRegex: regexp.MustCompile(".*"), - BranchRegex: regexp.MustCompile("(master|main)"), - PreWorkflowHooks: preWorkflowHooks, + IDRegex: regexp.MustCompile(".*"), + BranchRegex: regexp.MustCompile("(master|main)"), + PreWorkflowHooks: preWorkflowHooks, + PostWorkflowHooks: postWorkflowHooks, }, }, Workflows: map[string]valid.Workflow{ diff --git a/server/events/yaml/raw/global_cfg.go b/server/events/yaml/raw/global_cfg.go index c64112e107..28c9d63ff0 100644 --- a/server/events/yaml/raw/global_cfg.go +++ b/server/events/yaml/raw/global_cfg.go @@ -19,15 +19,16 @@ type GlobalCfg struct { // Repo is the raw schema for repos in the server-side repo config. type Repo struct { - ID string `yaml:"id" json:"id"` - Branch string `yaml:"branch" json:"branch"` - ApplyRequirements []string `yaml:"apply_requirements" json:"apply_requirements"` - PreWorkflowHooks []PreWorkflowHook `yaml:"pre_workflow_hooks" json:"pre_workflow_hooks"` - Workflow *string `yaml:"workflow,omitempty" json:"workflow,omitempty"` - AllowedWorkflows []string `yaml:"allowed_workflows,omitempty" json:"allowed_workflows,omitempty"` - AllowedOverrides []string `yaml:"allowed_overrides" json:"allowed_overrides"` - AllowCustomWorkflows *bool `yaml:"allow_custom_workflows,omitempty" json:"allow_custom_workflows,omitempty"` - DeleteSourceBranchOnMerge *bool `yaml:"delete_source_branch_on_merge,omitempty" json:"delete_source_branch_on_merge,omitempty"` + ID string `yaml:"id" json:"id"` + Branch string `yaml:"branch" json:"branch"` + ApplyRequirements []string `yaml:"apply_requirements" json:"apply_requirements"` + PreWorkflowHooks []WorkflowHook `yaml:"pre_workflow_hooks" json:"pre_workflow_hooks"` + Workflow *string `yaml:"workflow,omitempty" json:"workflow,omitempty"` + PostWorkflowHooks []WorkflowHook `yaml:"post_workflow_hooks" json:"post_workflow_hooks"` + AllowedWorkflows []string `yaml:"allowed_workflows,omitempty" json:"allowed_workflows,omitempty"` + AllowedOverrides []string `yaml:"allowed_overrides" json:"allowed_overrides"` + AllowCustomWorkflows *bool `yaml:"allow_custom_workflows,omitempty" json:"allow_custom_workflows,omitempty"` + DeleteSourceBranchOnMerge *bool `yaml:"delete_source_branch_on_merge,omitempty" json:"delete_source_branch_on_merge,omitempty"` } func (g GlobalCfg) Validate() error { @@ -218,13 +219,20 @@ func (r Repo) ToValid(workflows map[string]valid.Workflow, globalApplyReqs []str workflow = &ptr } - var preWorkflowHooks []*valid.PreWorkflowHook + var preWorkflowHooks []*valid.WorkflowHook if len(r.PreWorkflowHooks) > 0 { for _, hook := range r.PreWorkflowHooks { preWorkflowHooks = append(preWorkflowHooks, hook.ToValid()) } } + var postWorkflowHooks []*valid.WorkflowHook + if len(r.PostWorkflowHooks) > 0 { + for _, hook := range r.PostWorkflowHooks { + postWorkflowHooks = append(postWorkflowHooks, hook.ToValid()) + } + } + var mergedApplyReqs []string mergedApplyReqs = append(mergedApplyReqs, r.ApplyRequirements...) @@ -247,6 +255,7 @@ OUTER: ApplyRequirements: mergedApplyReqs, PreWorkflowHooks: preWorkflowHooks, Workflow: workflow, + PostWorkflowHooks: postWorkflowHooks, AllowedWorkflows: r.AllowedWorkflows, AllowedOverrides: r.AllowedOverrides, AllowCustomWorkflows: r.AllowCustomWorkflows, diff --git a/server/events/yaml/raw/pre_workflow_step.go b/server/events/yaml/raw/workflow_step.go similarity index 76% rename from server/events/yaml/raw/pre_workflow_step.go rename to server/events/yaml/raw/workflow_step.go index 221d09207f..848234a848 100644 --- a/server/events/yaml/raw/pre_workflow_step.go +++ b/server/events/yaml/raw/workflow_step.go @@ -11,29 +11,29 @@ import ( "github.com/runatlantis/atlantis/server/events/yaml/valid" ) -// PreWorkflowHook represents a single action/command to perform. In YAML, +// WorkflowHook represents a single action/command to perform. In YAML, // it can be set as // A map for a custom run commands: // - run: my custom command -type PreWorkflowHook struct { +type WorkflowHook struct { StringVal map[string]string } -func (s *PreWorkflowHook) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (s *WorkflowHook) UnmarshalYAML(unmarshal func(interface{}) error) error { return s.unmarshalGeneric(unmarshal) } -func (s PreWorkflowHook) MarshalYAML() (interface{}, error) { +func (s WorkflowHook) MarshalYAML() (interface{}, error) { return s.marshalGeneric() } -func (s *PreWorkflowHook) UnmarshalJSON(data []byte) error { +func (s *WorkflowHook) UnmarshalJSON(data []byte) error { return s.unmarshalGeneric(func(i interface{}) error { return json.Unmarshal(data, i) }) } -func (s *PreWorkflowHook) MarshalJSON() ([]byte, error) { +func (s *WorkflowHook) MarshalJSON() ([]byte, error) { out, err := s.marshalGeneric() if err != nil { return nil, err @@ -41,7 +41,7 @@ func (s *PreWorkflowHook) MarshalJSON() ([]byte, error) { return json.Marshal(out) } -func (s PreWorkflowHook) Validate() error { +func (s WorkflowHook) Validate() error { runStep := func(value interface{}) error { elem := value.(map[string]string) var keys []string @@ -69,13 +69,13 @@ func (s PreWorkflowHook) Validate() error { return errors.New("step element is empty") } -func (s PreWorkflowHook) ToValid() *valid.PreWorkflowHook { - // This will trigger in case #4 (see PreWorkflowHook docs). +func (s WorkflowHook) ToValid() *valid.WorkflowHook { + // This will trigger in case #4 (see WorkflowHook 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. for _, v := range s.StringVal { - return &valid.PreWorkflowHook{ + return &valid.WorkflowHook{ StepName: RunStepName, RunCommand: v, } @@ -89,7 +89,7 @@ func (s PreWorkflowHook) ToValid() *valid.PreWorkflowHook { // a step a custom run step: " - run: my custom command" // It takes a parameter unmarshal that is a function that tries to unmarshal // the current element into a given object. -func (s *PreWorkflowHook) unmarshalGeneric(unmarshal func(interface{}) error) error { +func (s *WorkflowHook) unmarshalGeneric(unmarshal func(interface{}) error) error { // Try to unmarshal as a custom run step, ex. // repo_config: // - run: my command @@ -104,7 +104,7 @@ func (s *PreWorkflowHook) unmarshalGeneric(unmarshal func(interface{}) error) er return err } -func (s PreWorkflowHook) marshalGeneric() (interface{}, error) { +func (s WorkflowHook) marshalGeneric() (interface{}, error) { if len(s.StringVal) != 0 { return s.StringVal, nil } diff --git a/server/events/yaml/raw/pre_workflow_step_test.go b/server/events/yaml/raw/workflow_step_test.go similarity index 82% rename from server/events/yaml/raw/pre_workflow_step_test.go rename to server/events/yaml/raw/workflow_step_test.go index babbc6a38f..9d69ed1df4 100644 --- a/server/events/yaml/raw/pre_workflow_step_test.go +++ b/server/events/yaml/raw/workflow_step_test.go @@ -9,11 +9,11 @@ import ( yaml "gopkg.in/yaml.v2" ) -func TestPreWorkflowHook_YAMLMarshalling(t *testing.T) { +func TestWorkflowHook_YAMLMarshalling(t *testing.T) { cases := []struct { description string input string - exp raw.PreWorkflowHook + exp raw.WorkflowHook expErr string }{ // Run-step style @@ -21,7 +21,7 @@ func TestPreWorkflowHook_YAMLMarshalling(t *testing.T) { description: "run step", input: ` run: my command`, - exp: raw.PreWorkflowHook{ + exp: raw.WorkflowHook{ StringVal: map[string]string{ "run": "my command", }, @@ -32,7 +32,7 @@ run: my command`, input: ` run: my command key: value`, - exp: raw.PreWorkflowHook{ + exp: raw.WorkflowHook{ StringVal: map[string]string{ "run": "my command", "key": "value", @@ -53,7 +53,7 @@ key: for _, c := range cases { t.Run(c.description, func(t *testing.T) { - var got raw.PreWorkflowHook + var got raw.WorkflowHook err := yaml.UnmarshalStrict([]byte(c.input), &got) if c.expErr != "" { ErrEquals(t, c.expErr, err) @@ -65,7 +65,7 @@ key: _, err = yaml.Marshal(got) Ok(t, err) - var got2 raw.PreWorkflowHook + var got2 raw.WorkflowHook err = yaml.UnmarshalStrict([]byte(c.input), &got2) Ok(t, err) Equals(t, got2, got) @@ -76,12 +76,12 @@ key: func TestGlobalConfigStep_Validate(t *testing.T) { cases := []struct { description string - input raw.PreWorkflowHook + input raw.WorkflowHook expErr string }{ { description: "run step", - input: raw.PreWorkflowHook{ + input: raw.WorkflowHook{ StringVal: map[string]string{ "run": "my command", }, @@ -90,7 +90,7 @@ func TestGlobalConfigStep_Validate(t *testing.T) { }, { description: "invalid key in string val", - input: raw.PreWorkflowHook{ + input: raw.WorkflowHook{ StringVal: map[string]string{ "invalid": "", }, @@ -101,7 +101,7 @@ func TestGlobalConfigStep_Validate(t *testing.T) { // For atlantis.yaml v2, this wouldn't parse, but now there should // be no error. description: "unparseable shell command", - input: raw.PreWorkflowHook{ + input: raw.WorkflowHook{ StringVal: map[string]string{ "run": "my 'c", }, @@ -120,20 +120,20 @@ func TestGlobalConfigStep_Validate(t *testing.T) { } } -func TestPreWorkflowHook_ToValid(t *testing.T) { +func TestWorkflowHook_ToValid(t *testing.T) { cases := []struct { description string - input raw.PreWorkflowHook - exp *valid.PreWorkflowHook + input raw.WorkflowHook + exp *valid.WorkflowHook }{ { description: "run step", - input: raw.PreWorkflowHook{ + input: raw.WorkflowHook{ StringVal: map[string]string{ "run": "my 'run command'", }, }, - exp: &valid.PreWorkflowHook{ + exp: &valid.WorkflowHook{ StepName: "run", RunCommand: "my 'run command'", }, diff --git a/server/events/yaml/valid/global_cfg.go b/server/events/yaml/valid/global_cfg.go index c2c4ced7db..17eb265a9b 100644 --- a/server/events/yaml/valid/global_cfg.go +++ b/server/events/yaml/valid/global_cfg.go @@ -16,6 +16,7 @@ const PoliciesPassedApplyReq = "policies_passed" const ApplyRequirementsKey = "apply_requirements" const PreWorkflowHooksKey = "pre_workflow_hooks" const WorkflowKey = "workflow" +const PostWorkflowHooksKey = "post_workflow_hooks" const AllowedWorkflowsKey = "allowed_workflows" const AllowedOverridesKey = "allowed_overrides" const AllowCustomWorkflowsKey = "allow_custom_workflows" @@ -46,8 +47,9 @@ type Repo struct { IDRegex *regexp.Regexp BranchRegex *regexp.Regexp ApplyRequirements []string - PreWorkflowHooks []*PreWorkflowHook + PreWorkflowHooks []*WorkflowHook Workflow *Workflow + PostWorkflowHooks []*WorkflowHook AllowedWorkflows []string AllowedOverrides []string AllowCustomWorkflows *bool @@ -69,8 +71,8 @@ type MergedProjectCfg struct { DeleteSourceBranchOnMerge bool } -// PreWorkflowHook is a map of custom run commands to run before workflows. -type PreWorkflowHook struct { +// WorkflowHook is a map of custom run commands to run before or after workflows. +type WorkflowHook struct { StepName string RunCommand string } @@ -109,13 +111,14 @@ var DefaultPlanStage = Stage{ } // Deprecated: use NewGlobalCfgFromArgs -func NewGlobalCfgWithHooks(allowRepoCfg bool, mergeableReq bool, approvedReq bool, unDivergedReq bool, preWorkflowHooks []*PreWorkflowHook) GlobalCfg { +func NewGlobalCfgWithHooks(allowRepoCfg bool, mergeableReq bool, approvedReq bool, unDivergedReq bool, preWorkflowHooks []*WorkflowHook, postWorkflowHooks []*WorkflowHook) GlobalCfg { return NewGlobalCfgFromArgs(GlobalCfgArgs{ - AllowRepoCfg: allowRepoCfg, - MergeableReq: mergeableReq, - ApprovedReq: approvedReq, - UnDivergedReq: unDivergedReq, - PreWorkflowHooks: preWorkflowHooks, + AllowRepoCfg: allowRepoCfg, + MergeableReq: mergeableReq, + ApprovedReq: approvedReq, + UnDivergedReq: unDivergedReq, + PreWorkflowHooks: preWorkflowHooks, + PostWorkflowHooks: postWorkflowHooks, }) } @@ -140,7 +143,8 @@ type GlobalCfgArgs struct { ApprovedReq bool UnDivergedReq bool PolicyCheckEnabled bool - PreWorkflowHooks []*PreWorkflowHook + PreWorkflowHooks []*WorkflowHook + PostWorkflowHooks []*WorkflowHook } func NewGlobalCfgFromArgs(args GlobalCfgArgs) GlobalCfg { @@ -184,6 +188,7 @@ func NewGlobalCfgFromArgs(args GlobalCfgArgs) GlobalCfg { ApplyRequirements: applyReqs, PreWorkflowHooks: args.PreWorkflowHooks, Workflow: &defaultWorkflow, + PostWorkflowHooks: args.PostWorkflowHooks, AllowedWorkflows: allowedWorkflows, AllowedOverrides: allowedOverrides, AllowCustomWorkflows: &allowCustomWorkflows, diff --git a/server/server.go b/server/server.go index f77b6dd8cf..a0ecd14f2b 100644 --- a/server/server.go +++ b/server/server.go @@ -79,24 +79,25 @@ const ( // Server runs the Atlantis web server. type Server struct { - AtlantisVersion string - AtlantisURL *url.URL - Router *mux.Router - Port int - PreWorkflowHooksCommandRunner *events.DefaultPreWorkflowHooksCommandRunner - CommandRunner *events.DefaultCommandRunner - Logger logging.SimpleLogging - Locker locking.Locker - ApplyLocker locking.ApplyLocker - VCSEventsController *events_controllers.VCSEventsController - GithubAppController *controllers.GithubAppController - LocksController *controllers.LocksController - StatusController *controllers.StatusController - IndexTemplate templates.TemplateWriter - LockDetailTemplate templates.TemplateWriter - SSLCertFile string - SSLKeyFile string - Drainer *events.Drainer + AtlantisVersion string + AtlantisURL *url.URL + Router *mux.Router + Port int + PostWorkflowHooksCommandRunner *events.DefaultPostWorkflowHooksCommandRunner + PreWorkflowHooksCommandRunner *events.DefaultPreWorkflowHooksCommandRunner + CommandRunner *events.DefaultCommandRunner + Logger logging.SimpleLogging + Locker locking.Locker + ApplyLocker locking.ApplyLocker + VCSEventsController *events_controllers.VCSEventsController + GithubAppController *controllers.GithubAppController + LocksController *controllers.LocksController + StatusController *controllers.StatusController + IndexTemplate templates.TemplateWriter + LockDetailTemplate templates.TemplateWriter + SSLCertFile string + SSLKeyFile string + Drainer *events.Drainer } // Config holds config for server that isn't passed in by the user. @@ -419,6 +420,13 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { WorkingDir: workingDir, PreWorkflowHookRunner: runtime.DefaultPreWorkflowHookRunner{}, } + postWorkflowHooksCommandRunner := &events.DefaultPostWorkflowHooksCommandRunner{ + VCSClient: vcsClient, + GlobalCfg: globalCfg, + WorkingDirLocker: workingDirLocker, + WorkingDir: workingDir, + PostWorkflowHookRunner: runtime.DefaultPostWorkflowHookRunner{}, + } projectCommandBuilder := events.NewProjectCommandBuilder( policyChecksEnabled, validator, @@ -561,21 +569,22 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { } commandRunner := &events.DefaultCommandRunner{ - VCSClient: vcsClient, - GithubPullGetter: githubClient, - GitlabMergeRequestGetter: gitlabClient, - AzureDevopsPullGetter: azuredevopsClient, - CommentCommandRunnerByCmd: commentCommandRunnerByCmd, - EventParser: eventParser, - Logger: logger, - AllowForkPRs: userConfig.AllowForkPRs, - AllowForkPRsFlag: config.AllowForkPRsFlag, - SilenceForkPRErrors: userConfig.SilenceForkPRErrors, - SilenceForkPRErrorsFlag: config.SilenceForkPRErrorsFlag, - DisableAutoplan: userConfig.DisableAutoplan, - Drainer: drainer, - PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner, - PullStatusFetcher: boltdb, + VCSClient: vcsClient, + GithubPullGetter: githubClient, + GitlabMergeRequestGetter: gitlabClient, + AzureDevopsPullGetter: azuredevopsClient, + CommentCommandRunnerByCmd: commentCommandRunnerByCmd, + EventParser: eventParser, + Logger: logger, + AllowForkPRs: userConfig.AllowForkPRs, + AllowForkPRsFlag: config.AllowForkPRsFlag, + SilenceForkPRErrors: userConfig.SilenceForkPRErrors, + SilenceForkPRErrorsFlag: config.SilenceForkPRErrorsFlag, + DisableAutoplan: userConfig.DisableAutoplan, + Drainer: drainer, + PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner, + PostWorkflowHooksCommandRunner: postWorkflowHooksCommandRunner, + PullStatusFetcher: boltdb, } repoAllowlist, err := events.NewRepoAllowlistChecker(userConfig.RepoAllowlist) if err != nil { @@ -623,24 +632,25 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { } return &Server{ - AtlantisVersion: config.AtlantisVersion, - AtlantisURL: parsedURL, - Router: underlyingRouter, - Port: userConfig.Port, - PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner, - CommandRunner: commandRunner, - Logger: logger, - Locker: lockingClient, - ApplyLocker: applyLockingClient, - VCSEventsController: eventsController, - GithubAppController: githubAppController, - LocksController: locksController, - StatusController: statusController, - IndexTemplate: templates.IndexTemplate, - LockDetailTemplate: templates.LockTemplate, - SSLKeyFile: userConfig.SSLKeyFile, - SSLCertFile: userConfig.SSLCertFile, - Drainer: drainer, + AtlantisVersion: config.AtlantisVersion, + AtlantisURL: parsedURL, + Router: underlyingRouter, + Port: userConfig.Port, + PostWorkflowHooksCommandRunner: postWorkflowHooksCommandRunner, + PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner, + CommandRunner: commandRunner, + Logger: logger, + Locker: lockingClient, + ApplyLocker: applyLockingClient, + VCSEventsController: eventsController, + GithubAppController: githubAppController, + LocksController: locksController, + StatusController: statusController, + IndexTemplate: templates.IndexTemplate, + LockDetailTemplate: templates.LockTemplate, + SSLKeyFile: userConfig.SSLKeyFile, + SSLCertFile: userConfig.SSLCertFile, + Drainer: drainer, }, nil } From 8c4eb2c21e0d4699c292af5d822fdbfc06e9107e Mon Sep 17 00:00:00 2001 From: tim775 <52185+tim775@users.noreply.github.com> Date: Fri, 7 Jan 2022 17:07:01 -0500 Subject: [PATCH 2/2] docs: Add cost estimation as post workflow use case --- runatlantis.io/docs/post-workflow-hooks.md | 42 +++--- runatlantis.io/docs/pre-workflow-hooks.md | 6 +- .../events/events_controller_e2e_test.go | 26 ++-- .../models_preworkflowhookcommandcontext.go | 33 ----- .../models_workflowhookcommandcontext.go | 0 .../mocks/mock_post_workflows_hook_runner.go | 2 +- .../mocks/mock_pull_approved_checker.go | 12 +- .../runtime/post_workflow_hook_runner.go | 0 .../runtime/post_workflow_hook_runner_test.go | 6 +- server/events/command_runner.go | 12 +- server/events/command_runner_test.go | 26 ++-- .../post_workflow_hooks_command_runner.go | 2 +- ...post_workflow_hooks_command_runner_test.go | 3 +- server/server.go | 122 +++++++++--------- 14 files changed, 136 insertions(+), 156 deletions(-) delete mode 100644 server/core/runtime/mocks/matchers/models_preworkflowhookcommandcontext.go rename server/{events => core}/runtime/mocks/matchers/models_workflowhookcommandcontext.go (100%) rename server/{events => core}/runtime/mocks/mock_post_workflows_hook_runner.go (97%) rename server/{events => core}/runtime/post_workflow_hook_runner.go (100%) rename server/{events => core}/runtime/post_workflow_hook_runner_test.go (93%) diff --git a/runatlantis.io/docs/post-workflow-hooks.md b/runatlantis.io/docs/post-workflow-hooks.md index 6258e89491..66c4972e34 100644 --- a/runatlantis.io/docs/post-workflow-hooks.md +++ b/runatlantis.io/docs/post-workflow-hooks.md @@ -1,9 +1,9 @@ # Post Workflow Hooks Post workflow hooks can be defined to run scripts after default or custom -workflows are executed. Pre workflow hooks differ from [custom -workflows](custom-workflows.html#custom-run-command) in that they are ran -outside of Atlantis commands. Which means they do not surface their output +workflows are executed. Post workflow hooks differ from [custom +workflows](custom-workflows.html#custom-run-command) in that they are run +outside of Atlantis commands. Which means they do not surface their output back to the PR as a comment. [[toc]] @@ -15,25 +15,38 @@ Post workflow hooks can only be specified in the Server-Side Repo Config under ## Use Cases -### Deleting files that hold credenatils +### Cost estimation reporting -If you generate files that hold credentials needed by your Terraform as part of either a -[pre workflow hooks](pre-workflow-hooks.html) or as part of a [custom workflow](custom-workflows.html) -you may want to make sure it gets deleted even if your main workflow has failed +You can add a post workflow hook to perform custom reporting after all workflows +have finished. + +In this example we use a custom workflow to generate cost estimates for each +workflow, then create a summary report after all workflows have completed. ```yaml +# repos.yaml +workflows: + myworkflow: + plan: + steps: + - init + - plan + - run: infracost breakdown --path=$PLANFILE --format=json --out-file=/tmp/$BASE_REPO_OWNER-$BASE_REPO_NAME-$PULL_NUM-$WORKSPACE-$REPO_REL_DIR-infracost.json repos: - - id: /.*/ - post_workflow_hooks: - - run: rm $DIR/asupersecretfile + - id: /.*/ + workflow: myworkflow + post_workflow_hooks: + - run: infracost output --path=/tmp/$BASE_REPO_OWNER-$BASE_REPO_NAME-$PULL_NUM-*-infracost.json --format=github-comment --out-file=/tmp/infracost-comment.md + # Now report the output as desired, e.g. post to GitHub as a comment. + # ... ``` -### Reference +## Reference -#### Custom `run` Command +### Custom `run` Command This is very similar to [custom workflow run -command](custom-workflows.html#custom-run-command). +command](custom-workflows.html#custom-run-command). ```yaml - run: custom-command @@ -44,7 +57,6 @@ command](custom-workflows.html#custom-run-command). | run | string | none | no | Run a custom command | ::: tip Notes - * `run` commands are executed with the following environment variables: * `BASE_REPO_NAME` - Name of the repository that the pull request will be merged into, ex. `atlantis`. * `BASE_REPO_OWNER` - Owner of the repository that the pull request will be merged into, ex. `runatlantis`. @@ -55,6 +67,6 @@ command](custom-workflows.html#custom-run-command). * `BASE_BRANCH_NAME` - Name of the base branch of the pull request (the branch that the pull request is getting merged into) * `PULL_NUM` - Pull request number or ID, ex. `2`. * `PULL_AUTHOR` - Username of the pull request author, ex. `acme-user`. - * `DIR` - The absolute path to the root of the cloned repository. + * `DIR` - The absolute path to the root of the cloned repository. * `USER_NAME` - Username of the VCS user running command, ex. `acme-user`. During an autoplan, the user will be the Atlantis API user, ex. `atlantis`. ::: diff --git a/runatlantis.io/docs/pre-workflow-hooks.md b/runatlantis.io/docs/pre-workflow-hooks.md index e94d927197..a84f91e9c6 100644 --- a/runatlantis.io/docs/pre-workflow-hooks.md +++ b/runatlantis.io/docs/pre-workflow-hooks.md @@ -6,7 +6,7 @@ workflows](custom-workflows.html#custom-run-command) in several ways. 1. Pre workflow hooks do not require for repository configuration to be present. This be utilized to [dynamically generate repo configs](pre-workflow-hooks.html#dynamic-repo-config-generation). -2. Pre workflow hooks are ran outside of Atlantis commands. Which means +2. Pre workflow hooks are run outside of Atlantis commands. Which means they do not surface their output back to the PR as a comment. [[toc]] @@ -31,8 +31,8 @@ repos: pre_workflow_hooks: - run: ./repo-config-generator.sh ``` -### Reference -#### Custom `run` Command +## Reference +### Custom `run` Command This is very similar to [custom workflow run command](custom-workflows.html#custom-run-command). ```yaml diff --git a/server/controllers/events/events_controller_e2e_test.go b/server/controllers/events/events_controller_e2e_test.go index 23f9892867..dfdac57253 100644 --- a/server/controllers/events/events_controller_e2e_test.go +++ b/server/controllers/events/events_controller_e2e_test.go @@ -599,7 +599,7 @@ func TestSimlpleWorkflow_terraformLockFile(t *testing.T) { } // Let's verify the pre-workflow hook was called for each comment including the pull request opened event - mockPreWorkflowHookRunner.VerifyWasCalled(Times(2)).Run(runtimematchers.AnyModelsPreWorkflowHookCommandContext(), EqString("some dummy command"), AnyString()) + mockPreWorkflowHookRunner.VerifyWasCalled(Times(2)).Run(runtimematchers.AnyModelsWorkflowHookCommandContext(), EqString("some dummy command"), AnyString()) // Now we're ready to verify Atlantis made all the comments back (or // replies) that we expect. We expect each plan to have 1 comment, @@ -1059,19 +1059,19 @@ func setupE2E(t *testing.T, repoDir string) (events_controllers.VCSEventsControl } commandRunner := &events.DefaultCommandRunner{ - EventParser: eventParser, - VCSClient: e2eVCSClient, - GithubPullGetter: e2eGithubGetter, - GitlabMergeRequestGetter: e2eGitlabGetter, - Logger: logger, - GlobalCfg: globalCfg, - AllowForkPRs: allowForkPRs, - AllowForkPRsFlag: "allow-fork-prs", - CommentCommandRunnerByCmd: commentCommandRunnerByCmd, - Drainer: drainer, - PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner, + EventParser: eventParser, + VCSClient: e2eVCSClient, + GithubPullGetter: e2eGithubGetter, + GitlabMergeRequestGetter: e2eGitlabGetter, + Logger: logger, + GlobalCfg: globalCfg, + AllowForkPRs: allowForkPRs, + AllowForkPRsFlag: "allow-fork-prs", + CommentCommandRunnerByCmd: commentCommandRunnerByCmd, + Drainer: drainer, + PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner, PostWorkflowHooksCommandRunner: postWorkflowHooksCommandRunner, - PullStatusFetcher: boltdb, + PullStatusFetcher: boltdb, } repoAllowlistChecker, err := events.NewRepoAllowlistChecker("*") diff --git a/server/core/runtime/mocks/matchers/models_preworkflowhookcommandcontext.go b/server/core/runtime/mocks/matchers/models_preworkflowhookcommandcontext.go deleted file mode 100644 index 8a57120c7c..0000000000 --- a/server/core/runtime/mocks/matchers/models_preworkflowhookcommandcontext.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code generated by pegomock. DO NOT EDIT. -package matchers - -import ( - "github.com/petergtz/pegomock" - "reflect" - - models "github.com/runatlantis/atlantis/server/events/models" -) - -func AnyModelsPreWorkflowHookCommandContext() models.PreWorkflowHookCommandContext { - pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(models.PreWorkflowHookCommandContext))(nil)).Elem())) - var nullValue models.PreWorkflowHookCommandContext - return nullValue -} - -func EqModelsPreWorkflowHookCommandContext(value models.PreWorkflowHookCommandContext) models.PreWorkflowHookCommandContext { - pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) - var nullValue models.PreWorkflowHookCommandContext - return nullValue -} - -func NotEqModelsPreWorkflowHookCommandContext(value models.PreWorkflowHookCommandContext) models.PreWorkflowHookCommandContext { - pegomock.RegisterMatcher(&pegomock.NotEqMatcher{Value: value}) - var nullValue models.PreWorkflowHookCommandContext - return nullValue -} - -func ModelsPreWorkflowHookCommandContextThat(matcher pegomock.ArgumentMatcher) models.PreWorkflowHookCommandContext { - pegomock.RegisterMatcher(matcher) - var nullValue models.PreWorkflowHookCommandContext - return nullValue -} diff --git a/server/events/runtime/mocks/matchers/models_workflowhookcommandcontext.go b/server/core/runtime/mocks/matchers/models_workflowhookcommandcontext.go similarity index 100% rename from server/events/runtime/mocks/matchers/models_workflowhookcommandcontext.go rename to server/core/runtime/mocks/matchers/models_workflowhookcommandcontext.go diff --git a/server/events/runtime/mocks/mock_post_workflows_hook_runner.go b/server/core/runtime/mocks/mock_post_workflows_hook_runner.go similarity index 97% rename from server/events/runtime/mocks/mock_post_workflows_hook_runner.go rename to server/core/runtime/mocks/mock_post_workflows_hook_runner.go index 9dcdc738b1..018a093dce 100644 --- a/server/events/runtime/mocks/mock_post_workflows_hook_runner.go +++ b/server/core/runtime/mocks/mock_post_workflows_hook_runner.go @@ -1,5 +1,5 @@ // Code generated by pegomock. DO NOT EDIT. -// Source: github.com/runatlantis/atlantis/server/events/runtime (interfaces: PostWorkflowHookRunner) +// Source: github.com/runatlantis/atlantis/server/core/runtime (interfaces: PostWorkflowHookRunner) package mocks diff --git a/server/core/runtime/mocks/mock_pull_approved_checker.go b/server/core/runtime/mocks/mock_pull_approved_checker.go index a71dd3506c..39eeb63317 100644 --- a/server/core/runtime/mocks/mock_pull_approved_checker.go +++ b/server/core/runtime/mocks/mock_pull_approved_checker.go @@ -25,11 +25,11 @@ func NewMockPullApprovedChecker(options ...pegomock.Option) *MockPullApprovedChe func (mock *MockPullApprovedChecker) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } func (mock *MockPullApprovedChecker) FailHandler() pegomock.FailHandler { return mock.fail } -func (mock *MockPullApprovedChecker) PullIsApproved(_param0 models.Repo, _param1 models.PullRequest) (models.ApprovalStatus, error) { +func (mock *MockPullApprovedChecker) PullIsApproved(baseRepo models.Repo, pull models.PullRequest) (models.ApprovalStatus, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockPullApprovedChecker().") } - params := []pegomock.Param{_param0, _param1} + params := []pegomock.Param{baseRepo, pull} result := pegomock.GetGenericMockFrom(mock).Invoke("PullIsApproved", params, []reflect.Type{reflect.TypeOf((*models.ApprovalStatus)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) var ret0 models.ApprovalStatus var ret1 error @@ -81,8 +81,8 @@ type VerifierMockPullApprovedChecker struct { timeout time.Duration } -func (verifier *VerifierMockPullApprovedChecker) PullIsApproved(_param0 models.Repo, _param1 models.PullRequest) *MockPullApprovedChecker_PullIsApproved_OngoingVerification { - params := []pegomock.Param{_param0, _param1} +func (verifier *VerifierMockPullApprovedChecker) PullIsApproved(baseRepo models.Repo, pull models.PullRequest) *MockPullApprovedChecker_PullIsApproved_OngoingVerification { + params := []pegomock.Param{baseRepo, pull} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "PullIsApproved", params, verifier.timeout) return &MockPullApprovedChecker_PullIsApproved_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } @@ -93,8 +93,8 @@ type MockPullApprovedChecker_PullIsApproved_OngoingVerification struct { } func (c *MockPullApprovedChecker_PullIsApproved_OngoingVerification) GetCapturedArguments() (models.Repo, models.PullRequest) { - _param0, _param1 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1], _param1[len(_param1)-1] + baseRepo, pull := c.GetAllCapturedArguments() + return baseRepo[len(baseRepo)-1], pull[len(pull)-1] } func (c *MockPullApprovedChecker_PullIsApproved_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo, _param1 []models.PullRequest) { diff --git a/server/events/runtime/post_workflow_hook_runner.go b/server/core/runtime/post_workflow_hook_runner.go similarity index 100% rename from server/events/runtime/post_workflow_hook_runner.go rename to server/core/runtime/post_workflow_hook_runner.go diff --git a/server/events/runtime/post_workflow_hook_runner_test.go b/server/core/runtime/post_workflow_hook_runner_test.go similarity index 93% rename from server/events/runtime/post_workflow_hook_runner_test.go rename to server/core/runtime/post_workflow_hook_runner_test.go index 8926df28b3..32a17a7702 100644 --- a/server/events/runtime/post_workflow_hook_runner_test.go +++ b/server/core/runtime/post_workflow_hook_runner_test.go @@ -5,11 +5,11 @@ import ( "testing" . "github.com/petergtz/pegomock" + "github.com/runatlantis/atlantis/server/core/runtime" + "github.com/runatlantis/atlantis/server/core/terraform/mocks" + matchers2 "github.com/runatlantis/atlantis/server/core/terraform/mocks/matchers" "github.com/runatlantis/atlantis/server/events/mocks/matchers" "github.com/runatlantis/atlantis/server/events/models" - "github.com/runatlantis/atlantis/server/events/runtime" - "github.com/runatlantis/atlantis/server/events/terraform/mocks" - matchers2 "github.com/runatlantis/atlantis/server/events/terraform/mocks/matchers" "github.com/runatlantis/atlantis/server/logging" . "github.com/runatlantis/atlantis/testing" ) diff --git a/server/events/command_runner.go b/server/events/command_runner.go index b986a6de0c..13fd5c4ed2 100644 --- a/server/events/command_runner.go +++ b/server/events/command_runner.go @@ -111,13 +111,13 @@ type DefaultCommandRunner struct { // SilenceForkPRErrorsFlag is the name of the flag that controls fork PR's. We use // this in our error message back to the user on a forked PR so they know // how to disable error comment - SilenceForkPRErrorsFlag string - CommentCommandRunnerByCmd map[models.CommandName]CommentCommandRunner - Drainer *Drainer - PreWorkflowHooksCommandRunner PreWorkflowHooksCommandRunner + SilenceForkPRErrorsFlag string + CommentCommandRunnerByCmd map[models.CommandName]CommentCommandRunner + Drainer *Drainer + PreWorkflowHooksCommandRunner PreWorkflowHooksCommandRunner PostWorkflowHooksCommandRunner PostWorkflowHooksCommandRunner - PullStatusFetcher PullStatusFetcher - TeamAllowlistChecker *TeamAllowlistChecker + PullStatusFetcher PullStatusFetcher + TeamAllowlistChecker *TeamAllowlistChecker } // RunAutoplanCommand runs plan and policy_checks when a pull request is opened or updated. diff --git a/server/events/command_runner_test.go b/server/events/command_runner_test.go index 681f7b5c08..637b9155b4 100644 --- a/server/events/command_runner_test.go +++ b/server/events/command_runner_test.go @@ -194,20 +194,20 @@ func setup(t *testing.T) *vcsmocks.MockClient { globalCfg := valid.NewGlobalCfgFromArgs(valid.GlobalCfgArgs{}) ch = events.DefaultCommandRunner{ - VCSClient: vcsClient, - CommentCommandRunnerByCmd: commentCommandRunnerByCmd, - EventParser: eventParsing, - GithubPullGetter: githubGetter, - GitlabMergeRequestGetter: gitlabGetter, - AzureDevopsPullGetter: azuredevopsGetter, - Logger: logger, - GlobalCfg: globalCfg, - AllowForkPRs: false, - AllowForkPRsFlag: "allow-fork-prs-flag", - Drainer: drainer, - PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner, + VCSClient: vcsClient, + CommentCommandRunnerByCmd: commentCommandRunnerByCmd, + EventParser: eventParsing, + GithubPullGetter: githubGetter, + GitlabMergeRequestGetter: gitlabGetter, + AzureDevopsPullGetter: azuredevopsGetter, + Logger: logger, + GlobalCfg: globalCfg, + AllowForkPRs: false, + AllowForkPRsFlag: "allow-fork-prs-flag", + Drainer: drainer, + PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner, PostWorkflowHooksCommandRunner: postWorkflowHooksCommandRunner, - PullStatusFetcher: defaultBoltDB, + PullStatusFetcher: defaultBoltDB, } return vcsClient } diff --git a/server/events/post_workflow_hooks_command_runner.go b/server/events/post_workflow_hooks_command_runner.go index 1307defe3f..56666de3e6 100644 --- a/server/events/post_workflow_hooks_command_runner.go +++ b/server/events/post_workflow_hooks_command_runner.go @@ -1,8 +1,8 @@ package events import ( + "github.com/runatlantis/atlantis/server/core/runtime" "github.com/runatlantis/atlantis/server/events/models" - "github.com/runatlantis/atlantis/server/events/runtime" "github.com/runatlantis/atlantis/server/events/vcs" "github.com/runatlantis/atlantis/server/events/yaml/valid" ) diff --git a/server/events/post_workflow_hooks_command_runner_test.go b/server/events/post_workflow_hooks_command_runner_test.go index 3aef3ca9ab..9c615bc2da 100644 --- a/server/events/post_workflow_hooks_command_runner_test.go +++ b/server/events/post_workflow_hooks_command_runner_test.go @@ -4,12 +4,13 @@ import ( "errors" "testing" + runtime_mocks "github.com/runatlantis/atlantis/server/core/runtime/mocks" + . "github.com/petergtz/pegomock" "github.com/runatlantis/atlantis/server/events" "github.com/runatlantis/atlantis/server/events/mocks" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/events/models/fixtures" - runtime_mocks "github.com/runatlantis/atlantis/server/events/runtime/mocks" vcsmocks "github.com/runatlantis/atlantis/server/events/vcs/mocks" "github.com/runatlantis/atlantis/server/events/yaml/valid" "github.com/runatlantis/atlantis/server/logging" diff --git a/server/server.go b/server/server.go index c7432b3252..49319433c3 100644 --- a/server/server.go +++ b/server/server.go @@ -81,29 +81,29 @@ const ( // Server runs the Atlantis web server. type Server struct { - AtlantisVersion string - AtlantisURL *url.URL - Router *mux.Router - Port int + AtlantisVersion string + AtlantisURL *url.URL + Router *mux.Router + Port int PostWorkflowHooksCommandRunner *events.DefaultPostWorkflowHooksCommandRunner - PreWorkflowHooksCommandRunner *events.DefaultPreWorkflowHooksCommandRunner - CommandRunner *events.DefaultCommandRunner - Logger logging.SimpleLogging - Locker locking.Locker - ApplyLocker locking.ApplyLocker - VCSEventsController *events_controllers.VCSEventsController - GithubAppController *controllers.GithubAppController - LocksController *controllers.LocksController - StatusController *controllers.StatusController - JobsController *controllers.JobsController - IndexTemplate templates.TemplateWriter - LockDetailTemplate templates.TemplateWriter - ProjectJobsTemplate templates.TemplateWriter - ProjectJobsErrorTemplate templates.TemplateWriter - SSLCertFile string - SSLKeyFile string - Drainer *events.Drainer - ProjectCmdOutputHandler handlers.ProjectCommandOutputHandler + PreWorkflowHooksCommandRunner *events.DefaultPreWorkflowHooksCommandRunner + CommandRunner *events.DefaultCommandRunner + Logger logging.SimpleLogging + Locker locking.Locker + ApplyLocker locking.ApplyLocker + VCSEventsController *events_controllers.VCSEventsController + GithubAppController *controllers.GithubAppController + LocksController *controllers.LocksController + StatusController *controllers.StatusController + JobsController *controllers.JobsController + IndexTemplate templates.TemplateWriter + LockDetailTemplate templates.TemplateWriter + ProjectJobsTemplate templates.TemplateWriter + ProjectJobsErrorTemplate templates.TemplateWriter + SSLCertFile string + SSLKeyFile string + Drainer *events.Drainer + ProjectCmdOutputHandler handlers.ProjectCommandOutputHandler } // Config holds config for server that isn't passed in by the user. @@ -640,24 +640,24 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { } commandRunner := &events.DefaultCommandRunner{ - VCSClient: vcsClient, - GithubPullGetter: githubClient, - GitlabMergeRequestGetter: gitlabClient, - AzureDevopsPullGetter: azuredevopsClient, - CommentCommandRunnerByCmd: commentCommandRunnerByCmd, - EventParser: eventParser, - Logger: logger, - GlobalCfg: globalCfg, - AllowForkPRs: userConfig.AllowForkPRs, - AllowForkPRsFlag: config.AllowForkPRsFlag, - SilenceForkPRErrors: userConfig.SilenceForkPRErrors, - SilenceForkPRErrorsFlag: config.SilenceForkPRErrorsFlag, - DisableAutoplan: userConfig.DisableAutoplan, - Drainer: drainer, - PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner, + VCSClient: vcsClient, + GithubPullGetter: githubClient, + GitlabMergeRequestGetter: gitlabClient, + AzureDevopsPullGetter: azuredevopsClient, + CommentCommandRunnerByCmd: commentCommandRunnerByCmd, + EventParser: eventParser, + Logger: logger, + GlobalCfg: globalCfg, + AllowForkPRs: userConfig.AllowForkPRs, + AllowForkPRsFlag: config.AllowForkPRsFlag, + SilenceForkPRErrors: userConfig.SilenceForkPRErrors, + SilenceForkPRErrorsFlag: config.SilenceForkPRErrorsFlag, + DisableAutoplan: userConfig.DisableAutoplan, + Drainer: drainer, + PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner, PostWorkflowHooksCommandRunner: postWorkflowHooksCommandRunner, - PullStatusFetcher: boltdb, - TeamAllowlistChecker: githubTeamAllowlistChecker, + PullStatusFetcher: boltdb, + TeamAllowlistChecker: githubTeamAllowlistChecker, } repoAllowlist, err := events.NewRepoAllowlistChecker(userConfig.RepoAllowlist) if err != nil { @@ -721,29 +721,29 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { GithubOrg: userConfig.GithubOrg, } return &Server{ - AtlantisVersion: config.AtlantisVersion, - AtlantisURL: parsedURL, - Router: underlyingRouter, - Port: userConfig.Port, + AtlantisVersion: config.AtlantisVersion, + AtlantisURL: parsedURL, + Router: underlyingRouter, + Port: userConfig.Port, PostWorkflowHooksCommandRunner: postWorkflowHooksCommandRunner, - PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner, - CommandRunner: commandRunner, - Logger: logger, - Locker: lockingClient, - ApplyLocker: applyLockingClient, - VCSEventsController: eventsController, - GithubAppController: githubAppController, - LocksController: locksController, - JobsController: jobsController, - StatusController: statusController, - IndexTemplate: templates.IndexTemplate, - LockDetailTemplate: templates.LockTemplate, - ProjectJobsTemplate: templates.ProjectJobsTemplate, - ProjectJobsErrorTemplate: templates.ProjectJobsErrorTemplate, - SSLKeyFile: userConfig.SSLKeyFile, - SSLCertFile: userConfig.SSLCertFile, - Drainer: drainer, - ProjectCmdOutputHandler: projectCmdOutputHandler, + PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner, + CommandRunner: commandRunner, + Logger: logger, + Locker: lockingClient, + ApplyLocker: applyLockingClient, + VCSEventsController: eventsController, + GithubAppController: githubAppController, + LocksController: locksController, + JobsController: jobsController, + StatusController: statusController, + IndexTemplate: templates.IndexTemplate, + LockDetailTemplate: templates.LockTemplate, + ProjectJobsTemplate: templates.ProjectJobsTemplate, + ProjectJobsErrorTemplate: templates.ProjectJobsErrorTemplate, + SSLKeyFile: userConfig.SSLKeyFile, + SSLCertFile: userConfig.SSLCertFile, + Drainer: drainer, + ProjectCmdOutputHandler: projectCmdOutputHandler, }, nil }