From d3ec832effdff19a9d83df2c4ab07a20a9f8040c Mon Sep 17 00:00:00 2001 From: Luke Kysow Date: Thu, 21 Jun 2018 18:18:54 +0100 Subject: [PATCH] WIP --- server/events/atlantis_workspace.go | 1 + server/events/atlantis_workspace_locker.go | 13 + server/events/command_context.go | 5 +- server/events/command_handler_test.go | 247 ------------------ ...{command_response.go => command_result.go} | 4 +- .../{command_handler.go => command_runner.go} | 155 +++++++---- server/events/command_runner_test.go | 239 +++++++++++++++++ server/events/comment_parser.go | 4 +- server/events/commit_status_updater.go | 12 +- server/events/commit_status_updater_test.go | 66 ++--- server/events/event_parser.go | 121 ++++++--- server/events/event_parser_test.go | 189 +++++++++++--- server/events/executor.go | 2 +- server/events/markdown_renderer.go | 6 +- server/events/markdown_renderer_test.go | 175 ++++++++----- ...mmandresponse.go => events_commandname.go} | 10 +- .../mocks/matchers/events_commandresult.go | 20 ++ .../matchers/models_projectcommandcontext.go | 20 ++ .../mocks/matchers/ptr_to_events_command.go | 10 +- .../matchers/ptr_to_events_commentcommand.go | 20 ++ .../ptr_to_github_pullrequestevent.go | 20 ++ .../mocks/matchers/ptr_to_models_repo.go | 20 ++ .../slice_of_models_projectcommandcontext.go | 20 ++ server/events/mocks/mock_command_runner.go | 84 ++++-- .../mocks/mock_commit_status_updater.go | 44 ++-- server/events/mocks/mock_event_parsing.go | 139 +++++++--- server/events/mocks/mock_executor.go | 8 +- .../mocks/mock_project_command_builder.go | 175 +++++++++++++ .../mocks/mock_pull_request_operator.go | 154 ----------- server/events/models/models_test.go | 7 + ...operator.go => project_command_builder.go} | 150 +++++------ ...est.go => project_command_builder_test.go} | 0 ..._operator.go => project_command_runner.go} | 106 ++++---- ...test.go => project_command_runner_test.go} | 0 server/events/project_result.go | 8 +- ..._step_operator.go => apply_step_runner.go} | 6 +- ...ator_test.go => apply_step_runner_test.go} | 8 +- ...t_step_operator.go => init_step_runner.go} | 4 +- ...rator_test.go => init_step_runner_test.go} | 2 +- ...n_step_operater.go => plan_step_runner.go} | 6 +- ...rater_test.go => plan_step_runner_test.go} | 40 ++- ...l_operator.go => pull_approved_checker.go} | 4 +- .../events/runtime/run_step_operator_test.go | 3 - ...un_step_operator.go => run_step_runner.go} | 6 +- server/events/runtime/run_step_runner_test.go | 50 ++++ server/events/vcs/fixtures/fixtures.go | 8 + server/events/vcs/vcs_test.go | 19 ++ server/events_controller.go | 50 ++-- server/events_controller_e2e_test.go | 78 +++--- server/events_controller_test.go | 159 ++++++++--- server/router_test.go | 27 ++ server/server.go | 69 +++-- server/server_test.go | 1 + .../githubPullRequestClosedEvent.json | 32 +-- .../githubPullRequestOpenedEvent.json | 6 +- .../exp-output-apply-default.txt.act | 7 - .../tfvars-yaml/exp-output-autoplan.txt.act | 61 ----- 57 files changed, 1776 insertions(+), 1124 deletions(-) delete mode 100644 server/events/command_handler_test.go rename server/events/{command_response.go => command_result.go} (89%) rename server/events/{command_handler.go => command_runner.go} (58%) create mode 100644 server/events/command_runner_test.go rename server/events/mocks/matchers/{events_commandresponse.go => events_commandname.go} (53%) create mode 100644 server/events/mocks/matchers/events_commandresult.go create mode 100644 server/events/mocks/matchers/models_projectcommandcontext.go create mode 100644 server/events/mocks/matchers/ptr_to_events_commentcommand.go create mode 100644 server/events/mocks/matchers/ptr_to_github_pullrequestevent.go create mode 100644 server/events/mocks/matchers/ptr_to_models_repo.go create mode 100644 server/events/mocks/matchers/slice_of_models_projectcommandcontext.go create mode 100644 server/events/mocks/mock_project_command_builder.go delete mode 100644 server/events/mocks/mock_pull_request_operator.go rename server/events/{pull_request_operator.go => project_command_builder.go} (62%) rename server/events/{pull_request_operator_test.go => project_command_builder_test.go} (100%) rename server/events/{project_operator.go => project_command_runner.go} (57%) rename server/events/{project_operator_test.go => project_command_runner_test.go} (100%) rename server/events/runtime/{apply_step_operator.go => apply_step_runner.go} (83%) rename server/events/runtime/{apply_step_operator_test.go => apply_step_runner_test.go} (95%) rename server/events/runtime/{init_step_operator.go => init_step_runner.go} (87%) rename server/events/runtime/{init_step_operator_test.go => init_step_runner_test.go} (97%) rename server/events/runtime/{plan_step_operater.go => plan_step_runner.go} (93%) rename server/events/runtime/{plan_step_operater_test.go => plan_step_runner_test.go} (87%) rename server/events/runtime/{approval_operator.go => pull_approved_checker.go} (67%) delete mode 100644 server/events/runtime/run_step_operator_test.go rename server/events/runtime/{run_step_operator.go => run_step_runner.go} (77%) create mode 100644 server/events/runtime/run_step_runner_test.go create mode 100644 server/events/vcs/vcs_test.go create mode 100644 server/router_test.go delete mode 100644 server/testfixtures/test-repos/tfvars-yaml/exp-output-apply-default.txt.act delete mode 100644 server/testfixtures/test-repos/tfvars-yaml/exp-output-autoplan.txt.act diff --git a/server/events/atlantis_workspace.go b/server/events/atlantis_workspace.go index 06944bdae..251adb7e3 100644 --- a/server/events/atlantis_workspace.go +++ b/server/events/atlantis_workspace.go @@ -35,6 +35,7 @@ type AtlantisWorkspace interface { // absolute path to the root of the cloned repo. Clone(log *logging.SimpleLogger, baseRepo models.Repo, headRepo models.Repo, p models.PullRequest, workspace string) (string, error) // GetWorkspace returns the path to the workspace for this repo and pull. + // If workspace does not exist on disk, error will be of type os.IsNotExist. GetWorkspace(r models.Repo, p models.PullRequest, workspace string) (string, error) // Delete deletes the workspace for this repo and pull. Delete(r models.Repo, p models.PullRequest) error diff --git a/server/events/atlantis_workspace_locker.go b/server/events/atlantis_workspace_locker.go index 594ea08ab..cc86db42c 100644 --- a/server/events/atlantis_workspace_locker.go +++ b/server/events/atlantis_workspace_locker.go @@ -30,6 +30,8 @@ import ( type AtlantisWorkspaceLocker interface { // TryLock tries to acquire a lock for this repo, workspace and pull. TryLock(repoFullName string, workspace string, pullNum int) bool + // TryLock2 tries to acquire a lock for this repo, workspace and pull. + TryLock2(repoFullName string, workspace string, pullNum int) (func(), error) // Unlock deletes the lock for this repo, workspace and pull. If there was no // lock it will do nothing. Unlock(repoFullName, workspace string, pullNum int) @@ -48,6 +50,17 @@ func NewDefaultAtlantisWorkspaceLocker() *DefaultAtlantisWorkspaceLocker { } } +func (d *DefaultAtlantisWorkspaceLocker) TryLock2(repoFullName string, workspace string, pullNum int) (func(), error) { + if !d.TryLock(repoFullName, workspace, pullNum) { + return func() {}, fmt.Errorf("the %s workspace is currently locked by another"+ + " command that is running for this pull request–"+ + "wait until the previous command is complete and try again", workspace) + } + return func() { + d.Unlock(repoFullName, workspace, pullNum) + }, nil +} + // TryLock returns true if a lock is acquired for this repo, pull and workspace and // false otherwise. func (d *DefaultAtlantisWorkspaceLocker) TryLock(repoFullName string, workspace string, pullNum int) bool { diff --git a/server/events/command_context.go b/server/events/command_context.go index f986e43cc..49b0f0726 100644 --- a/server/events/command_context.go +++ b/server/events/command_context.go @@ -30,7 +30,6 @@ type CommandContext struct { HeadRepo models.Repo Pull models.PullRequest // User is the user that triggered this command. - User models.User - Command *Command - Log *logging.SimpleLogger + User models.User + Log *logging.SimpleLogger } diff --git a/server/events/command_handler_test.go b/server/events/command_handler_test.go deleted file mode 100644 index 65bcf3c6f..000000000 --- a/server/events/command_handler_test.go +++ /dev/null @@ -1,247 +0,0 @@ -// Copyright 2017 HootSuite Media Inc. -// -// Licensed under the Apache License, Version 2.0 (the License); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an AS IS BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// Modified hereafter by contributors to runatlantis/atlantis. -// -package events_test - -import ( - "bytes" - "errors" - "log" - "strings" - "testing" - - "github.com/google/go-github/github" - . "github.com/petergtz/pegomock" - "github.com/runatlantis/atlantis/server/events" - "github.com/runatlantis/atlantis/server/events/mocks" - "github.com/runatlantis/atlantis/server/events/mocks/matchers" - "github.com/runatlantis/atlantis/server/events/models" - "github.com/runatlantis/atlantis/server/events/models/fixtures" - "github.com/runatlantis/atlantis/server/events/vcs" - vcsmocks "github.com/runatlantis/atlantis/server/events/vcs/mocks" - logmocks "github.com/runatlantis/atlantis/server/logging/mocks" - . "github.com/runatlantis/atlantis/testing" -) - -var operator *mocks.MockPullRequestOperator -var eventParsing *mocks.MockEventParsing -var vcsClient *vcsmocks.MockClientProxy -var ghStatus *mocks.MockCommitStatusUpdater -var githubGetter *mocks.MockGithubPullGetter -var gitlabGetter *mocks.MockGitlabMergeRequestGetter -var workspaceLocker *mocks.MockAtlantisWorkspaceLocker -var ch events.CommandHandler -var logBytes *bytes.Buffer - -func setup(t *testing.T) { - RegisterMockTestingT(t) - operator = mocks.NewMockPullRequestOperator() - eventParsing = mocks.NewMockEventParsing() - ghStatus = mocks.NewMockCommitStatusUpdater() - workspaceLocker = mocks.NewMockAtlantisWorkspaceLocker() - vcsClient = vcsmocks.NewMockClientProxy() - githubGetter = mocks.NewMockGithubPullGetter() - gitlabGetter = mocks.NewMockGitlabMergeRequestGetter() - logger := logmocks.NewMockSimpleLogging() - logBytes = new(bytes.Buffer) - When(logger.Underlying()).ThenReturn(log.New(logBytes, "", 0)) - ch = events.CommandHandler{ - VCSClient: vcsClient, - CommitStatusUpdater: ghStatus, - EventParser: eventParsing, - AtlantisWorkspaceLocker: workspaceLocker, - MarkdownRenderer: &events.MarkdownRenderer{}, - GithubPullGetter: githubGetter, - GitlabMergeRequestGetter: gitlabGetter, - Logger: logger, - AllowForkPRs: false, - AllowForkPRsFlag: "allow-fork-prs-flag", - PullRequestOperator: operator, - } -} - -func TestExecuteCommand_LogPanics(t *testing.T) { - t.Log("if there is a panic it is commented back on the pull request") - setup(t) - ch.AllowForkPRs = true // Lets us get to the panic code. - defer func() { ch.AllowForkPRs = false }() - When(ghStatus.Update(fixtures.GithubRepo, fixtures.Pull, vcs.Pending, nil)).ThenPanic("panic") - ch.ExecuteCommand(fixtures.GithubRepo, fixtures.GithubRepo, fixtures.User, 1, nil) - _, _, comment := vcsClient.VerifyWasCalledOnce().CreateComment(matchers.AnyModelsRepo(), AnyInt(), AnyString()).GetCapturedArguments() - Assert(t, strings.Contains(comment, "Error: goroutine panic"), "comment should be about a goroutine panic") -} - -func TestExecuteCommand_NoGithubPullGetter(t *testing.T) { - t.Log("if CommandHandler was constructed with a nil GithubPullGetter an error should be logged") - setup(t) - ch.GithubPullGetter = nil - ch.ExecuteCommand(fixtures.GithubRepo, fixtures.GithubRepo, fixtures.User, 1, nil) - Equals(t, "[ERROR] runatlantis/atlantis#1: Atlantis not configured to support GitHub\n", logBytes.String()) -} - -func TestExecuteCommand_NoGitlabMergeGetter(t *testing.T) { - t.Log("if CommandHandler was constructed with a nil GitlabMergeRequestGetter an error should be logged") - setup(t) - ch.GitlabMergeRequestGetter = nil - ch.ExecuteCommand(fixtures.GitlabRepo, fixtures.GitlabRepo, fixtures.User, 1, nil) - Equals(t, "[ERROR] runatlantis/atlantis#1: Atlantis not configured to support GitLab\n", logBytes.String()) -} - -func TestExecuteCommand_GithubPullErr(t *testing.T) { - t.Log("if getting the github pull request fails an error should be logged") - setup(t) - When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(nil, errors.New("err")) - ch.ExecuteCommand(fixtures.GithubRepo, fixtures.GithubRepo, fixtures.User, fixtures.Pull.Num, nil) - Equals(t, "[ERROR] runatlantis/atlantis#1: Making pull request API call to GitHub: err\n", logBytes.String()) -} - -func TestExecuteCommand_GitlabMergeRequestErr(t *testing.T) { - t.Log("if getting the gitlab merge request fails an error should be logged") - setup(t) - When(gitlabGetter.GetMergeRequest(fixtures.GithubRepo.FullName, fixtures.Pull.Num)).ThenReturn(nil, errors.New("err")) - ch.ExecuteCommand(fixtures.GitlabRepo, fixtures.GitlabRepo, fixtures.User, fixtures.Pull.Num, nil) - Equals(t, "[ERROR] runatlantis/atlantis#1: Making merge request API call to GitLab: err\n", logBytes.String()) -} - -func TestExecuteCommand_GithubPullParseErr(t *testing.T) { - t.Log("if parsing the returned github pull request fails an error should be logged") - setup(t) - var pull github.PullRequest - When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(&pull, nil) - When(eventParsing.ParseGithubPull(&pull)).ThenReturn(fixtures.Pull, fixtures.GithubRepo, errors.New("err")) - - ch.ExecuteCommand(fixtures.GithubRepo, fixtures.GithubRepo, fixtures.User, fixtures.Pull.Num, nil) - Equals(t, "[ERROR] runatlantis/atlantis#1: Extracting required fields from comment data: err\n", logBytes.String()) -} - -func TestExecuteCommand_ForkPRDisabled(t *testing.T) { - t.Log("if a command is run on a forked pull request and this is disabled atlantis should" + - " comment saying that this is not allowed") - setup(t) - ch.AllowForkPRs = false // by default it's false so don't need to reset - var pull github.PullRequest - modelPull := models.PullRequest{State: models.Open} - When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(&pull, nil) - - headRepo := fixtures.GithubRepo - headRepo.FullName = "forkrepo/atlantis" - headRepo.Owner = "forkrepo" - When(eventParsing.ParseGithubPull(&pull)).ThenReturn(modelPull, headRepo, nil) - - ch.ExecuteCommand(fixtures.GithubRepo, models.Repo{} /* this isn't used */, fixtures.User, fixtures.Pull.Num, nil) - vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GithubRepo, modelPull.Num, "Atlantis commands can't be run on fork pull requests. To enable, set --"+ch.AllowForkPRsFlag) -} - -func TestExecuteCommand_ClosedPull(t *testing.T) { - t.Log("if a command is run on a closed pull request atlantis should" + - " comment saying that this is not allowed") - setup(t) - pull := &github.PullRequest{ - State: github.String("closed"), - } - modelPull := models.PullRequest{State: models.Closed} - When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(pull, nil) - When(eventParsing.ParseGithubPull(pull)).ThenReturn(modelPull, fixtures.GithubRepo, nil) - - ch.ExecuteCommand(fixtures.GithubRepo, fixtures.GithubRepo, fixtures.User, fixtures.Pull.Num, nil) - vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GithubRepo, modelPull.Num, "Atlantis commands can't be run on closed pull requests") -} - -func TestExecuteCommand_WorkspaceLocked(t *testing.T) { - t.Log("if the workspace is locked, should comment back on the pull") - setup(t) - pull := &github.PullRequest{ - State: github.String("closed"), - } - cmd := events.Command{ - Name: events.Plan, - Workspace: "workspace", - } - - When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(pull, nil) - When(eventParsing.ParseGithubPull(pull)).ThenReturn(fixtures.Pull, fixtures.GithubRepo, nil) - When(workspaceLocker.TryLock(fixtures.GithubRepo.FullName, cmd.Workspace, fixtures.Pull.Num)).ThenReturn(false) - ch.ExecuteCommand(fixtures.GithubRepo, fixtures.GithubRepo, fixtures.User, fixtures.Pull.Num, &cmd) - - msg := "The workspace workspace is currently locked by another" + - " command that is running for this pull request." + - " Wait until the previous command is complete and try again." - ghStatus.VerifyWasCalledOnce().Update(fixtures.GithubRepo, fixtures.Pull, vcs.Pending, &cmd) - _, response := ghStatus.VerifyWasCalledOnce().UpdateProjectResult(matchers.AnyPtrToEventsCommandContext(), matchers.AnyEventsCommandResponse()).GetCapturedArguments() - Equals(t, msg, response.Failure) - vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GithubRepo, fixtures.Pull.Num, - "**Plan Failed**: "+msg+"\n\n") -} - -func TestExecuteCommand_FullRun(t *testing.T) { - t.Log("when running a plan, apply should comment") - pull := &github.PullRequest{ - State: github.String("closed"), - } - cmdResponse := events.CommandResponse{} - for _, c := range []events.CommandName{events.Plan, events.Apply} { - setup(t) - cmd := events.Command{ - Name: c, - Workspace: "workspace", - } - When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(pull, nil) - When(eventParsing.ParseGithubPull(pull)).ThenReturn(fixtures.Pull, fixtures.GithubRepo, nil) - When(workspaceLocker.TryLock(fixtures.GithubRepo.FullName, cmd.Workspace, fixtures.Pull.Num)).ThenReturn(true) - switch c { - case events.Plan: - When(operator.PlanViaComment(matchers.AnyPtrToEventsCommandContext())).ThenReturn(cmdResponse) - case events.Apply: - When(operator.ApplyViaComment(matchers.AnyPtrToEventsCommandContext())).ThenReturn(cmdResponse) - } - - ch.ExecuteCommand(fixtures.GithubRepo, fixtures.GithubRepo, fixtures.User, fixtures.Pull.Num, &cmd) - - ghStatus.VerifyWasCalledOnce().Update(fixtures.GithubRepo, fixtures.Pull, vcs.Pending, &cmd) - _, response := ghStatus.VerifyWasCalledOnce().UpdateProjectResult(matchers.AnyPtrToEventsCommandContext(), matchers.AnyEventsCommandResponse()).GetCapturedArguments() - Equals(t, cmdResponse, response) - vcsClient.VerifyWasCalledOnce().CreateComment(matchers.AnyModelsRepo(), AnyInt(), AnyString()) - workspaceLocker.VerifyWasCalledOnce().Unlock(fixtures.GithubRepo.FullName, cmd.Workspace, fixtures.Pull.Num) - } -} - -func TestExecuteCommand_ForkPREnabled(t *testing.T) { - t.Log("when running a plan on a fork PR, it should succeed") - setup(t) - - // Enable forked PRs. - ch.AllowForkPRs = true - defer func() { ch.AllowForkPRs = false }() // Reset after test. - - var pull github.PullRequest - cmdResponse := events.CommandResponse{} - cmd := events.Command{ - Name: events.Plan, - Workspace: "workspace", - } - When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(&pull, nil) - headRepo := fixtures.GithubRepo - headRepo.FullName = "forkrepo/atlantis" - headRepo.Owner = "forkrepo" - When(eventParsing.ParseGithubPull(&pull)).ThenReturn(fixtures.Pull, headRepo, nil) - When(workspaceLocker.TryLock(fixtures.GithubRepo.FullName, cmd.Workspace, fixtures.Pull.Num)).ThenReturn(true) - When(operator.PlanViaComment(matchers.AnyPtrToEventsCommandContext())).ThenReturn(cmdResponse) - - ch.ExecuteCommand(fixtures.GithubRepo, models.Repo{} /* this isn't used */, fixtures.User, fixtures.Pull.Num, &cmd) - - ghStatus.VerifyWasCalledOnce().Update(fixtures.GithubRepo, fixtures.Pull, vcs.Pending, &cmd) - _, response := ghStatus.VerifyWasCalledOnce().UpdateProjectResult(matchers.AnyPtrToEventsCommandContext(), matchers.AnyEventsCommandResponse()).GetCapturedArguments() - Equals(t, cmdResponse, response) - vcsClient.VerifyWasCalledOnce().CreateComment(matchers.AnyModelsRepo(), AnyInt(), AnyString()) - workspaceLocker.VerifyWasCalledOnce().Unlock(fixtures.GithubRepo.FullName, cmd.Workspace, fixtures.Pull.Num) -} diff --git a/server/events/command_response.go b/server/events/command_result.go similarity index 89% rename from server/events/command_response.go rename to server/events/command_result.go index abba3897b..ff767ddb5 100644 --- a/server/events/command_response.go +++ b/server/events/command_result.go @@ -13,8 +13,8 @@ // package events -// CommandResponse is the result of running a Command. -type CommandResponse struct { +// CommandResult is the result of running a Command. +type CommandResult struct { Error error Failure string ProjectResults []ProjectResult diff --git a/server/events/command_handler.go b/server/events/command_runner.go similarity index 58% rename from server/events/command_handler.go rename to server/events/command_runner.go index bfd25dd93..c54891e87 100644 --- a/server/events/command_handler.go +++ b/server/events/command_runner.go @@ -29,10 +29,11 @@ import ( // CommandRunner is the first step after a command request has been parsed. type CommandRunner interface { - // ExecuteCommand is the first step after a command request has been parsed. + // RunCommentCommand is the first step after a command request has been parsed. // It handles gathering additional information needed to execute the command // and then calling the appropriate services to finish executing the command. - ExecuteCommand(baseRepo models.Repo, headRepo models.Repo, user models.User, pullNum int, cmd *Command) + RunCommentCommand(baseRepo models.Repo, maybeHeadRepo *models.Repo, user models.User, pullNum int, cmd *CommentCommand) + RunAutoplanCommand(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) } //go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_github_pull_getter.go GithubPullGetter @@ -51,14 +52,13 @@ type GitlabMergeRequestGetter interface { GetMergeRequest(repoFullName string, pullNum int) (*gitlab.MergeRequest, error) } -// CommandHandler is the first step when processing a comment command. -type CommandHandler struct { +// DefaultCommandRunner is the first step when processing a comment command. +type DefaultCommandRunner struct { VCSClient vcs.ClientProxy GithubPullGetter GithubPullGetter GitlabMergeRequestGetter GitlabMergeRequestGetter CommitStatusUpdater CommitStatusUpdater EventParser EventParsing - AtlantisWorkspaceLocker AtlantisWorkspaceLocker MarkdownRenderer *MarkdownRenderer Logger logging.SimpleLogging // AllowForkPRs controls whether we operate on pull requests from forks. @@ -66,17 +66,50 @@ type CommandHandler struct { // AllowForkPRsFlag 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 enable this functionality. - AllowForkPRsFlag string - PullRequestOperator PullRequestOperator + AllowForkPRsFlag string + ProjectCommandBuilder ProjectCommandBuilder + ProjectCommandRunner *ProjectCommandRunner } -// ExecuteCommand executes the command. -// If the repo is from GitHub, we don't use headRepo and instead make an API call -// to get the headRepo. This is because the caller is unable to pass in a -// headRepo since there's not enough data available on the initial webhook -// payload. -func (c *CommandHandler) ExecuteCommand(baseRepo models.Repo, headRepo models.Repo, user models.User, pullNum int, cmd *Command) { +func (c *DefaultCommandRunner) RunAutoplanCommand(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) { + log := c.buildLogger(baseRepo.FullName, pull.Num) + ctx := &CommandContext{ + User: user, + Log: log, + Pull: pull, + HeadRepo: headRepo, + BaseRepo: baseRepo, + } + runFn := func() ([]ProjectResult, error) { + projectCmds, err := c.ProjectCommandBuilder.BuildAutoplanCommands(ctx) + if err != nil { + return nil, err + } + var results []ProjectResult + for _, cmd := range projectCmds { + res := c.ProjectCommandRunner.Plan(cmd) + results = append(results, ProjectResult{ + ProjectCommandResult: res, + Path: cmd.RepoRelPath, + Workspace: cmd.Workspace, + }) + } + return results, nil + } + c.run(ctx, AutoplanCommand{}, runFn) +} + +// RunCommentCommand executes the command. +// We take in a pointer for maybeHeadRepo because for some events there isn't +// enough data to construct the Repo model and callers might want to wait until +// the event is further validated before making an additional (potentially +// wasteful) call to get the necessary data. +func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHeadRepo *models.Repo, user models.User, pullNum int, cmd *CommentCommand) { log := c.buildLogger(baseRepo.FullName, pullNum) + var headRepo models.Repo + if maybeHeadRepo != nil { + headRepo = *maybeHeadRepo + } var err error var pull models.PullRequest @@ -97,13 +130,38 @@ func (c *CommandHandler) ExecuteCommand(baseRepo models.Repo, headRepo models.Re Log: log, Pull: pull, HeadRepo: headRepo, - Command: cmd, BaseRepo: baseRepo, } - c.run(ctx) + + runFn := func() ([]ProjectResult, error) { + var result ProjectCommandResult + switch cmd.Name { + case Plan: + projectCmd, err := c.ProjectCommandBuilder.BuildPlanCommand(ctx, cmd) + if err != nil { + return nil, err + } + result = c.ProjectCommandRunner.Plan(projectCmd) + case Apply: + projectCmd, err := c.ProjectCommandBuilder.BuildApplyCommand(ctx, cmd) + if err != nil { + return nil, err + } + result = c.ProjectCommandRunner.Apply(projectCmd) + default: + ctx.Log.Err("failed to determine desired command, neither plan nor apply") + } + return []ProjectResult{{ + Path: cmd.Dir, + Workspace: cmd.Workspace, + ProjectCommandResult: result, + }}, nil + } + + c.run(ctx, cmd, runFn) } -func (c *CommandHandler) getGithubData(baseRepo models.Repo, pullNum int) (models.PullRequest, models.Repo, error) { +func (c *DefaultCommandRunner) getGithubData(baseRepo models.Repo, pullNum int) (models.PullRequest, models.Repo, error) { if c.GithubPullGetter == nil { return models.PullRequest{}, models.Repo{}, errors.New("Atlantis not configured to support GitHub") } @@ -111,14 +169,14 @@ func (c *CommandHandler) getGithubData(baseRepo models.Repo, pullNum int) (model if err != nil { return models.PullRequest{}, models.Repo{}, errors.Wrap(err, "making pull request API call to GitHub") } - pull, repo, err := c.EventParser.ParseGithubPull(ghPull) + pull, _, headRepo, err := c.EventParser.ParseGithubPull(ghPull) if err != nil { - return pull, repo, errors.Wrap(err, "extracting required fields from comment data") + return pull, headRepo, errors.Wrap(err, "extracting required fields from comment data") } - return pull, repo, nil + return pull, headRepo, nil } -func (c *CommandHandler) getGitlabData(baseRepo models.Repo, pullNum int) (models.PullRequest, error) { +func (c *DefaultCommandRunner) getGitlabData(baseRepo models.Repo, pullNum int) (models.PullRequest, error) { if c.GitlabMergeRequestGetter == nil { return models.PullRequest{}, errors.New("Atlantis not configured to support GitLab") } @@ -130,60 +188,47 @@ func (c *CommandHandler) getGitlabData(baseRepo models.Repo, pullNum int) (model return pull, nil } -func (c *CommandHandler) buildLogger(repoFullName string, pullNum int) *logging.SimpleLogger { +func (c *DefaultCommandRunner) buildLogger(repoFullName string, pullNum int) *logging.SimpleLogger { src := fmt.Sprintf("%s#%d", repoFullName, pullNum) return logging.NewSimpleLogger(src, c.Logger.Underlying(), true, c.Logger.GetLevel()) } -func (c *CommandHandler) run(ctx *CommandContext) { - defer c.logPanics(ctx) - +func (c *DefaultCommandRunner) validateCtxAndComment(ctx *CommandContext) bool { if !c.AllowForkPRs && ctx.HeadRepo.Owner != ctx.BaseRepo.Owner { ctx.Log.Info("command was run on a fork pull request which is disallowed") c.VCSClient.CreateComment(ctx.BaseRepo, ctx.Pull.Num, fmt.Sprintf("Atlantis commands can't be run on fork pull requests. To enable, set --%s", c.AllowForkPRsFlag)) // nolint: errcheck - return + return false } if ctx.Pull.State != models.Open { ctx.Log.Info("command was run on closed pull request") c.VCSClient.CreateComment(ctx.BaseRepo, ctx.Pull.Num, "Atlantis commands can't be run on closed pull requests") // nolint: errcheck + return false + } + return true +} + +func (c *DefaultCommandRunner) run(ctx *CommandContext, command CommandInterface, commandRunner func() ([]ProjectResult, error)) { + defer c.logPanics(ctx) + + if !c.validateCtxAndComment(ctx) { return } ctx.Log.Debug("updating commit status to pending") - if err := c.CommitStatusUpdater.Update(ctx.BaseRepo, ctx.Pull, vcs.Pending, ctx.Command); err != nil { + if err := c.CommitStatusUpdater.Update(ctx.BaseRepo, ctx.Pull, vcs.Pending, command.CommandName()); err != nil { ctx.Log.Warn("unable to update commit status: %s", err) } - if !c.AtlantisWorkspaceLocker.TryLock(ctx.BaseRepo.FullName, ctx.Command.Workspace, ctx.Pull.Num) { - errMsg := fmt.Sprintf( - "The %s workspace is currently locked by another"+ - " command that is running for this pull request."+ - " Wait until the previous command is complete and try again.", - ctx.Command.Workspace) - ctx.Log.Warn(errMsg) - c.updatePull(ctx, CommandResponse{Failure: errMsg}) + + results, err := commandRunner() + if err != nil { + c.updatePull(ctx, command, CommandResult{Error: err}) return } - ctx.Log.Debug("successfully acquired workspace lock") - defer c.AtlantisWorkspaceLocker.Unlock(ctx.BaseRepo.FullName, ctx.Command.Workspace, ctx.Pull.Num) - - var cr CommandResponse - switch ctx.Command.Name { - case Plan: - if ctx.Command.Autoplan { - cr = c.PullRequestOperator.Autoplan(ctx) - } else { - cr = c.PullRequestOperator.PlanViaComment(ctx) - } - case Apply: - cr = c.PullRequestOperator.ApplyViaComment(ctx) - default: - ctx.Log.Err("failed to determine desired command, neither plan nor apply") - } - c.updatePull(ctx, cr) + c.updatePull(ctx, command, CommandResult{ProjectResults: results}) } -func (c *CommandHandler) updatePull(ctx *CommandContext, res CommandResponse) { +func (c *DefaultCommandRunner) updatePull(ctx *CommandContext, command CommandInterface, res CommandResult) { // Log if we got any errors or failures. if res.Error != nil { ctx.Log.Err(res.Error.Error()) @@ -192,15 +237,15 @@ func (c *CommandHandler) updatePull(ctx *CommandContext, res CommandResponse) { } // Update the pull request's status icon and comment back. - if err := c.CommitStatusUpdater.UpdateProjectResult(ctx, res); err != nil { + if err := c.CommitStatusUpdater.UpdateProjectResult(ctx, command.CommandName(), res); err != nil { ctx.Log.Warn("unable to update commit status: %s", err) } - comment := c.MarkdownRenderer.Render(res, ctx.Command.Name, ctx.Log.History.String(), ctx.Command.Verbose, ctx.Command.Autoplan) + comment := c.MarkdownRenderer.Render(res, command.CommandName(), ctx.Log.History.String(), command.IsVerbose(), command.IsAutoplan()) c.VCSClient.CreateComment(ctx.BaseRepo, ctx.Pull.Num, comment) // nolint: errcheck } // logPanics logs and creates a comment on the pull request for panics. -func (c *CommandHandler) logPanics(ctx *CommandContext) { +func (c *DefaultCommandRunner) logPanics(ctx *CommandContext) { if err := recover(); err != nil { stack := recovery.Stack(3) c.VCSClient.CreateComment(ctx.BaseRepo, ctx.Pull.Num, // nolint: errcheck diff --git a/server/events/command_runner_test.go b/server/events/command_runner_test.go new file mode 100644 index 000000000..a0d636c9a --- /dev/null +++ b/server/events/command_runner_test.go @@ -0,0 +1,239 @@ +// Copyright 2017 HootSuite Media Inc. +// +// Licensed under the Apache License, Version 2.0 (the License); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an AS IS BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// Modified hereafter by contributors to runatlantis/atlantis. +// +package events_test + +import ( + "bytes" + "log" + "testing" + + . "github.com/petergtz/pegomock" + "github.com/runatlantis/atlantis/server/events" + "github.com/runatlantis/atlantis/server/events/mocks" + vcsmocks "github.com/runatlantis/atlantis/server/events/vcs/mocks" + logmocks "github.com/runatlantis/atlantis/server/logging/mocks" + //. "github.com/runatlantis/atlantis/testing" +) + +var projectCommandBuilder *mocks.MockProjectCommandBuilder +var eventParsing *mocks.MockEventParsing +var vcsClient *vcsmocks.MockClientProxy +var ghStatus *mocks.MockCommitStatusUpdater +var githubGetter *mocks.MockGithubPullGetter +var gitlabGetter *mocks.MockGitlabMergeRequestGetter +var workspaceLocker *mocks.MockAtlantisWorkspaceLocker +var ch events.DefaultCommandRunner +var logBytes *bytes.Buffer + +func setup(t *testing.T) { + RegisterMockTestingT(t) + projectCommandBuilder = mocks.NewMockProjectCommandBuilder() + eventParsing = mocks.NewMockEventParsing() + ghStatus = mocks.NewMockCommitStatusUpdater() + workspaceLocker = mocks.NewMockAtlantisWorkspaceLocker() + vcsClient = vcsmocks.NewMockClientProxy() + githubGetter = mocks.NewMockGithubPullGetter() + gitlabGetter = mocks.NewMockGitlabMergeRequestGetter() + logger := logmocks.NewMockSimpleLogging() + logBytes = new(bytes.Buffer) + When(logger.Underlying()).ThenReturn(log.New(logBytes, "", 0)) + ch = events.DefaultCommandRunner{ + VCSClient: vcsClient, + CommitStatusUpdater: ghStatus, + EventParser: eventParsing, + MarkdownRenderer: &events.MarkdownRenderer{}, + GithubPullGetter: githubGetter, + GitlabMergeRequestGetter: gitlabGetter, + Logger: logger, + AllowForkPRs: false, + AllowForkPRsFlag: "allow-fork-prs-flag", + ProjectCommandBuilder: projectCommandBuilder, + } +} + +//func TestExecuteCommand_LogPanics(t *testing.T) { +// t.Log("if there is a panic it is commented back on the pull request") +// setup(t) +// ch.AllowForkPRs = true // Lets us get to the panic code. +// defer func() { ch.AllowForkPRs = false }() +// When(ghStatus.Update(fixtures.GithubRepo, fixtures.Pull, vcs.Pending, events.Plan)).ThenPanic("panic") +// ch.RunCommentCommand(fixtures.GithubRepo, &fixtures.GithubRepo, fixtures.User, 1, nil) +// _, _, comment := vcsClient.VerifyWasCalledOnce().CreateComment(matchers.AnyModelsRepo(), AnyInt(), AnyString()).GetCapturedArguments() +// Assert(t, strings.Contains(comment, "Error: goroutine panic"), "comment should be about a goroutine panic") +//} +// +//func TestExecuteCommand_NoGithubPullGetter(t *testing.T) { +// t.Log("if DefaultCommandRunner was constructed with a nil GithubPullGetter an error should be logged") +// setup(t) +// ch.GithubPullGetter = nil +// ch.RunCommentCommand(fixtures.GithubRepo, &fixtures.GithubRepo, fixtures.User, 1, nil) +// Equals(t, "[ERROR] runatlantis/atlantis#1: Atlantis not configured to support GitHub\n", logBytes.String()) +//} +// +//func TestExecuteCommand_NoGitlabMergeGetter(t *testing.T) { +// t.Log("if DefaultCommandRunner was constructed with a nil GitlabMergeRequestGetter an error should be logged") +// setup(t) +// ch.GitlabMergeRequestGetter = nil +// ch.RunCommentCommand(fixtures.GitlabRepo, &fixtures.GitlabRepo, fixtures.User, 1, nil) +// Equals(t, "[ERROR] runatlantis/atlantis#1: Atlantis not configured to support GitLab\n", logBytes.String()) +//} +// +//func TestExecuteCommand_GithubPullErr(t *testing.T) { +// t.Log("if getting the github pull request fails an error should be logged") +// setup(t) +// When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(nil, errors.New("err")) +// ch.RunCommentCommand(fixtures.GithubRepo, &fixtures.GithubRepo, fixtures.User, fixtures.Pull.Num, nil) +// Equals(t, "[ERROR] runatlantis/atlantis#1: Making pull request API call to GitHub: err\n", logBytes.String()) +//} +// +//func TestExecuteCommand_GitlabMergeRequestErr(t *testing.T) { +// t.Log("if getting the gitlab merge request fails an error should be logged") +// setup(t) +// When(gitlabGetter.GetMergeRequest(fixtures.GithubRepo.FullName, fixtures.Pull.Num)).ThenReturn(nil, errors.New("err")) +// ch.RunCommentCommand(fixtures.GitlabRepo, &fixtures.GitlabRepo, fixtures.User, fixtures.Pull.Num, nil) +// Equals(t, "[ERROR] runatlantis/atlantis#1: Making merge request API call to GitLab: err\n", logBytes.String()) +//} +// +//func TestExecuteCommand_GithubPullParseErr(t *testing.T) { +// t.Log("if parsing the returned github pull request fails an error should be logged") +// setup(t) +// var pull github.PullRequest +// When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(&pull, nil) +// When(eventParsing.ParseGithubPull(&pull)).ThenReturn(fixtures.Pull, fixtures.GithubRepo, errors.New("err")) +// +// ch.RunCommentCommand(fixtures.GithubRepo, &fixtures.GithubRepo, fixtures.User, fixtures.Pull.Num, nil) +// Equals(t, "[ERROR] runatlantis/atlantis#1: Extracting required fields from comment data: err\n", logBytes.String()) +//} +// +//func TestExecuteCommand_ForkPRDisabled(t *testing.T) { +// t.Log("if a command is run on a forked pull request and this is disabled atlantis should" + +// " comment saying that this is not allowed") +// setup(t) +// ch.AllowForkPRs = false // by default it's false so don't need to reset +// var pull github.PullRequest +// modelPull := models.PullRequest{State: models.Open} +// When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(&pull, nil) +// +// headRepo := fixtures.GithubRepo +// headRepo.FullName = "forkrepo/atlantis" +// headRepo.Owner = "forkrepo" +// When(eventParsing.ParseGithubPull(&pull)).ThenReturn(modelPull, headRepo, nil) +// +// ch.RunCommentCommand(fixtures.GithubRepo, nil, fixtures.User, fixtures.Pull.Num, nil) +// vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GithubRepo, modelPull.Num, "Atlantis commands can't be run on fork pull requests. To enable, set --"+ch.AllowForkPRsFlag) +//} +// +//func TestExecuteCommand_ClosedPull(t *testing.T) { +// t.Log("if a command is run on a closed pull request atlantis should" + +// " comment saying that this is not allowed") +// setup(t) +// pull := &github.PullRequest{ +// State: github.String("closed"), +// } +// modelPull := models.PullRequest{State: models.Closed} +// When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(pull, nil) +// When(eventParsing.ParseGithubPull(pull)).ThenReturn(modelPull, fixtures.GithubRepo, nil) +// +// ch.RunCommentCommand(fixtures.GithubRepo, &fixtures.GithubRepo, fixtures.User, fixtures.Pull.Num, nil) +// vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GithubRepo, modelPull.Num, "Atlantis commands can't be run on closed pull requests") +//} +// +//func TestExecuteCommand_WorkspaceLocked(t *testing.T) { +// t.Log("if the workspace is locked, should comment back on the pull") +// setup(t) +// pull := &github.PullRequest{ +// State: github.String("closed"), +// } +// cmd := events.CommentCommand{ +// Name: events.Plan, +// Workspace: "workspace", +// } +// +// When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(pull, nil) +// When(eventParsing.ParseGithubPull(pull)).ThenReturn(fixtures.Pull, fixtures.GithubRepo, nil) +// When(workspaceLocker.TryLock(fixtures.GithubRepo.FullName, cmd.Workspace, fixtures.Pull.Num)).ThenReturn(false) +// ch.RunCommentCommand(fixtures.GithubRepo, &fixtures.GithubRepo, fixtures.User, fixtures.Pull.Num, &cmd) +// +// msg := "The workspace workspace is currently locked by another" + +// " command that is running for this pull request." + +// " Wait until the previous command is complete and try again." +// ghStatus.VerifyWasCalledOnce().Update(fixtures.GithubRepo, fixtures.Pull, vcs.Pending, cmd.CommandName()) +// _, _, result := ghStatus.VerifyWasCalledOnce().UpdateProjectResult(matchers.AnyPtrToEventsCommandContext(), matchers.AnyEventsCommandName(), matchers.AnyEventsCommandResult()).GetCapturedArguments() +// Equals(t, msg, result.Failure) +// vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GithubRepo, fixtures.Pull.Num, +// "**Plan Failed**: "+msg+"\n\n") +//} +// +//func TestExecuteCommand_FullRun(t *testing.T) { +// t.Log("when running a plan, apply should comment") +// pull := &github.PullRequest{ +// State: github.String("closed"), +// } +// cmdResult := events.CommandResult{} +// for _, c := range []events.CommandName{events.Plan, events.Apply} { +// setup(t) +// cmd := events.CommentCommand{ +// Name: c, +// Workspace: "workspace", +// } +// When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(pull, nil) +// When(eventParsing.ParseGithubPull(pull)).ThenReturn(fixtures.Pull, fixtures.GithubRepo, nil) +// When(workspaceLocker.TryLock(fixtures.GithubRepo.FullName, cmd.Workspace, fixtures.Pull.Num)).ThenReturn(true) +// switch c { +// case events.Plan: +// When(projectCommandBuilder.PlanViaComment(matchers.AnyPtrToEventsCommandContext())).ThenReturn(cmdResult) +// case events.Apply: +// When(projectCommandBuilder.ApplyViaComment(matchers.AnyPtrToEventsCommandContext())).ThenReturn(cmdResult) +// } +// +// ch.RunCommentCommand(fixtures.GithubRepo, fixtures.GithubRepo, fixtures.User, fixtures.Pull.Num, &cmd) +// +// ghStatus.VerifyWasCalledOnce().Update(fixtures.GithubRepo, fixtures.Pull, vcs.Pending, &cmd) +// _, response := ghStatus.VerifyWasCalledOnce().UpdateProjectResult(matchers.AnyPtrToEventsCommandContext(), matchers.AnyEventsCommandResult()).GetCapturedArguments() +// Equals(t, cmdResult, response) +// vcsClient.VerifyWasCalledOnce().CreateComment(matchers.AnyModelsRepo(), AnyInt(), AnyString()) +// workspaceLocker.VerifyWasCalledOnce().Unlock(fixtures.GithubRepo.FullName, cmd.Workspace, fixtures.Pull.Num) +// } +//} +// +//func TestExecuteCommand_ForkPREnabled(t *testing.T) { +// t.Log("when running a plan on a fork PR, it should succeed") +// setup(t) +// +// // Enable forked PRs. +// ch.AllowForkPRs = true +// defer func() { ch.AllowForkPRs = false }() // Reset after test. +// +// var pull github.PullRequest +// cmdResponse := events.CommandResult{} +// cmd := events.CommentCommand{ +// Name: events.Plan, +// Workspace: "workspace", +// } +// When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(&pull, nil) +// headRepo := fixtures.GithubRepo +// headRepo.FullName = "forkrepo/atlantis" +// headRepo.Owner = "forkrepo" +// When(eventParsing.ParseGithubPull(&pull)).ThenReturn(fixtures.Pull, headRepo, nil) +// When(workspaceLocker.TryLock(fixtures.GithubRepo.FullName, cmd.Workspace, fixtures.Pull.Num)).ThenReturn(true) +// When(projectCommandBuilder.PlanViaComment(matchers.AnyPtrToEventsCommandContext())).ThenReturn(cmdResponse) +// +// ch.RunCommentCommand(fixtures.GithubRepo, models.Repo{} /* this isn't used */, fixtures.User, fixtures.Pull.Num, &cmd) +// +// ghStatus.VerifyWasCalledOnce().Update(fixtures.GithubRepo, fixtures.Pull, vcs.Pending, &cmd) +// _, response := ghStatus.VerifyWasCalledOnce().UpdateProjectResult(matchers.AnyPtrToEventsCommandContext(), matchers.AnyEventsCommandResult()).GetCapturedArguments() +// Equals(t, cmdResponse, response) +// vcsClient.VerifyWasCalledOnce().CreateComment(matchers.AnyModelsRepo(), AnyInt(), AnyString()) +// workspaceLocker.VerifyWasCalledOnce().Unlock(fixtures.GithubRepo.FullName, cmd.Workspace, fixtures.Pull.Num) +//} diff --git a/server/events/comment_parser.go b/server/events/comment_parser.go index 808d4493a..c6f9f2439 100644 --- a/server/events/comment_parser.go +++ b/server/events/comment_parser.go @@ -59,7 +59,7 @@ type CommentParser struct { type CommentParseResult struct { // Command is the successfully parsed command. Will be nil if // CommentResponse or Ignore is set. - Command *Command + Command *CommentCommand // CommentResponse is set when we should respond immediately to the command // for example for atlantis help. CommentResponse string @@ -215,7 +215,7 @@ func (e *CommentParser) Parse(comment string, vcsHost models.VCSHostType) Commen } return CommentParseResult{ - Command: NewCommand(dir, extraArgs, name, verbose, workspace, project, false), + Command: NewCommand(dir, extraArgs, name, verbose, workspace, project), } } diff --git a/server/events/commit_status_updater.go b/server/events/commit_status_updater.go index 3debcd43f..ba5a23503 100644 --- a/server/events/commit_status_updater.go +++ b/server/events/commit_status_updater.go @@ -27,10 +27,10 @@ import ( // the status to signify whether the plan/apply succeeds. type CommitStatusUpdater interface { // Update updates the status of the head commit of pull. - Update(repo models.Repo, pull models.PullRequest, status vcs.CommitStatus, cmd *Command) error + Update(repo models.Repo, pull models.PullRequest, status vcs.CommitStatus, command CommandName) error // UpdateProjectResult updates the status of the head commit given the // state of response. - UpdateProjectResult(ctx *CommandContext, res CommandResponse) error + UpdateProjectResult(ctx *CommandContext, commandName CommandName, res CommandResult) error } // DefaultCommitStatusUpdater implements CommitStatusUpdater. @@ -39,13 +39,13 @@ type DefaultCommitStatusUpdater struct { } // Update updates the commit status. -func (d *DefaultCommitStatusUpdater) Update(repo models.Repo, pull models.PullRequest, status vcs.CommitStatus, cmd *Command) error { - description := fmt.Sprintf("%s %s", strings.Title(cmd.Name.String()), strings.Title(status.String())) +func (d *DefaultCommitStatusUpdater) Update(repo models.Repo, pull models.PullRequest, status vcs.CommitStatus, command CommandName) error { + description := fmt.Sprintf("%s %s", strings.Title(command.String()), strings.Title(status.String())) return d.Client.UpdateStatus(repo, pull, status, description) } // UpdateProjectResult updates the commit status based on the status of res. -func (d *DefaultCommitStatusUpdater) UpdateProjectResult(ctx *CommandContext, res CommandResponse) error { +func (d *DefaultCommitStatusUpdater) UpdateProjectResult(ctx *CommandContext, commandName CommandName, res CommandResult) error { var status vcs.CommitStatus if res.Error != nil || res.Failure != "" { status = vcs.Failed @@ -56,7 +56,7 @@ func (d *DefaultCommitStatusUpdater) UpdateProjectResult(ctx *CommandContext, re } status = d.worstStatus(statuses) } - return d.Update(ctx.BaseRepo, ctx.Pull, status, ctx.Command) + return d.Update(ctx.BaseRepo, ctx.Pull, status, commandName) } func (d *DefaultCommitStatusUpdater) worstStatus(ss []vcs.CommitStatus) vcs.CommitStatus { diff --git a/server/events/commit_status_updater_test.go b/server/events/commit_status_updater_test.go index 7d05264b0..155822dc1 100644 --- a/server/events/commit_status_updater_test.go +++ b/server/events/commit_status_updater_test.go @@ -29,26 +29,12 @@ import ( var repoModel = models.Repo{} var pullModel = models.PullRequest{} var status = vcs.Success -var cmd = events.Command{ - Name: events.Plan, -} - -func TestStatus_String(t *testing.T) { - cases := map[vcs.CommitStatus]string{ - vcs.Pending: "pending", - vcs.Success: "success", - vcs.Failed: "failed", - } - for k, v := range cases { - Equals(t, v, k.String()) - } -} func TestUpdate(t *testing.T) { RegisterMockTestingT(t) client := mocks.NewMockClientProxy() s := events.DefaultCommitStatusUpdater{Client: client} - err := s.Update(repoModel, pullModel, status, &cmd) + err := s.Update(repoModel, pullModel, status, events.Plan) Ok(t, err) client.VerifyWasCalledOnce().UpdateStatus(repoModel, pullModel, status, "Plan Success") } @@ -58,11 +44,10 @@ func TestUpdateProjectResult_Error(t *testing.T) { ctx := &events.CommandContext{ BaseRepo: repoModel, Pull: pullModel, - Command: &events.Command{Name: events.Plan}, } client := mocks.NewMockClientProxy() s := events.DefaultCommitStatusUpdater{Client: client} - err := s.UpdateProjectResult(ctx, events.CommandResponse{Error: errors.New("err")}) + err := s.UpdateProjectResult(ctx, events.Plan, events.CommandResult{Error: errors.New("err")}) Ok(t, err) client.VerifyWasCalledOnce().UpdateStatus(repoModel, pullModel, vcs.Failed, "Plan Failed") } @@ -72,23 +57,20 @@ func TestUpdateProjectResult_Failure(t *testing.T) { ctx := &events.CommandContext{ BaseRepo: repoModel, Pull: pullModel, - Command: &events.Command{Name: events.Plan}, } client := mocks.NewMockClientProxy() s := events.DefaultCommitStatusUpdater{Client: client} - err := s.UpdateProjectResult(ctx, events.CommandResponse{Failure: "failure"}) + err := s.UpdateProjectResult(ctx, events.Plan, events.CommandResult{Failure: "failure"}) Ok(t, err) client.VerifyWasCalledOnce().UpdateStatus(repoModel, pullModel, vcs.Failed, "Plan Failed") } func TestUpdateProjectResult(t *testing.T) { - t.Log("should use worst status") RegisterMockTestingT(t) ctx := &events.CommandContext{ BaseRepo: repoModel, Pull: pullModel, - Command: &events.Command{Name: events.Plan}, } cases := []struct { @@ -126,25 +108,31 @@ func TestUpdateProjectResult(t *testing.T) { } for _, c := range cases { - var results []events.ProjectResult - for _, statusStr := range c.Statuses { - var result events.ProjectResult - switch statusStr { - case "failure": - result = events.ProjectResult{Failure: "failure"} - case "error": - result = events.ProjectResult{Error: errors.New("err")} - default: - result = events.ProjectResult{} + t.Run(strings.Join(c.Statuses, "-"), func(t *testing.T) { + var results []events.ProjectResult + for _, statusStr := range c.Statuses { + var result events.ProjectResult + switch statusStr { + case "failure": + result = events.ProjectResult{ + ProjectCommandResult: events.ProjectCommandResult{Failure: "failure"}, + } + case "error": + result = events.ProjectResult{ + ProjectCommandResult: events.ProjectCommandResult{Error: errors.New("err")}, + } + default: + result = events.ProjectResult{} + } + results = append(results, result) } - results = append(results, result) - } - resp := events.CommandResponse{ProjectResults: results} + resp := events.CommandResult{ProjectResults: results} - client := mocks.NewMockClientProxy() - s := events.DefaultCommitStatusUpdater{Client: client} - err := s.UpdateProjectResult(ctx, resp) - Ok(t, err) - client.VerifyWasCalledOnce().UpdateStatus(repoModel, pullModel, c.Expected, "Plan "+strings.Title(c.Expected.String())) + client := mocks.NewMockClientProxy() + s := events.DefaultCommitStatusUpdater{Client: client} + err := s.UpdateProjectResult(ctx, events.Plan, resp) + Ok(t, err) + client.VerifyWasCalledOnce().UpdateStatus(repoModel, pullModel, c.Expected, "Plan "+strings.Title(c.Expected.String())) + }) } } diff --git a/server/events/event_parser.go b/server/events/event_parser.go index dbdccc8e5..3bf4e7f15 100644 --- a/server/events/event_parser.go +++ b/server/events/event_parser.go @@ -34,7 +34,27 @@ var multiLineRegex = regexp.MustCompile(`.*\r?\n.+`) //go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_event_parsing.go EventParsing -type Command struct { +type CommandInterface interface { + CommandName() CommandName + IsVerbose() bool + IsAutoplan() bool +} + +type AutoplanCommand struct{} + +func (c AutoplanCommand) CommandName() CommandName { + return Plan +} + +func (c AutoplanCommand) IsVerbose() bool { + return false +} + +func (c AutoplanCommand) IsAutoplan() bool { + return true +} + +type CommentCommand struct { // Dir is the path relative to the repo root to run the command in. // Will never be an empty string and will never end in "/". Dir string @@ -44,20 +64,29 @@ type Command struct { Name CommandName Verbose bool Workspace string - // Autoplan is true if the command is a plan command being executed in an - // attempt to automatically run plan. - Autoplan bool // ProjectName is the name of a project to run the command on. It refers to a // project specified in an atlantis.yaml file. ProjectName string } -func (c Command) String() string { - return fmt.Sprintf("command=%q verbose=%t dir=%q workspace=%q project=%q autoplan=%t flags=%q", c.Name.String(), c.Verbose, c.Dir, c.Workspace, c.ProjectName, c.Autoplan, strings.Join(c.Flags, ",")) +func (c CommentCommand) CommandName() CommandName { + return c.Name +} + +func (c CommentCommand) IsVerbose() bool { + return c.Verbose +} + +func (c CommentCommand) IsAutoplan() bool { + return false +} + +func (c CommentCommand) String() string { + return fmt.Sprintf("command=%q verbose=%t dir=%q workspace=%q project=%q flags=%q", c.Name.String(), c.Verbose, c.Dir, c.Workspace, c.ProjectName, strings.Join(c.Flags, ",")) } // NewCommand constructs a Command, setting all missing fields to defaults. -func NewCommand(dir string, flags []string, name CommandName, verbose bool, workspace string, project string, autoplan bool) *Command { +func NewCommand(dir string, flags []string, name CommandName, verbose bool, workspace string, project string) *CommentCommand { // If dir was an empty string, this will return '.'. validDir := path.Clean(dir) if validDir == "/" { @@ -66,24 +95,27 @@ func NewCommand(dir string, flags []string, name CommandName, verbose bool, work if workspace == "" { workspace = DefaultWorkspace } - return &Command{ + return &CommentCommand{ Dir: validDir, Flags: flags, Name: name, Verbose: verbose, Workspace: workspace, - Autoplan: autoplan, ProjectName: project, } } type EventParsing interface { ParseGithubIssueCommentEvent(comment *github.IssueCommentEvent) (baseRepo models.Repo, user models.User, pullNum int, err error) - // ParseGithubPull returns the pull request and head repo. - ParseGithubPull(pull *github.PullRequest) (models.PullRequest, models.Repo, error) + // ParseGithubPull returns the pull request, base repo and head repo. + ParseGithubPull(pull *github.PullRequest) (models.PullRequest, models.Repo, models.Repo, error) + // ParseGithubPullEvent returns the pull request, head repo and user that + // caused the event. Base repo is available as a field on PullRequest. + ParseGithubPullEvent(pullEvent *github.PullRequestEvent) (pull models.PullRequest, baseRepo models.Repo, headRepo models.Repo, user models.User, err error) ParseGithubRepo(ghRepo *github.Repository) (models.Repo, error) - // ParseGitlabMergeEvent returns the pull request, base repo and head repo. - ParseGitlabMergeEvent(event gitlab.MergeEvent) (models.PullRequest, models.Repo, models.Repo, error) + // ParseGitlabMergeEvent returns the pull request, base repo, head repo and + // user that caused the event. + ParseGitlabMergeEvent(event gitlab.MergeEvent) (models.PullRequest, models.Repo, models.Repo, models.User, error) ParseGitlabMergeCommentEvent(event gitlab.MergeCommentEvent) (baseRepo models.Repo, headRepo models.Repo, user models.User, err error) ParseGitlabMergeRequest(mr *gitlab.MergeRequest, baseRepo models.Repo) models.PullRequest } @@ -116,38 +148,58 @@ func (e *EventParser) ParseGithubIssueCommentEvent(comment *github.IssueCommentE return } -func (e *EventParser) ParseGithubPull(pull *github.PullRequest) (models.PullRequest, models.Repo, error) { - var pullModel models.PullRequest - var headRepoModel models.Repo +func (e *EventParser) ParseGithubPullEvent(pullEvent *github.PullRequestEvent) (models.PullRequest, models.Repo, models.Repo, models.User, error) { + if pullEvent.PullRequest == nil { + return models.PullRequest{}, models.Repo{}, models.Repo{}, models.User{}, errors.New("pull_request is null") + } + pull, baseRepo, headRepo, err := e.ParseGithubPull(pullEvent.PullRequest) + if err != nil { + return models.PullRequest{}, models.Repo{}, models.Repo{}, models.User{}, err + } + if pullEvent.Sender == nil { + return models.PullRequest{}, models.Repo{}, models.Repo{}, models.User{}, errors.New("sender is null") + } + senderUsername := pullEvent.Sender.GetLogin() + if senderUsername == "" { + return models.PullRequest{}, models.Repo{}, models.Repo{}, models.User{}, errors.New("sender.login is null") + } + return pull, baseRepo, headRepo, models.User{Username: senderUsername}, nil +} +func (e *EventParser) ParseGithubPull(pull *github.PullRequest) (pullModel models.PullRequest, baseRepo models.Repo, headRepo models.Repo, err error) { commit := pull.Head.GetSHA() if commit == "" { - return pullModel, headRepoModel, errors.New("head.sha is null") + err = errors.New("head.sha is null") + return } url := pull.GetHTMLURL() if url == "" { - return pullModel, headRepoModel, errors.New("html_url is null") + err = errors.New("html_url is null") + return } branch := pull.Head.GetRef() if branch == "" { - return pullModel, headRepoModel, errors.New("head.ref is null") + err = errors.New("head.ref is null") + return } authorUsername := pull.User.GetLogin() if authorUsername == "" { - return pullModel, headRepoModel, errors.New("user.login is null") + err = errors.New("user.login is null") + return } num := pull.GetNumber() if num == 0 { - return pullModel, headRepoModel, errors.New("number is null") + err = errors.New("number is null") + return } - baseRepoModel, err := e.ParseGithubRepo(pull.Base.Repo) + baseRepo, err = e.ParseGithubRepo(pull.Base.Repo) if err != nil { - return pullModel, headRepoModel, err + return } - headRepoModel, err = e.ParseGithubRepo(pull.Head.Repo) + headRepo, err = e.ParseGithubRepo(pull.Head.Repo) if err != nil { - return pullModel, headRepoModel, err + return } pullState := models.Closed @@ -155,22 +207,23 @@ func (e *EventParser) ParseGithubPull(pull *github.PullRequest) (models.PullRequ pullState = models.Open } - return models.PullRequest{ + pullModel = models.PullRequest{ Author: authorUsername, Branch: branch, HeadCommit: commit, URL: url, Num: num, State: pullState, - BaseRepo: baseRepoModel, - }, headRepoModel, nil + BaseRepo: baseRepo, + } + return } func (e *EventParser) ParseGithubRepo(ghRepo *github.Repository) (models.Repo, error) { return models.NewRepo(models.Github, ghRepo.GetFullName(), ghRepo.GetCloneURL(), e.GithubUser, e.GithubToken) } -func (e *EventParser) ParseGitlabMergeEvent(event gitlab.MergeEvent) (models.PullRequest, models.Repo, models.Repo, error) { +func (e *EventParser) ParseGitlabMergeEvent(event gitlab.MergeEvent) (models.PullRequest, models.Repo, models.Repo, models.User, error) { modelState := models.Closed if event.ObjectAttributes.State == gitlabPullOpened { modelState = models.Open @@ -180,11 +233,11 @@ func (e *EventParser) ParseGitlabMergeEvent(event gitlab.MergeEvent) (models.Pul baseRepo, err := models.NewRepo(models.Gitlab, event.Project.PathWithNamespace, event.Project.GitHTTPURL, e.GitlabUser, e.GitlabToken) if err != nil { - return models.PullRequest{}, models.Repo{}, models.Repo{}, err + return models.PullRequest{}, models.Repo{}, models.Repo{}, models.User{}, err } headRepo, err := models.NewRepo(models.Gitlab, event.ObjectAttributes.Source.PathWithNamespace, event.ObjectAttributes.Source.GitHTTPURL, e.GitlabUser, e.GitlabToken) if err != nil { - return models.PullRequest{}, models.Repo{}, models.Repo{}, err + return models.PullRequest{}, models.Repo{}, models.Repo{}, models.User{}, err } pull := models.PullRequest{ @@ -197,7 +250,11 @@ func (e *EventParser) ParseGitlabMergeEvent(event gitlab.MergeEvent) (models.Pul BaseRepo: baseRepo, } - return pull, baseRepo, headRepo, err + user := models.User{ + Username: event.User.Username, + } + + return pull, baseRepo, headRepo, user, err } // ParseGitlabMergeCommentEvent creates Atlantis models out of a GitLab event. diff --git a/server/events/event_parser_test.go b/server/events/event_parser_test.go index 1415ffba3..d7ef7fce7 100644 --- a/server/events/event_parser_test.go +++ b/server/events/event_parser_test.go @@ -102,34 +102,40 @@ func TestParseGithubIssueCommentEvent(t *testing.T) { Equals(t, *comment.Issue.Number, pullNum) } -func TestParseGithubPull(t *testing.T) { - testPull := deepcopy.Copy(Pull).(github.PullRequest) - testPull.Head.SHA = nil - _, _, err := parser.ParseGithubPull(&testPull) - ErrEquals(t, "head.sha is null", err) +func TestParseGithubPullEvent(t *testing.T) { + _, _, _, _, err := parser.ParseGithubPullEvent(&github.PullRequestEvent{}) + ErrEquals(t, "pull_request is null", err) - testPull = deepcopy.Copy(Pull).(github.PullRequest) - testPull.HTMLURL = nil - _, _, err = parser.ParseGithubPull(&testPull) + testEvent := deepcopy.Copy(PullEvent).(github.PullRequestEvent) + testEvent.PullRequest.HTMLURL = nil + _, _, _, _, err = parser.ParseGithubPullEvent(&testEvent) ErrEquals(t, "html_url is null", err) - testPull = deepcopy.Copy(Pull).(github.PullRequest) - testPull.Head.Ref = nil - _, _, err = parser.ParseGithubPull(&testPull) - ErrEquals(t, "head.ref is null", err) + testEvent = deepcopy.Copy(PullEvent).(github.PullRequestEvent) + testEvent.Sender = nil + _, _, _, _, err = parser.ParseGithubPullEvent(&testEvent) + ErrEquals(t, "sender is null", err) - testPull = deepcopy.Copy(Pull).(github.PullRequest) - testPull.User.Login = nil - _, _, err = parser.ParseGithubPull(&testPull) - ErrEquals(t, "user.login is null", err) + testEvent = deepcopy.Copy(PullEvent).(github.PullRequestEvent) + testEvent.Sender.Login = nil + _, _, _, _, err = parser.ParseGithubPullEvent(&testEvent) + ErrEquals(t, "sender.login is null", err) - testPull = deepcopy.Copy(Pull).(github.PullRequest) - testPull.Number = nil - _, _, err = parser.ParseGithubPull(&testPull) - ErrEquals(t, "number is null", err) - - pullRes, _, err := parser.ParseGithubPull(&Pull) + actPull, actBaseRepo, actHeadRepo, actUser, err := parser.ParseGithubPullEvent(&PullEvent) Ok(t, err) + expBaseRepo := models.Repo{ + Owner: "owner", + FullName: "owner/repo", + CloneURL: "https://github-user:github-token@github.com/owner/repo.git", + SanitizedCloneURL: Repo.GetCloneURL(), + Name: "repo", + VCSHost: models.VCSHost{ + Hostname: "github.com", + Type: models.Github, + }, + } + Equals(t, expBaseRepo, actBaseRepo) + Equals(t, expBaseRepo, actHeadRepo) Equals(t, models.PullRequest{ URL: Pull.GetHTMLURL(), Author: Pull.User.GetLogin(), @@ -137,18 +143,61 @@ func TestParseGithubPull(t *testing.T) { HeadCommit: Pull.Head.GetSHA(), Num: Pull.GetNumber(), State: models.Open, - BaseRepo: models.Repo{ - Owner: "owner", - FullName: "owner/repo", - CloneURL: "https://github-user:github-token@github.com/owner/repo.git", - SanitizedCloneURL: Repo.GetCloneURL(), - Name: "repo", - VCSHost: models.VCSHost{ - Hostname: "github.com", - Type: models.Github, - }, + BaseRepo: expBaseRepo, + }, actPull) + Equals(t, models.User{Username: "user"}, actUser) +} + +func TestParseGithubPull(t *testing.T) { + testPull := deepcopy.Copy(Pull).(github.PullRequest) + testPull.Head.SHA = nil + _, _, _, err := parser.ParseGithubPull(&testPull) + ErrEquals(t, "head.sha is null", err) + + testPull = deepcopy.Copy(Pull).(github.PullRequest) + testPull.HTMLURL = nil + _, _, _, err = parser.ParseGithubPull(&testPull) + ErrEquals(t, "html_url is null", err) + + testPull = deepcopy.Copy(Pull).(github.PullRequest) + testPull.Head.Ref = nil + _, _, _, err = parser.ParseGithubPull(&testPull) + ErrEquals(t, "head.ref is null", err) + + testPull = deepcopy.Copy(Pull).(github.PullRequest) + testPull.User.Login = nil + _, _, _, err = parser.ParseGithubPull(&testPull) + ErrEquals(t, "user.login is null", err) + + testPull = deepcopy.Copy(Pull).(github.PullRequest) + testPull.Number = nil + _, _, _, err = parser.ParseGithubPull(&testPull) + ErrEquals(t, "number is null", err) + + pullRes, actBaseRepo, actHeadRepo, err := parser.ParseGithubPull(&Pull) + Ok(t, err) + expBaseRepo := models.Repo{ + Owner: "owner", + FullName: "owner/repo", + CloneURL: "https://github-user:github-token@github.com/owner/repo.git", + SanitizedCloneURL: Repo.GetCloneURL(), + Name: "repo", + VCSHost: models.VCSHost{ + Hostname: "github.com", + Type: models.Github, }, + } + Equals(t, models.PullRequest{ + URL: Pull.GetHTMLURL(), + Author: Pull.User.GetLogin(), + Branch: Pull.Head.GetRef(), + HeadCommit: Pull.Head.GetSHA(), + Num: Pull.GetNumber(), + State: models.Open, + BaseRepo: expBaseRepo, }, pullRes) + Equals(t, expBaseRepo, actBaseRepo) + Equals(t, expBaseRepo, actHeadRepo) } func TestParseGitlabMergeEvent(t *testing.T) { @@ -156,10 +205,10 @@ func TestParseGitlabMergeEvent(t *testing.T) { var event *gitlab.MergeEvent err := json.Unmarshal([]byte(mergeEventJSON), &event) Ok(t, err) - pull, repo, _, err := parser.ParseGitlabMergeEvent(*event) + pull, actBaseRepo, actHeadRepo, actUser, err := parser.ParseGitlabMergeEvent(*event) Ok(t, err) - expRepo := models.Repo{ + expBaseRepo := models.Repo{ FullName: "gitlabhq/gitlab-test", Name: "gitlab-test", SanitizedCloneURL: "https://example.com/gitlabhq/gitlab-test.git", @@ -178,14 +227,26 @@ func TestParseGitlabMergeEvent(t *testing.T) { HeadCommit: "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", Branch: "ms-viewport", State: models.Open, - BaseRepo: expRepo, + BaseRepo: expBaseRepo, }, pull) - Equals(t, expRepo, repo) + Equals(t, expBaseRepo, actBaseRepo) + Equals(t, models.Repo{ + FullName: "awesome_space/awesome_project", + Name: "awesome_project", + SanitizedCloneURL: "http://example.com/awesome_space/awesome_project.git", + Owner: "awesome_space", + CloneURL: "http://gitlab-user:gitlab-token@example.com/awesome_space/awesome_project.git", + VCSHost: models.VCSHost{ + Hostname: "example.com", + Type: models.Gitlab, + }, + }, actHeadRepo) + Equals(t, models.User{Username: "root"}, actUser) t.Log("If the state is closed, should set field correctly.") event.ObjectAttributes.State = "closed" - pull, _, _, err = parser.ParseGitlabMergeEvent(*event) + pull, _, _, _, err = parser.ParseGitlabMergeEvent(*event) Ok(t, err) Equals(t, models.Closed, pull.State) } @@ -283,20 +344,20 @@ func TestNewCommand_CleansDir(t *testing.T) { for _, c := range cases { t.Run(c.Dir, func(t *testing.T) { - cmd := events.NewCommand(c.Dir, nil, events.Plan, false, "workspace", "", false) + cmd := events.NewCommand(c.Dir, nil, events.Plan, false, "workspace", "") Equals(t, c.ExpDir, cmd.Dir) }) } } func TestNewCommand_EmptyWorkspace(t *testing.T) { - cmd := events.NewCommand("dir", nil, events.Plan, false, "", "", false) + cmd := events.NewCommand("dir", nil, events.Plan, false, "", "") Equals(t, "default", cmd.Workspace) } func TestNewCommand_AllFieldsSet(t *testing.T) { - cmd := events.NewCommand("dir", []string{"a", "b"}, events.Plan, true, "workspace", "project", false) - Equals(t, events.Command{ + cmd := events.NewCommand("dir", []string{"a", "b"}, events.Plan, true, "workspace", "project") + Equals(t, events.CommentCommand{ Workspace: "workspace", Dir: "dir", Verbose: true, @@ -306,6 +367,52 @@ func TestNewCommand_AllFieldsSet(t *testing.T) { }, *cmd) } +func TestAutoplanCommand_CommandName(t *testing.T) { + Equals(t, events.Plan, (events.AutoplanCommand{}).CommandName()) +} + +func TestAutoplanCommand_IsVerbose(t *testing.T) { + Equals(t, false, (events.AutoplanCommand{}).IsVerbose()) +} + +func TestAutoplanCommand_IsAutoplan(t *testing.T) { + Equals(t, true, (events.AutoplanCommand{}).IsAutoplan()) +} + +func TestCommentCommand_CommandName(t *testing.T) { + Equals(t, events.Plan, (events.CommentCommand{ + Name: events.Plan, + }).CommandName()) + Equals(t, events.Apply, (events.CommentCommand{ + Name: events.Apply, + }).CommandName()) +} + +func TestCommentCommand_IsVerbose(t *testing.T) { + Equals(t, false, (events.CommentCommand{ + Verbose: false, + }).IsVerbose()) + Equals(t, true, (events.CommentCommand{ + Verbose: true, + }).IsVerbose()) +} + +func TestCommentCommand_IsAutoplan(t *testing.T) { + Equals(t, false, (events.CommentCommand{}).IsAutoplan()) +} + +func TestCommentCommand_String(t *testing.T) { + exp := `command="plan" verbose=true dir="mydir" workspace="myworkspace" project="myproject" flags="flag1,flag2"` + Equals(t, exp, (events.CommentCommand{ + Dir: "mydir", + Flags: []string{"flag1", "flag2"}, + Name: events.Plan, + Verbose: true, + Workspace: "myworkspace", + ProjectName: "myproject", + }).String()) +} + var mergeEventJSON = `{ "object_kind": "merge_request", "user": { diff --git a/server/events/executor.go b/server/events/executor.go index 10df87a76..b308e9d02 100644 --- a/server/events/executor.go +++ b/server/events/executor.go @@ -18,5 +18,5 @@ package events // Executor is the generic interface implemented by each command type: // help, plan, and apply. type Executor interface { - Execute(ctx *CommandContext) CommandResponse + Execute(ctx *CommandContext) CommandResult } diff --git a/server/events/markdown_renderer.go b/server/events/markdown_renderer.go index acb0d8679..eb06c4ead 100644 --- a/server/events/markdown_renderer.go +++ b/server/events/markdown_renderer.go @@ -58,7 +58,7 @@ type ProjectResultTmplData struct { // Render formats the data into a markdown string. // nolint: interfacer -func (m *MarkdownRenderer) Render(res CommandResponse, cmdName CommandName, log string, verbose bool, autoplan bool) string { +func (m *MarkdownRenderer) Render(res CommandResult, cmdName CommandName, log string, verbose bool, autoplan bool) string { commandStr := strings.Title(cmdName.String()) common := CommonData{commandStr, verbose, log} if res.Error != nil { @@ -73,10 +73,10 @@ func (m *MarkdownRenderer) Render(res CommandResponse, cmdName CommandName, log return m.renderProjectResults(res.ProjectResults, common) } -func (m *MarkdownRenderer) renderProjectResults(pathResults []ProjectResult, common CommonData) string { +func (m *MarkdownRenderer) renderProjectResults(results []ProjectResult, common CommonData) string { var resultsTmplData []ProjectResultTmplData - for _, result := range pathResults { + for _, result := range results { resultData := ProjectResultTmplData{ Workspace: result.Workspace, Dir: result.Path, diff --git a/server/events/markdown_renderer_test.go b/server/events/markdown_renderer_test.go index 202097e9b..040c47731 100644 --- a/server/events/markdown_renderer_test.go +++ b/server/events/markdown_renderer_test.go @@ -45,18 +45,20 @@ func TestRenderErr(t *testing.T) { r := events.MarkdownRenderer{} for _, c := range cases { - res := events.CommandResponse{ - Error: c.Error, - } - for _, verbose := range []bool{true, false} { - t.Log("testing " + c.Description) - s := r.Render(res, c.Command, "log", verbose, false) - if !verbose { - Equals(t, c.Expected, s) - } else { - Equals(t, c.Expected+"
Log\n

\n\n```\nlog```\n

\n", s) + t.Run(c.Description, func(t *testing.T) { + res := events.CommandResult{ + Error: c.Error, } - } + for _, verbose := range []bool{true, false} { + t.Log("testing " + c.Description) + s := r.Render(res, c.Command, "log", verbose, false) + if !verbose { + Equals(t, c.Expected, s) + } else { + Equals(t, c.Expected+"
Log\n

\n\n```\nlog```\n

\n", s) + } + } + }) } } @@ -83,25 +85,27 @@ func TestRenderFailure(t *testing.T) { r := events.MarkdownRenderer{} for _, c := range cases { - res := events.CommandResponse{ - Failure: c.Failure, - } - for _, verbose := range []bool{true, false} { - t.Log("testing " + c.Description) - s := r.Render(res, c.Command, "log", verbose, false) - if !verbose { - Equals(t, c.Expected, s) - } else { - Equals(t, c.Expected+"
Log\n

\n\n```\nlog```\n

\n", s) + t.Run(c.Description, func(t *testing.T) { + res := events.CommandResult{ + Failure: c.Failure, } - } + for _, verbose := range []bool{true, false} { + t.Log("testing " + c.Description) + s := r.Render(res, c.Command, "log", verbose, false) + if !verbose { + Equals(t, c.Expected, s) + } else { + Equals(t, c.Expected+"
Log\n

\n\n```\nlog```\n

\n", s) + } + } + }) } } func TestRenderErrAndFailure(t *testing.T) { t.Log("if there is an error and a failure, the error should be printed") r := events.MarkdownRenderer{} - res := events.CommandResponse{ + res := events.CommandResult{ Error: errors.New("error"), Failure: "failure", } @@ -109,6 +113,15 @@ func TestRenderErrAndFailure(t *testing.T) { Equals(t, "**Plan Error**\n```\nerror\n```\n\n", s) } +func TestRenderAutoplanNoResults(t *testing.T) { + // If there are no project results during an autoplan we should still comment + // back because the user might expect some output. + r := events.MarkdownRenderer{} + res := events.CommandResult{} + s := r.Render(res, events.Plan, "", false, true) + Equals(t, "Ran `plan` in 0 projects because Atlantis detected no Terraform changes or could not determine where to run `plan`.\n\n", s) +} + func TestRenderProjectResults(t *testing.T) { cases := []struct { Description string @@ -121,9 +134,11 @@ func TestRenderProjectResults(t *testing.T) { events.Plan, []events.ProjectResult{ { - PlanSuccess: &events.PlanSuccess{ - TerraformOutput: "terraform-output", - LockURL: "lock-url", + ProjectCommandResult: events.ProjectCommandResult{ + PlanSuccess: &events.PlanSuccess{ + TerraformOutput: "terraform-output", + LockURL: "lock-url", + }, }, Workspace: "workspace", Path: "path", @@ -136,9 +151,11 @@ func TestRenderProjectResults(t *testing.T) { events.Apply, []events.ProjectResult{ { - ApplySuccess: "success", - Workspace: "workspace", - Path: "path", + ProjectCommandResult: events.ProjectCommandResult{ + ApplySuccess: "success", + }, + Workspace: "workspace", + Path: "path", }, }, "Ran Apply in dir: `path` workspace: `workspace`\n```diff\nsuccess\n```\n\n", @@ -150,17 +167,21 @@ func TestRenderProjectResults(t *testing.T) { { Workspace: "workspace", Path: "path", - PlanSuccess: &events.PlanSuccess{ - TerraformOutput: "terraform-output", - LockURL: "lock-url", + ProjectCommandResult: events.ProjectCommandResult{ + PlanSuccess: &events.PlanSuccess{ + TerraformOutput: "terraform-output", + LockURL: "lock-url", + }, }, }, { Workspace: "workspace", Path: "path2", - PlanSuccess: &events.PlanSuccess{ - TerraformOutput: "terraform-output2", - LockURL: "lock-url2", + ProjectCommandResult: events.ProjectCommandResult{ + PlanSuccess: &events.PlanSuccess{ + TerraformOutput: "terraform-output2", + LockURL: "lock-url2", + }, }, }, }, @@ -171,14 +192,18 @@ func TestRenderProjectResults(t *testing.T) { events.Apply, []events.ProjectResult{ { - Path: "path", - Workspace: "workspace", - ApplySuccess: "success", + Path: "path", + Workspace: "workspace", + ProjectCommandResult: events.ProjectCommandResult{ + ApplySuccess: "success", + }, }, { - Path: "path2", - Workspace: "workspace", - ApplySuccess: "success2", + Path: "path2", + Workspace: "workspace", + ProjectCommandResult: events.ProjectCommandResult{ + ApplySuccess: "success2", + }, }, }, "Ran Apply for 2 projects:\n1. workspace: `workspace` path: `path`\n1. workspace: `workspace` path: `path2`\n\n### 1. workspace: `workspace` path: `path`\n```diff\nsuccess\n```\n---\n### 2. workspace: `workspace` path: `path2`\n```diff\nsuccess2\n```\n---\n\n", @@ -188,7 +213,9 @@ func TestRenderProjectResults(t *testing.T) { events.Plan, []events.ProjectResult{ { - Error: errors.New("error"), + ProjectCommandResult: events.ProjectCommandResult{ + Error: errors.New("error"), + }, Path: "path", Workspace: "workspace", }, @@ -202,7 +229,9 @@ func TestRenderProjectResults(t *testing.T) { { Path: "path", Workspace: "workspace", - Failure: "failure", + ProjectCommandResult: events.ProjectCommandResult{ + Failure: "failure", + }, }, }, "Ran Plan in dir: `path` workspace: `workspace`\n**Plan Failed**: failure\n\n\n", @@ -214,20 +243,26 @@ func TestRenderProjectResults(t *testing.T) { { Workspace: "workspace", Path: "path", - PlanSuccess: &events.PlanSuccess{ - TerraformOutput: "terraform-output", - LockURL: "lock-url", + ProjectCommandResult: events.ProjectCommandResult{ + PlanSuccess: &events.PlanSuccess{ + TerraformOutput: "terraform-output", + LockURL: "lock-url", + }, }, }, { Workspace: "workspace", Path: "path2", - Failure: "failure", + ProjectCommandResult: events.ProjectCommandResult{ + Failure: "failure", + }, }, { Workspace: "workspace", Path: "path3", - Error: errors.New("error"), + ProjectCommandResult: events.ProjectCommandResult{ + Error: errors.New("error"), + }, }, }, "Ran Plan for 3 projects:\n1. workspace: `workspace` path: `path`\n1. workspace: `workspace` path: `path2`\n1. workspace: `workspace` path: `path3`\n\n### 1. workspace: `workspace` path: `path`\n```diff\nterraform-output\n```\n\n* To **discard** this plan click [here](lock-url).\n---\n### 2. workspace: `workspace` path: `path2`\n**Plan Failed**: failure\n\n---\n### 3. workspace: `workspace` path: `path3`\n**Plan Error**\n```\nerror\n```\n\n---\n\n", @@ -237,19 +272,25 @@ func TestRenderProjectResults(t *testing.T) { events.Apply, []events.ProjectResult{ { - Workspace: "workspace", - Path: "path", - ApplySuccess: "success", + Workspace: "workspace", + Path: "path", + ProjectCommandResult: events.ProjectCommandResult{ + ApplySuccess: "success", + }, }, { Workspace: "workspace", Path: "path2", - Failure: "failure", + ProjectCommandResult: events.ProjectCommandResult{ + Failure: "failure", + }, }, { Workspace: "workspace", Path: "path3", - Error: errors.New("error"), + ProjectCommandResult: events.ProjectCommandResult{ + Error: errors.New("error"), + }, }, }, "Ran Apply for 3 projects:\n1. workspace: `workspace` path: `path`\n1. workspace: `workspace` path: `path2`\n1. workspace: `workspace` path: `path3`\n\n### 1. workspace: `workspace` path: `path`\n```diff\nsuccess\n```\n---\n### 2. workspace: `workspace` path: `path2`\n**Apply Failed**: failure\n\n---\n### 3. workspace: `workspace` path: `path3`\n**Apply Error**\n```\nerror\n```\n\n---\n\n", @@ -258,18 +299,20 @@ func TestRenderProjectResults(t *testing.T) { r := events.MarkdownRenderer{} for _, c := range cases { - res := events.CommandResponse{ - ProjectResults: c.ProjectResults, - } - for _, verbose := range []bool{true, false} { - t.Run(c.Description, func(t *testing.T) { - s := r.Render(res, c.Command, "log", verbose, false) - if !verbose { - Equals(t, c.Expected, s) - } else { - Equals(t, c.Expected+"
Log\n

\n\n```\nlog```\n

\n", s) - } - }) - } + t.Run(c.Description, func(t *testing.T) { + res := events.CommandResult{ + ProjectResults: c.ProjectResults, + } + for _, verbose := range []bool{true, false} { + t.Run(c.Description, func(t *testing.T) { + s := r.Render(res, c.Command, "log", verbose, false) + if !verbose { + Equals(t, c.Expected, s) + } else { + Equals(t, c.Expected+"
Log\n

\n\n```\nlog```\n

\n", s) + } + }) + } + }) } } diff --git a/server/events/mocks/matchers/events_commandresponse.go b/server/events/mocks/matchers/events_commandname.go similarity index 53% rename from server/events/mocks/matchers/events_commandresponse.go rename to server/events/mocks/matchers/events_commandname.go index f596b2c4d..448c937ab 100644 --- a/server/events/mocks/matchers/events_commandresponse.go +++ b/server/events/mocks/matchers/events_commandname.go @@ -7,14 +7,14 @@ import ( events "github.com/runatlantis/atlantis/server/events" ) -func AnyEventsCommandResponse() events.CommandResponse { - pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(events.CommandResponse))(nil)).Elem())) - var nullValue events.CommandResponse +func AnyEventsCommandName() events.CommandName { + pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(events.CommandName))(nil)).Elem())) + var nullValue events.CommandName return nullValue } -func EqEventsCommandResponse(value events.CommandResponse) events.CommandResponse { +func EqEventsCommandName(value events.CommandName) events.CommandName { pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) - var nullValue events.CommandResponse + var nullValue events.CommandName return nullValue } diff --git a/server/events/mocks/matchers/events_commandresult.go b/server/events/mocks/matchers/events_commandresult.go new file mode 100644 index 000000000..54269ef12 --- /dev/null +++ b/server/events/mocks/matchers/events_commandresult.go @@ -0,0 +1,20 @@ +package matchers + +import ( + "reflect" + + "github.com/petergtz/pegomock" + events "github.com/runatlantis/atlantis/server/events" +) + +func AnyEventsCommandResult() events.CommandResult { + pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(events.CommandResult))(nil)).Elem())) + var nullValue events.CommandResult + return nullValue +} + +func EqEventsCommandResult(value events.CommandResult) events.CommandResult { + pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) + var nullValue events.CommandResult + return nullValue +} diff --git a/server/events/mocks/matchers/models_projectcommandcontext.go b/server/events/mocks/matchers/models_projectcommandcontext.go new file mode 100644 index 000000000..3f76b9a22 --- /dev/null +++ b/server/events/mocks/matchers/models_projectcommandcontext.go @@ -0,0 +1,20 @@ +package matchers + +import ( + "reflect" + + "github.com/petergtz/pegomock" + models "github.com/runatlantis/atlantis/server/events/models" +) + +func AnyModelsProjectCommandContext() models.ProjectCommandContext { + pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(models.ProjectCommandContext))(nil)).Elem())) + var nullValue models.ProjectCommandContext + return nullValue +} + +func EqModelsProjectCommandContext(value models.ProjectCommandContext) models.ProjectCommandContext { + pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) + var nullValue models.ProjectCommandContext + return nullValue +} diff --git a/server/events/mocks/matchers/ptr_to_events_command.go b/server/events/mocks/matchers/ptr_to_events_command.go index 91edd3b66..771aacf30 100644 --- a/server/events/mocks/matchers/ptr_to_events_command.go +++ b/server/events/mocks/matchers/ptr_to_events_command.go @@ -7,14 +7,14 @@ import ( events "github.com/runatlantis/atlantis/server/events" ) -func AnyPtrToEventsCommand() *events.Command { - pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(*events.Command))(nil)).Elem())) - var nullValue *events.Command +func AnyPtrToEventsCommand() *events.CommentCommand { + pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(*events.CommentCommand))(nil)).Elem())) + var nullValue *events.CommentCommand return nullValue } -func EqPtrToEventsCommand(value *events.Command) *events.Command { +func EqPtrToEventsCommand(value *events.CommentCommand) *events.CommentCommand { pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) - var nullValue *events.Command + var nullValue *events.CommentCommand return nullValue } diff --git a/server/events/mocks/matchers/ptr_to_events_commentcommand.go b/server/events/mocks/matchers/ptr_to_events_commentcommand.go new file mode 100644 index 000000000..fbbbfcc15 --- /dev/null +++ b/server/events/mocks/matchers/ptr_to_events_commentcommand.go @@ -0,0 +1,20 @@ +package matchers + +import ( + "reflect" + + "github.com/petergtz/pegomock" + events "github.com/runatlantis/atlantis/server/events" +) + +func AnyPtrToEventsCommentCommand() *events.CommentCommand { + pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(*events.CommentCommand))(nil)).Elem())) + var nullValue *events.CommentCommand + return nullValue +} + +func EqPtrToEventsCommentCommand(value *events.CommentCommand) *events.CommentCommand { + pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) + var nullValue *events.CommentCommand + return nullValue +} diff --git a/server/events/mocks/matchers/ptr_to_github_pullrequestevent.go b/server/events/mocks/matchers/ptr_to_github_pullrequestevent.go new file mode 100644 index 000000000..1952cf1f7 --- /dev/null +++ b/server/events/mocks/matchers/ptr_to_github_pullrequestevent.go @@ -0,0 +1,20 @@ +package matchers + +import ( + "reflect" + + github "github.com/google/go-github/github" + "github.com/petergtz/pegomock" +) + +func AnyPtrToGithubPullRequestEvent() *github.PullRequestEvent { + pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(*github.PullRequestEvent))(nil)).Elem())) + var nullValue *github.PullRequestEvent + return nullValue +} + +func EqPtrToGithubPullRequestEvent(value *github.PullRequestEvent) *github.PullRequestEvent { + pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) + var nullValue *github.PullRequestEvent + return nullValue +} diff --git a/server/events/mocks/matchers/ptr_to_models_repo.go b/server/events/mocks/matchers/ptr_to_models_repo.go new file mode 100644 index 000000000..05ba1aef3 --- /dev/null +++ b/server/events/mocks/matchers/ptr_to_models_repo.go @@ -0,0 +1,20 @@ +package matchers + +import ( + "reflect" + + "github.com/petergtz/pegomock" + models "github.com/runatlantis/atlantis/server/events/models" +) + +func AnyPtrToModelsRepo() *models.Repo { + pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(*models.Repo))(nil)).Elem())) + var nullValue *models.Repo + return nullValue +} + +func EqPtrToModelsRepo(value *models.Repo) *models.Repo { + pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) + var nullValue *models.Repo + return nullValue +} diff --git a/server/events/mocks/matchers/slice_of_models_projectcommandcontext.go b/server/events/mocks/matchers/slice_of_models_projectcommandcontext.go new file mode 100644 index 000000000..08974c59c --- /dev/null +++ b/server/events/mocks/matchers/slice_of_models_projectcommandcontext.go @@ -0,0 +1,20 @@ +package matchers + +import ( + "reflect" + + "github.com/petergtz/pegomock" + models "github.com/runatlantis/atlantis/server/events/models" +) + +func AnySliceOfModelsProjectCommandContext() []models.ProjectCommandContext { + pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*([]models.ProjectCommandContext))(nil)).Elem())) + var nullValue []models.ProjectCommandContext + return nullValue +} + +func EqSliceOfModelsProjectCommandContext(value []models.ProjectCommandContext) []models.ProjectCommandContext { + pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) + var nullValue []models.ProjectCommandContext + return nullValue +} diff --git a/server/events/mocks/mock_command_runner.go b/server/events/mocks/mock_command_runner.go index 481fcf72f..8ef3387c0 100644 --- a/server/events/mocks/mock_command_runner.go +++ b/server/events/mocks/mock_command_runner.go @@ -19,9 +19,14 @@ func NewMockCommandRunner() *MockCommandRunner { return &MockCommandRunner{fail: pegomock.GlobalFailHandler} } -func (mock *MockCommandRunner) ExecuteCommand(baseRepo models.Repo, headRepo models.Repo, user models.User, pullNum int, cmd *events.Command) { - params := []pegomock.Param{baseRepo, headRepo, user, pullNum, cmd} - pegomock.GetGenericMockFrom(mock).Invoke("ExecuteCommand", params, []reflect.Type{}) +func (mock *MockCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHeadRepo *models.Repo, user models.User, pullNum int, cmd *events.CommentCommand) { + params := []pegomock.Param{baseRepo, maybeHeadRepo, user, pullNum, cmd} + pegomock.GetGenericMockFrom(mock).Invoke("RunCommentCommand", params, []reflect.Type{}) +} + +func (mock *MockCommandRunner) RunAutoplanCommand(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) { + params := []pegomock.Param{baseRepo, headRepo, pull, user} + pegomock.GetGenericMockFrom(mock).Invoke("RunAutoplanCommand", params, []reflect.Type{}) } func (mock *MockCommandRunner) VerifyWasCalledOnce() *VerifierCommandRunner { @@ -42,23 +47,66 @@ type VerifierCommandRunner struct { inOrderContext *pegomock.InOrderContext } -func (verifier *VerifierCommandRunner) ExecuteCommand(baseRepo models.Repo, headRepo models.Repo, user models.User, pullNum int, cmd *events.Command) *CommandRunner_ExecuteCommand_OngoingVerification { - params := []pegomock.Param{baseRepo, headRepo, user, pullNum, cmd} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ExecuteCommand", params) - return &CommandRunner_ExecuteCommand_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +func (verifier *VerifierCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHeadRepo *models.Repo, user models.User, pullNum int, cmd *events.CommentCommand) *CommandRunner_RunCommentCommand_OngoingVerification { + params := []pegomock.Param{baseRepo, maybeHeadRepo, user, pullNum, cmd} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "RunCommentCommand", params) + return &CommandRunner_RunCommentCommand_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } -type CommandRunner_ExecuteCommand_OngoingVerification struct { +type CommandRunner_RunCommentCommand_OngoingVerification struct { mock *MockCommandRunner methodInvocations []pegomock.MethodInvocation } -func (c *CommandRunner_ExecuteCommand_OngoingVerification) GetCapturedArguments() (models.Repo, models.Repo, models.User, int, *events.Command) { - baseRepo, headRepo, user, pullNum, cmd := c.GetAllCapturedArguments() - return baseRepo[len(baseRepo)-1], headRepo[len(headRepo)-1], user[len(user)-1], pullNum[len(pullNum)-1], cmd[len(cmd)-1] +func (c *CommandRunner_RunCommentCommand_OngoingVerification) GetCapturedArguments() (models.Repo, *models.Repo, models.User, int, *events.CommentCommand) { + baseRepo, maybeHeadRepo, user, pullNum, cmd := c.GetAllCapturedArguments() + return baseRepo[len(baseRepo)-1], maybeHeadRepo[len(maybeHeadRepo)-1], user[len(user)-1], pullNum[len(pullNum)-1], cmd[len(cmd)-1] } -func (c *CommandRunner_ExecuteCommand_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo, _param1 []models.Repo, _param2 []models.User, _param3 []int, _param4 []*events.Command) { +func (c *CommandRunner_RunCommentCommand_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo, _param1 []*models.Repo, _param2 []models.User, _param3 []int, _param4 []*events.CommentCommand) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]models.Repo, len(params[0])) + for u, param := range params[0] { + _param0[u] = param.(models.Repo) + } + _param1 = make([]*models.Repo, len(params[1])) + for u, param := range params[1] { + _param1[u] = param.(*models.Repo) + } + _param2 = make([]models.User, len(params[2])) + for u, param := range params[2] { + _param2[u] = param.(models.User) + } + _param3 = make([]int, len(params[3])) + for u, param := range params[3] { + _param3[u] = param.(int) + } + _param4 = make([]*events.CommentCommand, len(params[4])) + for u, param := range params[4] { + _param4[u] = param.(*events.CommentCommand) + } + } + return +} + +func (verifier *VerifierCommandRunner) RunAutoplanCommand(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) *CommandRunner_RunAutoplanCommand_OngoingVerification { + params := []pegomock.Param{baseRepo, headRepo, pull, user} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "RunAutoplanCommand", params) + return &CommandRunner_RunAutoplanCommand_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type CommandRunner_RunAutoplanCommand_OngoingVerification struct { + mock *MockCommandRunner + methodInvocations []pegomock.MethodInvocation +} + +func (c *CommandRunner_RunAutoplanCommand_OngoingVerification) GetCapturedArguments() (models.Repo, models.Repo, models.PullRequest, models.User) { + baseRepo, headRepo, pull, user := c.GetAllCapturedArguments() + return baseRepo[len(baseRepo)-1], headRepo[len(headRepo)-1], pull[len(pull)-1], user[len(user)-1] +} + +func (c *CommandRunner_RunAutoplanCommand_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo, _param1 []models.Repo, _param2 []models.PullRequest, _param3 []models.User) { params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) if len(params) > 0 { _param0 = make([]models.Repo, len(params[0])) @@ -69,17 +117,13 @@ func (c *CommandRunner_ExecuteCommand_OngoingVerification) GetAllCapturedArgumen for u, param := range params[1] { _param1[u] = param.(models.Repo) } - _param2 = make([]models.User, len(params[2])) + _param2 = make([]models.PullRequest, len(params[2])) for u, param := range params[2] { - _param2[u] = param.(models.User) + _param2[u] = param.(models.PullRequest) } - _param3 = make([]int, len(params[3])) + _param3 = make([]models.User, len(params[3])) for u, param := range params[3] { - _param3[u] = param.(int) - } - _param4 = make([]*events.Command, len(params[4])) - for u, param := range params[4] { - _param4[u] = param.(*events.Command) + _param3[u] = param.(models.User) } } return diff --git a/server/events/mocks/mock_commit_status_updater.go b/server/events/mocks/mock_commit_status_updater.go index 3ed9e933b..e5815d414 100644 --- a/server/events/mocks/mock_commit_status_updater.go +++ b/server/events/mocks/mock_commit_status_updater.go @@ -20,8 +20,8 @@ func NewMockCommitStatusUpdater() *MockCommitStatusUpdater { return &MockCommitStatusUpdater{fail: pegomock.GlobalFailHandler} } -func (mock *MockCommitStatusUpdater) Update(repo models.Repo, pull models.PullRequest, status vcs.CommitStatus, cmd *events.Command) error { - params := []pegomock.Param{repo, pull, status, cmd} +func (mock *MockCommitStatusUpdater) Update(repo models.Repo, pull models.PullRequest, status vcs.CommitStatus, command events.CommandName) error { + params := []pegomock.Param{repo, pull, status, command} result := pegomock.GetGenericMockFrom(mock).Invoke("Update", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) var ret0 error if len(result) != 0 { @@ -32,8 +32,8 @@ func (mock *MockCommitStatusUpdater) Update(repo models.Repo, pull models.PullRe return ret0 } -func (mock *MockCommitStatusUpdater) UpdateProjectResult(ctx *events.CommandContext, res events.CommandResponse) error { - params := []pegomock.Param{ctx, res} +func (mock *MockCommitStatusUpdater) UpdateProjectResult(ctx *events.CommandContext, commandName events.CommandName, res events.CommandResult) error { + params := []pegomock.Param{ctx, commandName, res} result := pegomock.GetGenericMockFrom(mock).Invoke("UpdateProjectResult", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) var ret0 error if len(result) != 0 { @@ -62,8 +62,8 @@ type VerifierCommitStatusUpdater struct { inOrderContext *pegomock.InOrderContext } -func (verifier *VerifierCommitStatusUpdater) Update(repo models.Repo, pull models.PullRequest, status vcs.CommitStatus, cmd *events.Command) *CommitStatusUpdater_Update_OngoingVerification { - params := []pegomock.Param{repo, pull, status, cmd} +func (verifier *VerifierCommitStatusUpdater) Update(repo models.Repo, pull models.PullRequest, status vcs.CommitStatus, command events.CommandName) *CommitStatusUpdater_Update_OngoingVerification { + params := []pegomock.Param{repo, pull, status, command} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Update", params) return &CommitStatusUpdater_Update_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } @@ -73,12 +73,12 @@ type CommitStatusUpdater_Update_OngoingVerification struct { methodInvocations []pegomock.MethodInvocation } -func (c *CommitStatusUpdater_Update_OngoingVerification) GetCapturedArguments() (models.Repo, models.PullRequest, vcs.CommitStatus, *events.Command) { - repo, pull, status, cmd := c.GetAllCapturedArguments() - return repo[len(repo)-1], pull[len(pull)-1], status[len(status)-1], cmd[len(cmd)-1] +func (c *CommitStatusUpdater_Update_OngoingVerification) GetCapturedArguments() (models.Repo, models.PullRequest, vcs.CommitStatus, events.CommandName) { + repo, pull, status, command := c.GetAllCapturedArguments() + return repo[len(repo)-1], pull[len(pull)-1], status[len(status)-1], command[len(command)-1] } -func (c *CommitStatusUpdater_Update_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo, _param1 []models.PullRequest, _param2 []vcs.CommitStatus, _param3 []*events.Command) { +func (c *CommitStatusUpdater_Update_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo, _param1 []models.PullRequest, _param2 []vcs.CommitStatus, _param3 []events.CommandName) { params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) if len(params) > 0 { _param0 = make([]models.Repo, len(params[0])) @@ -93,16 +93,16 @@ func (c *CommitStatusUpdater_Update_OngoingVerification) GetAllCapturedArguments for u, param := range params[2] { _param2[u] = param.(vcs.CommitStatus) } - _param3 = make([]*events.Command, len(params[3])) + _param3 = make([]events.CommandName, len(params[3])) for u, param := range params[3] { - _param3[u] = param.(*events.Command) + _param3[u] = param.(events.CommandName) } } return } -func (verifier *VerifierCommitStatusUpdater) UpdateProjectResult(ctx *events.CommandContext, res events.CommandResponse) *CommitStatusUpdater_UpdateProjectResult_OngoingVerification { - params := []pegomock.Param{ctx, res} +func (verifier *VerifierCommitStatusUpdater) UpdateProjectResult(ctx *events.CommandContext, commandName events.CommandName, res events.CommandResult) *CommitStatusUpdater_UpdateProjectResult_OngoingVerification { + params := []pegomock.Param{ctx, commandName, res} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "UpdateProjectResult", params) return &CommitStatusUpdater_UpdateProjectResult_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } @@ -112,21 +112,25 @@ type CommitStatusUpdater_UpdateProjectResult_OngoingVerification struct { methodInvocations []pegomock.MethodInvocation } -func (c *CommitStatusUpdater_UpdateProjectResult_OngoingVerification) GetCapturedArguments() (*events.CommandContext, events.CommandResponse) { - ctx, res := c.GetAllCapturedArguments() - return ctx[len(ctx)-1], res[len(res)-1] +func (c *CommitStatusUpdater_UpdateProjectResult_OngoingVerification) GetCapturedArguments() (*events.CommandContext, events.CommandName, events.CommandResult) { + ctx, commandName, res := c.GetAllCapturedArguments() + return ctx[len(ctx)-1], commandName[len(commandName)-1], res[len(res)-1] } -func (c *CommitStatusUpdater_UpdateProjectResult_OngoingVerification) GetAllCapturedArguments() (_param0 []*events.CommandContext, _param1 []events.CommandResponse) { +func (c *CommitStatusUpdater_UpdateProjectResult_OngoingVerification) GetAllCapturedArguments() (_param0 []*events.CommandContext, _param1 []events.CommandName, _param2 []events.CommandResult) { params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) if len(params) > 0 { _param0 = make([]*events.CommandContext, len(params[0])) for u, param := range params[0] { _param0[u] = param.(*events.CommandContext) } - _param1 = make([]events.CommandResponse, len(params[1])) + _param1 = make([]events.CommandName, len(params[1])) for u, param := range params[1] { - _param1[u] = param.(events.CommandResponse) + _param1[u] = param.(events.CommandName) + } + _param2 = make([]events.CommandResult, len(params[2])) + for u, param := range params[2] { + _param2[u] = param.(events.CommandResult) } } return diff --git a/server/events/mocks/mock_event_parsing.go b/server/events/mocks/mock_event_parsing.go index 007f823a4..1c1feaf31 100644 --- a/server/events/mocks/mock_event_parsing.go +++ b/server/events/mocks/mock_event_parsing.go @@ -44,45 +44,9 @@ func (mock *MockEventParsing) ParseGithubIssueCommentEvent(comment *github.Issue return ret0, ret1, ret2, ret3 } -func (mock *MockEventParsing) ParseGithubPull(pull *github.PullRequest) (models.PullRequest, models.Repo, error) { +func (mock *MockEventParsing) ParseGithubPull(pull *github.PullRequest) (models.PullRequest, models.Repo, models.Repo, error) { params := []pegomock.Param{pull} - result := pegomock.GetGenericMockFrom(mock).Invoke("ParseGithubPull", params, []reflect.Type{reflect.TypeOf((*models.PullRequest)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 models.PullRequest - var ret1 models.Repo - var ret2 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(models.PullRequest) - } - if result[1] != nil { - ret1 = result[1].(models.Repo) - } - if result[2] != nil { - ret2 = result[2].(error) - } - } - return ret0, ret1, ret2 -} - -func (mock *MockEventParsing) ParseGithubRepo(ghRepo *github.Repository) (models.Repo, error) { - params := []pegomock.Param{ghRepo} - result := pegomock.GetGenericMockFrom(mock).Invoke("ParseGithubRepo", params, []reflect.Type{reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 models.Repo - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(models.Repo) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockEventParsing) ParseGitlabMergeEvent(event go_gitlab.MergeEvent) (models.PullRequest, models.Repo, models.Repo, error) { - params := []pegomock.Param{event} - result := pegomock.GetGenericMockFrom(mock).Invoke("ParseGitlabMergeEvent", params, []reflect.Type{reflect.TypeOf((*models.PullRequest)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + result := pegomock.GetGenericMockFrom(mock).Invoke("ParseGithubPull", params, []reflect.Type{reflect.TypeOf((*models.PullRequest)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) var ret0 models.PullRequest var ret1 models.Repo var ret2 models.Repo @@ -104,6 +68,78 @@ func (mock *MockEventParsing) ParseGitlabMergeEvent(event go_gitlab.MergeEvent) return ret0, ret1, ret2, ret3 } +func (mock *MockEventParsing) ParseGithubPullEvent(pullEvent *github.PullRequestEvent) (models.PullRequest, models.Repo, models.Repo, models.User, error) { + params := []pegomock.Param{pullEvent} + result := pegomock.GetGenericMockFrom(mock).Invoke("ParseGithubPullEvent", params, []reflect.Type{reflect.TypeOf((*models.PullRequest)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.User)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 models.PullRequest + var ret1 models.Repo + var ret2 models.Repo + var ret3 models.User + var ret4 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(models.PullRequest) + } + if result[1] != nil { + ret1 = result[1].(models.Repo) + } + if result[2] != nil { + ret2 = result[2].(models.Repo) + } + if result[3] != nil { + ret3 = result[3].(models.User) + } + if result[4] != nil { + ret4 = result[4].(error) + } + } + return ret0, ret1, ret2, ret3, ret4 +} + +func (mock *MockEventParsing) ParseGithubRepo(ghRepo *github.Repository) (models.Repo, error) { + params := []pegomock.Param{ghRepo} + result := pegomock.GetGenericMockFrom(mock).Invoke("ParseGithubRepo", params, []reflect.Type{reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 models.Repo + var ret1 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(models.Repo) + } + if result[1] != nil { + ret1 = result[1].(error) + } + } + return ret0, ret1 +} + +func (mock *MockEventParsing) ParseGitlabMergeEvent(event go_gitlab.MergeEvent) (models.PullRequest, models.Repo, models.Repo, models.User, error) { + params := []pegomock.Param{event} + result := pegomock.GetGenericMockFrom(mock).Invoke("ParseGitlabMergeEvent", params, []reflect.Type{reflect.TypeOf((*models.PullRequest)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.User)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 models.PullRequest + var ret1 models.Repo + var ret2 models.Repo + var ret3 models.User + var ret4 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(models.PullRequest) + } + if result[1] != nil { + ret1 = result[1].(models.Repo) + } + if result[2] != nil { + ret2 = result[2].(models.Repo) + } + if result[3] != nil { + ret3 = result[3].(models.User) + } + if result[4] != nil { + ret4 = result[4].(error) + } + } + return ret0, ret1, ret2, ret3, ret4 +} + func (mock *MockEventParsing) ParseGitlabMergeCommentEvent(event go_gitlab.MergeCommentEvent) (models.Repo, models.Repo, models.User, error) { params := []pegomock.Param{event} result := pegomock.GetGenericMockFrom(mock).Invoke("ParseGitlabMergeCommentEvent", params, []reflect.Type{reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.User)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) @@ -212,6 +248,33 @@ func (c *EventParsing_ParseGithubPull_OngoingVerification) GetAllCapturedArgumen return } +func (verifier *VerifierEventParsing) ParseGithubPullEvent(pullEvent *github.PullRequestEvent) *EventParsing_ParseGithubPullEvent_OngoingVerification { + params := []pegomock.Param{pullEvent} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ParseGithubPullEvent", params) + return &EventParsing_ParseGithubPullEvent_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type EventParsing_ParseGithubPullEvent_OngoingVerification struct { + mock *MockEventParsing + methodInvocations []pegomock.MethodInvocation +} + +func (c *EventParsing_ParseGithubPullEvent_OngoingVerification) GetCapturedArguments() *github.PullRequestEvent { + pullEvent := c.GetAllCapturedArguments() + return pullEvent[len(pullEvent)-1] +} + +func (c *EventParsing_ParseGithubPullEvent_OngoingVerification) GetAllCapturedArguments() (_param0 []*github.PullRequestEvent) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]*github.PullRequestEvent, len(params[0])) + for u, param := range params[0] { + _param0[u] = param.(*github.PullRequestEvent) + } + } + return +} + func (verifier *VerifierEventParsing) ParseGithubRepo(ghRepo *github.Repository) *EventParsing_ParseGithubRepo_OngoingVerification { params := []pegomock.Param{ghRepo} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ParseGithubRepo", params) diff --git a/server/events/mocks/mock_executor.go b/server/events/mocks/mock_executor.go index f3c9ce749..208f43050 100644 --- a/server/events/mocks/mock_executor.go +++ b/server/events/mocks/mock_executor.go @@ -18,13 +18,13 @@ func NewMockExecutor() *MockExecutor { return &MockExecutor{fail: pegomock.GlobalFailHandler} } -func (mock *MockExecutor) Execute(ctx *events.CommandContext) events.CommandResponse { +func (mock *MockExecutor) Execute(ctx *events.CommandContext) events.CommandResult { params := []pegomock.Param{ctx} - result := pegomock.GetGenericMockFrom(mock).Invoke("Execute", params, []reflect.Type{reflect.TypeOf((*events.CommandResponse)(nil)).Elem()}) - var ret0 events.CommandResponse + result := pegomock.GetGenericMockFrom(mock).Invoke("Execute", params, []reflect.Type{reflect.TypeOf((*events.CommandResult)(nil)).Elem()}) + var ret0 events.CommandResult if len(result) != 0 { if result[0] != nil { - ret0 = result[0].(events.CommandResponse) + ret0 = result[0].(events.CommandResult) } } return ret0 diff --git a/server/events/mocks/mock_project_command_builder.go b/server/events/mocks/mock_project_command_builder.go new file mode 100644 index 000000000..f779acab3 --- /dev/null +++ b/server/events/mocks/mock_project_command_builder.go @@ -0,0 +1,175 @@ +// Automatically generated by pegomock. DO NOT EDIT! +// Source: github.com/runatlantis/atlantis/server/events (interfaces: ProjectCommandBuilder) + +package mocks + +import ( + "reflect" + + pegomock "github.com/petergtz/pegomock" + events "github.com/runatlantis/atlantis/server/events" + models "github.com/runatlantis/atlantis/server/events/models" +) + +type MockProjectCommandBuilder struct { + fail func(message string, callerSkip ...int) +} + +func NewMockProjectCommandBuilder() *MockProjectCommandBuilder { + return &MockProjectCommandBuilder{fail: pegomock.GlobalFailHandler} +} + +func (mock *MockProjectCommandBuilder) BuildAutoplanCommands(ctx *events.CommandContext) ([]models.ProjectCommandContext, error) { + params := []pegomock.Param{ctx} + result := pegomock.GetGenericMockFrom(mock).Invoke("BuildAutoplanCommands", params, []reflect.Type{reflect.TypeOf((*[]models.ProjectCommandContext)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 []models.ProjectCommandContext + var ret1 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].([]models.ProjectCommandContext) + } + if result[1] != nil { + ret1 = result[1].(error) + } + } + return ret0, ret1 +} + +func (mock *MockProjectCommandBuilder) BuildPlanCommand(ctx *events.CommandContext, commentCommand *events.CommentCommand) (models.ProjectCommandContext, error) { + params := []pegomock.Param{ctx, commentCommand} + result := pegomock.GetGenericMockFrom(mock).Invoke("BuildPlanCommand", params, []reflect.Type{reflect.TypeOf((*models.ProjectCommandContext)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 models.ProjectCommandContext + var ret1 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(models.ProjectCommandContext) + } + if result[1] != nil { + ret1 = result[1].(error) + } + } + return ret0, ret1 +} + +func (mock *MockProjectCommandBuilder) BuildApplyCommand(ctx *events.CommandContext, commentCommand *events.CommentCommand) (models.ProjectCommandContext, error) { + params := []pegomock.Param{ctx, commentCommand} + result := pegomock.GetGenericMockFrom(mock).Invoke("BuildApplyCommand", params, []reflect.Type{reflect.TypeOf((*models.ProjectCommandContext)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 models.ProjectCommandContext + var ret1 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(models.ProjectCommandContext) + } + if result[1] != nil { + ret1 = result[1].(error) + } + } + return ret0, ret1 +} + +func (mock *MockProjectCommandBuilder) VerifyWasCalledOnce() *VerifierProjectCommandBuilder { + return &VerifierProjectCommandBuilder{mock, pegomock.Times(1), nil} +} + +func (mock *MockProjectCommandBuilder) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierProjectCommandBuilder { + return &VerifierProjectCommandBuilder{mock, invocationCountMatcher, nil} +} + +func (mock *MockProjectCommandBuilder) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierProjectCommandBuilder { + return &VerifierProjectCommandBuilder{mock, invocationCountMatcher, inOrderContext} +} + +type VerifierProjectCommandBuilder struct { + mock *MockProjectCommandBuilder + invocationCountMatcher pegomock.Matcher + inOrderContext *pegomock.InOrderContext +} + +func (verifier *VerifierProjectCommandBuilder) BuildAutoplanCommands(ctx *events.CommandContext) *ProjectCommandBuilder_BuildAutoplanCommands_OngoingVerification { + params := []pegomock.Param{ctx} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "BuildAutoplanCommands", params) + return &ProjectCommandBuilder_BuildAutoplanCommands_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type ProjectCommandBuilder_BuildAutoplanCommands_OngoingVerification struct { + mock *MockProjectCommandBuilder + methodInvocations []pegomock.MethodInvocation +} + +func (c *ProjectCommandBuilder_BuildAutoplanCommands_OngoingVerification) GetCapturedArguments() *events.CommandContext { + ctx := c.GetAllCapturedArguments() + return ctx[len(ctx)-1] +} + +func (c *ProjectCommandBuilder_BuildAutoplanCommands_OngoingVerification) GetAllCapturedArguments() (_param0 []*events.CommandContext) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]*events.CommandContext, len(params[0])) + for u, param := range params[0] { + _param0[u] = param.(*events.CommandContext) + } + } + return +} + +func (verifier *VerifierProjectCommandBuilder) BuildPlanCommand(ctx *events.CommandContext, commentCommand *events.CommentCommand) *ProjectCommandBuilder_BuildPlanCommand_OngoingVerification { + params := []pegomock.Param{ctx, commentCommand} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "BuildPlanCommand", params) + return &ProjectCommandBuilder_BuildPlanCommand_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type ProjectCommandBuilder_BuildPlanCommand_OngoingVerification struct { + mock *MockProjectCommandBuilder + methodInvocations []pegomock.MethodInvocation +} + +func (c *ProjectCommandBuilder_BuildPlanCommand_OngoingVerification) GetCapturedArguments() (*events.CommandContext, *events.CommentCommand) { + ctx, commentCommand := c.GetAllCapturedArguments() + return ctx[len(ctx)-1], commentCommand[len(commentCommand)-1] +} + +func (c *ProjectCommandBuilder_BuildPlanCommand_OngoingVerification) GetAllCapturedArguments() (_param0 []*events.CommandContext, _param1 []*events.CommentCommand) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]*events.CommandContext, len(params[0])) + for u, param := range params[0] { + _param0[u] = param.(*events.CommandContext) + } + _param1 = make([]*events.CommentCommand, len(params[1])) + for u, param := range params[1] { + _param1[u] = param.(*events.CommentCommand) + } + } + return +} + +func (verifier *VerifierProjectCommandBuilder) BuildApplyCommand(ctx *events.CommandContext, commentCommand *events.CommentCommand) *ProjectCommandBuilder_BuildApplyCommand_OngoingVerification { + params := []pegomock.Param{ctx, commentCommand} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "BuildApplyCommand", params) + return &ProjectCommandBuilder_BuildApplyCommand_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type ProjectCommandBuilder_BuildApplyCommand_OngoingVerification struct { + mock *MockProjectCommandBuilder + methodInvocations []pegomock.MethodInvocation +} + +func (c *ProjectCommandBuilder_BuildApplyCommand_OngoingVerification) GetCapturedArguments() (*events.CommandContext, *events.CommentCommand) { + ctx, commentCommand := c.GetAllCapturedArguments() + return ctx[len(ctx)-1], commentCommand[len(commentCommand)-1] +} + +func (c *ProjectCommandBuilder_BuildApplyCommand_OngoingVerification) GetAllCapturedArguments() (_param0 []*events.CommandContext, _param1 []*events.CommentCommand) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]*events.CommandContext, len(params[0])) + for u, param := range params[0] { + _param0[u] = param.(*events.CommandContext) + } + _param1 = make([]*events.CommentCommand, len(params[1])) + for u, param := range params[1] { + _param1[u] = param.(*events.CommentCommand) + } + } + return +} diff --git a/server/events/mocks/mock_pull_request_operator.go b/server/events/mocks/mock_pull_request_operator.go deleted file mode 100644 index 443636863..000000000 --- a/server/events/mocks/mock_pull_request_operator.go +++ /dev/null @@ -1,154 +0,0 @@ -// Automatically generated by pegomock. DO NOT EDIT! -// Source: github.com/runatlantis/atlantis/server/events (interfaces: PullRequestOperator) - -package mocks - -import ( - "reflect" - - pegomock "github.com/petergtz/pegomock" - events "github.com/runatlantis/atlantis/server/events" -) - -type MockPullRequestOperator struct { - fail func(message string, callerSkip ...int) -} - -func NewMockPullRequestOperator() *MockPullRequestOperator { - return &MockPullRequestOperator{fail: pegomock.GlobalFailHandler} -} - -func (mock *MockPullRequestOperator) Autoplan(ctx *events.CommandContext) events.CommandResponse { - params := []pegomock.Param{ctx} - result := pegomock.GetGenericMockFrom(mock).Invoke("Autoplan", params, []reflect.Type{reflect.TypeOf((*events.CommandResponse)(nil)).Elem()}) - var ret0 events.CommandResponse - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(events.CommandResponse) - } - } - return ret0 -} - -func (mock *MockPullRequestOperator) PlanViaComment(ctx *events.CommandContext) events.CommandResponse { - params := []pegomock.Param{ctx} - result := pegomock.GetGenericMockFrom(mock).Invoke("PlanViaComment", params, []reflect.Type{reflect.TypeOf((*events.CommandResponse)(nil)).Elem()}) - var ret0 events.CommandResponse - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(events.CommandResponse) - } - } - return ret0 -} - -func (mock *MockPullRequestOperator) ApplyViaComment(ctx *events.CommandContext) events.CommandResponse { - params := []pegomock.Param{ctx} - result := pegomock.GetGenericMockFrom(mock).Invoke("ApplyViaComment", params, []reflect.Type{reflect.TypeOf((*events.CommandResponse)(nil)).Elem()}) - var ret0 events.CommandResponse - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(events.CommandResponse) - } - } - return ret0 -} - -func (mock *MockPullRequestOperator) VerifyWasCalledOnce() *VerifierPullRequestOperator { - return &VerifierPullRequestOperator{mock, pegomock.Times(1), nil} -} - -func (mock *MockPullRequestOperator) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierPullRequestOperator { - return &VerifierPullRequestOperator{mock, invocationCountMatcher, nil} -} - -func (mock *MockPullRequestOperator) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierPullRequestOperator { - return &VerifierPullRequestOperator{mock, invocationCountMatcher, inOrderContext} -} - -type VerifierPullRequestOperator struct { - mock *MockPullRequestOperator - invocationCountMatcher pegomock.Matcher - inOrderContext *pegomock.InOrderContext -} - -func (verifier *VerifierPullRequestOperator) Autoplan(ctx *events.CommandContext) *PullRequestOperator_Autoplan_OngoingVerification { - params := []pegomock.Param{ctx} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Autoplan", params) - return &PullRequestOperator_Autoplan_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type PullRequestOperator_Autoplan_OngoingVerification struct { - mock *MockPullRequestOperator - methodInvocations []pegomock.MethodInvocation -} - -func (c *PullRequestOperator_Autoplan_OngoingVerification) GetCapturedArguments() *events.CommandContext { - ctx := c.GetAllCapturedArguments() - return ctx[len(ctx)-1] -} - -func (c *PullRequestOperator_Autoplan_OngoingVerification) GetAllCapturedArguments() (_param0 []*events.CommandContext) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]*events.CommandContext, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(*events.CommandContext) - } - } - return -} - -func (verifier *VerifierPullRequestOperator) PlanViaComment(ctx *events.CommandContext) *PullRequestOperator_PlanViaComment_OngoingVerification { - params := []pegomock.Param{ctx} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "PlanViaComment", params) - return &PullRequestOperator_PlanViaComment_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type PullRequestOperator_PlanViaComment_OngoingVerification struct { - mock *MockPullRequestOperator - methodInvocations []pegomock.MethodInvocation -} - -func (c *PullRequestOperator_PlanViaComment_OngoingVerification) GetCapturedArguments() *events.CommandContext { - ctx := c.GetAllCapturedArguments() - return ctx[len(ctx)-1] -} - -func (c *PullRequestOperator_PlanViaComment_OngoingVerification) GetAllCapturedArguments() (_param0 []*events.CommandContext) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]*events.CommandContext, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(*events.CommandContext) - } - } - return -} - -func (verifier *VerifierPullRequestOperator) ApplyViaComment(ctx *events.CommandContext) *PullRequestOperator_ApplyViaComment_OngoingVerification { - params := []pegomock.Param{ctx} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ApplyViaComment", params) - return &PullRequestOperator_ApplyViaComment_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type PullRequestOperator_ApplyViaComment_OngoingVerification struct { - mock *MockPullRequestOperator - methodInvocations []pegomock.MethodInvocation -} - -func (c *PullRequestOperator_ApplyViaComment_OngoingVerification) GetCapturedArguments() *events.CommandContext { - ctx := c.GetAllCapturedArguments() - return ctx[len(ctx)-1] -} - -func (c *PullRequestOperator_ApplyViaComment_OngoingVerification) GetAllCapturedArguments() (_param0 []*events.CommandContext) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]*events.CommandContext, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(*events.CommandContext) - } - } - return -} diff --git a/server/events/models/models_test.go b/server/events/models/models_test.go index 0dbb4f49f..5883f2193 100644 --- a/server/events/models/models_test.go +++ b/server/events/models/models_test.go @@ -92,3 +92,10 @@ func TestNewRepo_HTTPSAuth(t *testing.T) { Name: "repo", }, repo) } + +func TestProject_String(t *testing.T) { + Equals(t, "repofullname=owner/repo path=my/path", (models.Project{ + RepoFullName: "owner/repo", + Path: "my/path", + }).String()) +} diff --git a/server/events/pull_request_operator.go b/server/events/project_command_builder.go similarity index 62% rename from server/events/pull_request_operator.go rename to server/events/project_command_builder.go index fabad33d0..90f2324dd 100644 --- a/server/events/pull_request_operator.go +++ b/server/events/project_command_builder.go @@ -15,41 +15,45 @@ import ( "github.com/runatlantis/atlantis/server/logging" ) -//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_pull_request_operator.go PullRequestOperator +//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_project_command_builder.go ProjectCommandBuilder -type PullRequestOperator interface { - Autoplan(ctx *CommandContext) CommandResponse - PlanViaComment(ctx *CommandContext) CommandResponse - ApplyViaComment(ctx *CommandContext) CommandResponse +type ProjectCommandBuilder interface { + BuildAutoplanCommands(ctx *CommandContext) ([]models.ProjectCommandContext, error) + BuildPlanCommand(ctx *CommandContext, commentCommand *CommentCommand) (models.ProjectCommandContext, error) + BuildApplyCommand(ctx *CommandContext, commentCommand *CommentCommand) (models.ProjectCommandContext, error) } -type DefaultPullRequestOperator struct { - TerraformExecutor TerraformExec - DefaultTFVersion *version.Version - ParserValidator *yaml.ParserValidator - ProjectFinder ProjectFinder - VCSClient vcs.ClientProxy - Workspace AtlantisWorkspace - ProjectOperator ProjectOperator +type DefaultProjectCommandBuilder struct { + ParserValidator *yaml.ParserValidator + ProjectFinder ProjectFinder + VCSClient vcs.ClientProxy + Workspace AtlantisWorkspace + AtlantisWorkspaceLocker AtlantisWorkspaceLocker } type TerraformExec interface { RunCommandWithVersion(log *logging.SimpleLogger, path string, args []string, v *version.Version, workspace string) (string, error) } -func (p *DefaultPullRequestOperator) Autoplan(ctx *CommandContext) CommandResponse { - // check out repo to parse atlantis.yaml - // this will check out the repo to a * dir - repoDir, err := p.Workspace.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, ctx.Command.Workspace) +func (p *DefaultProjectCommandBuilder) BuildAutoplanCommands(ctx *CommandContext) ([]models.ProjectCommandContext, error) { + // Need to lock the workspace we're about to clone to. + workspace := DefaultWorkspace + unlockFn, err := p.AtlantisWorkspaceLocker.TryLock2(ctx.BaseRepo.FullName, workspace, ctx.Pull.Num) if err != nil { - return CommandResponse{Error: err} + return nil, err + } + defer unlockFn() + + repoDir, err := p.Workspace.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, workspace) + if err != nil { + return nil, err } // Parse config file if it exists. ctx.Log.Debug("parsing config file") config, err := p.ParserValidator.ReadConfig(repoDir) if err != nil && !os.IsNotExist(err) { - return CommandResponse{Error: err} + return nil, err } noAtlantisYAML := os.IsNotExist(err) if noAtlantisYAML { @@ -61,11 +65,11 @@ func (p *DefaultPullRequestOperator) Autoplan(ctx *CommandContext) CommandRespon // We'll need the list of modified files. modifiedFiles, err := p.VCSClient.GetModifiedFiles(ctx.BaseRepo, ctx.Pull) if err != nil { - return CommandResponse{Error: err} + return nil, err } ctx.Log.Debug("%d files were modified in this pull request", len(modifiedFiles)) - // Prepare the project contexts so the ProjectOperator can execute. + // Prepare the project contexts so the ProjectCommandRunner can execute. var projCtxs []models.ProjectCommandContext // If there is no config file, then we try to plan for each project that @@ -93,7 +97,7 @@ func (p *DefaultPullRequestOperator) Autoplan(ctx *CommandContext) CommandRespon // in the config file. matchingProjects, err := p.matchingProjects(ctx.Log, modifiedFiles, config) if err != nil { - return CommandResponse{Error: err} + return nil, err } ctx.Log.Info("%d projects are to be autoplanned based on their when_modified config", len(matchingProjects)) @@ -120,22 +124,21 @@ func (p *DefaultPullRequestOperator) Autoplan(ctx *CommandContext) CommandRespon }) } } - - // Execute the operations. - var results []ProjectResult - for _, pCtx := range projCtxs { - res := p.ProjectOperator.Plan(pCtx, nil) - res.Path = pCtx.RepoRelPath - res.Workspace = pCtx.Workspace - results = append(results, res) - } - return CommandResponse{ProjectResults: results} + return projCtxs, nil } -func (p *DefaultPullRequestOperator) PlanViaComment(ctx *CommandContext) CommandResponse { - repoDir, err := p.Workspace.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, ctx.Command.Workspace) +func (p *DefaultProjectCommandBuilder) BuildPlanCommand(ctx *CommandContext, cmd *CommentCommand) (models.ProjectCommandContext, error) { + var projCtx models.ProjectCommandContext + + unlockFn, err := p.AtlantisWorkspaceLocker.TryLock2(ctx.BaseRepo.FullName, cmd.Workspace, ctx.Pull.Num) if err != nil { - return CommandResponse{Error: err} + return projCtx, err + } + defer unlockFn() + + repoDir, err := p.Workspace.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, cmd.Workspace) + if err != nil { + return projCtx, err } var projCfg *valid.Project @@ -144,55 +147,49 @@ func (p *DefaultPullRequestOperator) PlanViaComment(ctx *CommandContext) Command // Parse config file if it exists. config, err := p.ParserValidator.ReadConfig(repoDir) if err != nil && !os.IsNotExist(err) { - return CommandResponse{Error: err} + return projCtx, err } hasAtlantisYAML := !os.IsNotExist(err) if hasAtlantisYAML { // If they've specified a project by name we look it up. Otherwise we // use the dir and workspace. - if ctx.Command.ProjectName != "" { - projCfg = config.FindProjectByName(ctx.Command.ProjectName) + if cmd.ProjectName != "" { + projCfg = config.FindProjectByName(cmd.ProjectName) if projCfg == nil { - return CommandResponse{Error: fmt.Errorf("no project with name %q configured", ctx.Command.ProjectName)} + return projCtx, fmt.Errorf("no project with name %q configured", cmd.ProjectName) } } else { - projCfg = config.FindProject(ctx.Command.Dir, ctx.Command.Workspace) + projCfg = config.FindProject(cmd.Dir, cmd.Workspace) } globalCfg = &config } - if ctx.Command.ProjectName != "" && !hasAtlantisYAML { - return CommandResponse{Error: fmt.Errorf("cannot specify a project name unless an %s file exists to configure projects", yaml.AtlantisYAMLFilename)} + if cmd.ProjectName != "" && !hasAtlantisYAML { + return projCtx, fmt.Errorf("cannot specify a project name unless an %s file exists to configure projects", yaml.AtlantisYAMLFilename) } - projCtx := models.ProjectCommandContext{ + projCtx = models.ProjectCommandContext{ BaseRepo: ctx.BaseRepo, HeadRepo: ctx.HeadRepo, Pull: ctx.Pull, User: ctx.User, Log: ctx.Log, - CommentArgs: ctx.Command.Flags, - Workspace: ctx.Command.Workspace, - RepoRelPath: ctx.Command.Dir, - ProjectName: ctx.Command.ProjectName, + CommentArgs: cmd.Flags, + Workspace: cmd.Workspace, + RepoRelPath: cmd.Dir, + ProjectName: cmd.ProjectName, ProjectConfig: projCfg, GlobalConfig: globalCfg, } - projAbsPath := filepath.Join(repoDir, ctx.Command.Dir) - res := p.ProjectOperator.Plan(projCtx, &projAbsPath) - res.Workspace = projCtx.Workspace - res.Path = projCtx.RepoRelPath - return CommandResponse{ - ProjectResults: []ProjectResult{ - res, - }, - } + return projCtx, nil } -func (p *DefaultPullRequestOperator) ApplyViaComment(ctx *CommandContext) CommandResponse { - repoDir, err := p.Workspace.GetWorkspace(ctx.BaseRepo, ctx.Pull, ctx.Command.Workspace) +func (p *DefaultProjectCommandBuilder) BuildApplyCommand(ctx *CommandContext, cmd *CommentCommand) (models.ProjectCommandContext, error) { + var projCtx models.ProjectCommandContext + + repoDir, err := p.Workspace.GetWorkspace(ctx.BaseRepo, ctx.Pull, cmd.Workspace) if err != nil { - return CommandResponse{Failure: "No workspace found. Did you run plan?"} + return projCtx, err } // todo: can deduplicate this between PlanViaComment @@ -202,53 +199,46 @@ func (p *DefaultPullRequestOperator) ApplyViaComment(ctx *CommandContext) Comman // Parse config file if it exists. config, err := p.ParserValidator.ReadConfig(repoDir) if err != nil && !os.IsNotExist(err) { - return CommandResponse{Error: err} + return projCtx, err } hasAtlantisYAML := !os.IsNotExist(err) if hasAtlantisYAML { // If they've specified a project by name we look it up. Otherwise we // use the dir and workspace. - if ctx.Command.ProjectName != "" { - projCfg = config.FindProjectByName(ctx.Command.ProjectName) + if cmd.ProjectName != "" { + projCfg = config.FindProjectByName(cmd.ProjectName) if projCfg == nil { - return CommandResponse{Error: fmt.Errorf("no project with name %q configured", ctx.Command.ProjectName)} + return projCtx, fmt.Errorf("no project with name %q configured", cmd.ProjectName) } } else { - projCfg = config.FindProject(ctx.Command.Dir, ctx.Command.Workspace) + projCfg = config.FindProject(cmd.Dir, cmd.Workspace) } globalCfg = &config } - if ctx.Command.ProjectName != "" && !hasAtlantisYAML { - return CommandResponse{Error: fmt.Errorf("cannot specify a project name unless an %s file exists to configure projects", yaml.AtlantisYAMLFilename)} + if cmd.ProjectName != "" && !hasAtlantisYAML { + return projCtx, fmt.Errorf("cannot specify a project name unless an %s file exists to configure projects", yaml.AtlantisYAMLFilename) } - projCtx := models.ProjectCommandContext{ + projCtx = models.ProjectCommandContext{ BaseRepo: ctx.BaseRepo, HeadRepo: ctx.HeadRepo, Pull: ctx.Pull, User: ctx.User, Log: ctx.Log, - CommentArgs: ctx.Command.Flags, - Workspace: ctx.Command.Workspace, - RepoRelPath: ctx.Command.Dir, - ProjectName: ctx.Command.ProjectName, + CommentArgs: cmd.Flags, + Workspace: cmd.Workspace, + RepoRelPath: cmd.Dir, + ProjectName: cmd.ProjectName, ProjectConfig: projCfg, GlobalConfig: globalCfg, } - res := p.ProjectOperator.Apply(projCtx, filepath.Join(repoDir, ctx.Command.Dir)) - res.Workspace = projCtx.Workspace - res.Path = projCtx.RepoRelPath - return CommandResponse{ - ProjectResults: []ProjectResult{ - res, - }, - } + return projCtx, nil } // matchingProjects returns the list of projects whose WhenModified fields match // any of the modifiedFiles. -func (p *DefaultPullRequestOperator) matchingProjects(log *logging.SimpleLogger, modifiedFiles []string, config valid.Spec) ([]valid.Project, error) { +func (p *DefaultProjectCommandBuilder) matchingProjects(log *logging.SimpleLogger, modifiedFiles []string, config valid.Spec) ([]valid.Project, error) { var projects []valid.Project for _, project := range config.Projects { log.Debug("checking if project at dir %q workspace %q was modified", project.Dir, project.Workspace) diff --git a/server/events/pull_request_operator_test.go b/server/events/project_command_builder_test.go similarity index 100% rename from server/events/pull_request_operator_test.go rename to server/events/project_command_builder_test.go diff --git a/server/events/project_operator.go b/server/events/project_command_runner.go similarity index 57% rename from server/events/project_operator.go rename to server/events/project_command_runner.go index b5e8184ec..0db221def 100644 --- a/server/events/project_operator.go +++ b/server/events/project_command_runner.go @@ -14,6 +14,7 @@ package events import ( + "os" "path/filepath" "strings" @@ -41,46 +42,48 @@ type PlanSuccess struct { LockURL string } -type ProjectOperator struct { - Locker ProjectLocker - LockURLGenerator LockURLGenerator - InitStepOperator runtime.InitStepOperator - PlanStepOperator runtime.PlanStepOperator - ApplyStepOperator runtime.ApplyStepOperator - RunStepOperator runtime.RunStepOperator - ApprovalOperator runtime.ApprovalOperator - Workspace AtlantisWorkspace - Webhooks WebhooksSender +type ProjectCommandRunner struct { + Locker ProjectLocker + LockURLGenerator LockURLGenerator + InitStepRunner runtime.InitStepRunner + PlanStepRunner runtime.PlanStepRunner + ApplyStepRunner runtime.ApplyStepRunner + RunStepRunner runtime.RunStepRunner + PullApprovedChecker runtime.PullApprovedChecker + Workspace AtlantisWorkspace + Webhooks WebhooksSender + AtlantisWorkspaceLocker AtlantisWorkspaceLocker } -func (p *ProjectOperator) Plan(ctx models.ProjectCommandContext, projAbsPathPtr *string) ProjectResult { +func (p *ProjectCommandRunner) Plan(ctx models.ProjectCommandContext) ProjectCommandResult { // Acquire Atlantis lock for this repo/dir/workspace. lockAttempt, err := p.Locker.TryLock(ctx.Log, ctx.Pull, ctx.User, ctx.Workspace, models.NewProject(ctx.BaseRepo.FullName, ctx.RepoRelPath)) if err != nil { - return ProjectResult{Error: errors.Wrap(err, "acquiring lock")} + return ProjectCommandResult{ + Error: errors.Wrap(err, "acquiring lock"), + } } if !lockAttempt.LockAcquired { - return ProjectResult{Failure: lockAttempt.LockFailureReason} + return ProjectCommandResult{Failure: lockAttempt.LockFailureReason} } ctx.Log.Debug("acquired lock for project") - // Ensure project has been cloned. - var projAbsPath string - if projAbsPathPtr == nil { - ctx.Log.Debug("project has not yet been cloned") - repoDir, cloneErr := p.Workspace.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, ctx.Workspace) - if cloneErr != nil { - if unlockErr := lockAttempt.UnlockFn(); unlockErr != nil { - ctx.Log.Err("error unlocking state after plan error: %v", unlockErr) - } - return ProjectResult{Error: cloneErr} - } - projAbsPath = filepath.Join(repoDir, ctx.RepoRelPath) - ctx.Log.Debug("project successfully cloned to %q", projAbsPath) - } else { - projAbsPath = *projAbsPathPtr - ctx.Log.Debug("project was already cloned to %q", projAbsPath) + // Acquire internal lock for the directory we're going to operate in. + unlockFn, err := p.AtlantisWorkspaceLocker.TryLock2(ctx.BaseRepo.FullName, ctx.Workspace, ctx.Pull.Num) + if err != nil { + return ProjectCommandResult{Error: err} } + defer unlockFn() + + // Clone is idempotent so okay to run even if the repo was already cloned. + repoDir, cloneErr := p.Workspace.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, ctx.Workspace) + if cloneErr != nil { + if unlockErr := lockAttempt.UnlockFn(); unlockErr != nil { + ctx.Log.Err("error unlocking state after plan error: %v", unlockErr) + } + return ProjectCommandResult{Error: cloneErr} + } + projAbsPath := filepath.Join(repoDir, ctx.RepoRelPath) // Use default stage unless another workflow is defined in config stage := p.defaultPlanStage() @@ -98,10 +101,10 @@ func (p *ProjectOperator) Plan(ctx models.ProjectCommandContext, projAbsPathPtr ctx.Log.Err("error unlocking state after plan error: %v", unlockErr) } // todo: include output from other steps. - return ProjectResult{Error: err} + return ProjectCommandResult{Error: err} } - return ProjectResult{ + return ProjectCommandResult{ PlanSuccess: &PlanSuccess{ LockURL: p.LockURLGenerator.GenerateLockURL(lockAttempt.LockKey), TerraformOutput: strings.Join(outputs, "\n"), @@ -109,20 +112,20 @@ func (p *ProjectOperator) Plan(ctx models.ProjectCommandContext, projAbsPathPtr } } -func (p *ProjectOperator) runSteps(steps []valid.Step, ctx models.ProjectCommandContext, absPath string) ([]string, error) { +func (p *ProjectCommandRunner) runSteps(steps []valid.Step, ctx models.ProjectCommandContext, absPath string) ([]string, error) { var outputs []string for _, step := range steps { var out string var err error switch step.StepName { case "init": - out, err = p.InitStepOperator.Run(ctx, step.ExtraArgs, absPath) + out, err = p.InitStepRunner.Run(ctx, step.ExtraArgs, absPath) case "plan": - out, err = p.PlanStepOperator.Run(ctx, step.ExtraArgs, absPath) + out, err = p.PlanStepRunner.Run(ctx, step.ExtraArgs, absPath) case "apply": - out, err = p.ApplyStepOperator.Run(ctx, step.ExtraArgs, absPath) + out, err = p.ApplyStepRunner.Run(ctx, step.ExtraArgs, absPath) case "run": - out, err = p.RunStepOperator.Run(ctx, step.RunCommand, absPath) + out, err = p.RunStepRunner.Run(ctx, step.RunCommand, absPath) } if err != nil { @@ -136,21 +139,36 @@ func (p *ProjectOperator) runSteps(steps []valid.Step, ctx models.ProjectCommand return outputs, nil } -func (p *ProjectOperator) Apply(ctx models.ProjectCommandContext, absPath string) ProjectResult { +func (p *ProjectCommandRunner) Apply(ctx models.ProjectCommandContext) ProjectCommandResult { + repoDir, err := p.Workspace.GetWorkspace(ctx.BaseRepo, ctx.Pull, ctx.Workspace) + if err != nil { + if os.IsNotExist(err) { + return ProjectCommandResult{Error: errors.New("project has not been cloned–did you run plan?")} + } + return ProjectCommandResult{Error: err} + } + absPath := filepath.Join(repoDir, ctx.RepoRelPath) + if ctx.ProjectConfig != nil { for _, req := range ctx.ProjectConfig.ApplyRequirements { switch req { case "approved": - approved, err := p.ApprovalOperator.IsApproved(ctx.BaseRepo, ctx.Pull) + approved, err := p.PullApprovedChecker.IsApproved(ctx.BaseRepo, ctx.Pull) if err != nil { - return ProjectResult{Error: errors.Wrap(err, "checking if pull request was approved")} + return ProjectCommandResult{Error: errors.Wrap(err, "checking if pull request was approved")} } if !approved { - return ProjectResult{Failure: "Pull request must be approved before running apply."} + return ProjectCommandResult{Failure: "Pull request must be approved before running apply."} } } } } + // Acquire internal lock for the directory we're going to operate in. + unlockFn, err := p.AtlantisWorkspaceLocker.TryLock2(ctx.BaseRepo.FullName, ctx.Workspace, ctx.Pull.Num) + if err != nil { + return ProjectCommandResult{Error: err} + } + defer unlockFn() // Use default stage unless another workflow is defined in config stage := p.defaultApplyStage() @@ -170,14 +188,14 @@ func (p *ProjectOperator) Apply(ctx models.ProjectCommandContext, absPath string }) if err != nil { // todo: include output from other steps. - return ProjectResult{Error: err} + return ProjectCommandResult{Error: err} } - return ProjectResult{ + return ProjectCommandResult{ ApplySuccess: strings.Join(outputs, "\n"), } } -func (p ProjectOperator) defaultPlanStage() valid.Stage { +func (p ProjectCommandRunner) defaultPlanStage() valid.Stage { return valid.Stage{ Steps: []valid.Step{ { @@ -190,7 +208,7 @@ func (p ProjectOperator) defaultPlanStage() valid.Stage { } } -func (p ProjectOperator) defaultApplyStage() valid.Stage { +func (p ProjectCommandRunner) defaultApplyStage() valid.Stage { return valid.Stage{ Steps: []valid.Step{ { diff --git a/server/events/project_operator_test.go b/server/events/project_command_runner_test.go similarity index 100% rename from server/events/project_operator_test.go rename to server/events/project_command_runner_test.go diff --git a/server/events/project_result.go b/server/events/project_result.go index ca5a1ecee..f01ccd936 100644 --- a/server/events/project_result.go +++ b/server/events/project_result.go @@ -17,8 +17,12 @@ import "github.com/runatlantis/atlantis/server/events/vcs" // ProjectResult is the result of executing a plan/apply for a project. type ProjectResult struct { - Path string - Workspace string + ProjectCommandResult + Path string + Workspace string +} + +type ProjectCommandResult struct { Error error Failure string PlanSuccess *PlanSuccess diff --git a/server/events/runtime/apply_step_operator.go b/server/events/runtime/apply_step_runner.go similarity index 83% rename from server/events/runtime/apply_step_operator.go rename to server/events/runtime/apply_step_runner.go index 7b4a58c80..2cb3a50c1 100644 --- a/server/events/runtime/apply_step_operator.go +++ b/server/events/runtime/apply_step_runner.go @@ -9,12 +9,12 @@ import ( "github.com/runatlantis/atlantis/server/events/models" ) -// ApplyStepOperator runs `terraform apply`. -type ApplyStepOperator struct { +// ApplyStepRunner runs `terraform apply`. +type ApplyStepRunner struct { TerraformExecutor TerraformExec } -func (a *ApplyStepOperator) Run(ctx models.ProjectCommandContext, extraArgs []string, path string) (string, error) { +func (a *ApplyStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []string, path string) (string, error) { // todo: move this to a common library planFileName := fmt.Sprintf("%s.tfplan", ctx.Workspace) if ctx.ProjectName != "" { diff --git a/server/events/runtime/apply_step_operator_test.go b/server/events/runtime/apply_step_runner_test.go similarity index 95% rename from server/events/runtime/apply_step_operator_test.go rename to server/events/runtime/apply_step_runner_test.go index 1aad3c9e2..afc5c99e3 100644 --- a/server/events/runtime/apply_step_operator_test.go +++ b/server/events/runtime/apply_step_runner_test.go @@ -17,7 +17,7 @@ import ( ) func TestRun_NoDir(t *testing.T) { - o := runtime.ApplyStepOperator{ + o := runtime.ApplyStepRunner{ TerraformExecutor: nil, } _, err := o.Run(models.ProjectCommandContext{ @@ -30,7 +30,7 @@ func TestRun_NoDir(t *testing.T) { func TestRun_NoPlanFile(t *testing.T) { tmpDir, cleanup := TempDir(t) defer cleanup() - o := runtime.ApplyStepOperator{ + o := runtime.ApplyStepRunner{ TerraformExecutor: nil, } _, err := o.Run(models.ProjectCommandContext{ @@ -49,7 +49,7 @@ func TestRun_Success(t *testing.T) { RegisterMockTestingT(t) terraform := mocks.NewMockClient() - o := runtime.ApplyStepOperator{ + o := runtime.ApplyStepRunner{ TerraformExecutor: terraform, } @@ -74,7 +74,7 @@ func TestRun_UsesConfiguredTFVersion(t *testing.T) { RegisterMockTestingT(t) terraform := mocks.NewMockClient() - o := runtime.ApplyStepOperator{ + o := runtime.ApplyStepRunner{ TerraformExecutor: terraform, } tfVersion, _ := version.NewVersion("0.11.0") diff --git a/server/events/runtime/init_step_operator.go b/server/events/runtime/init_step_runner.go similarity index 87% rename from server/events/runtime/init_step_operator.go rename to server/events/runtime/init_step_runner.go index f483ecbbd..da8ae585f 100644 --- a/server/events/runtime/init_step_operator.go +++ b/server/events/runtime/init_step_runner.go @@ -6,13 +6,13 @@ import ( ) // InitStep runs `terraform init`. -type InitStepOperator struct { +type InitStepRunner struct { TerraformExecutor TerraformExec DefaultTFVersion *version.Version } // nolint: unparam -func (i *InitStepOperator) Run(ctx models.ProjectCommandContext, extraArgs []string, path string) (string, error) { +func (i *InitStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []string, path string) (string, error) { tfVersion := i.DefaultTFVersion if ctx.ProjectConfig != nil && ctx.ProjectConfig.TerraformVersion != nil { tfVersion = ctx.ProjectConfig.TerraformVersion diff --git a/server/events/runtime/init_step_operator_test.go b/server/events/runtime/init_step_runner_test.go similarity index 97% rename from server/events/runtime/init_step_operator_test.go rename to server/events/runtime/init_step_runner_test.go index efbf4ca28..4a1416fba 100644 --- a/server/events/runtime/init_step_operator_test.go +++ b/server/events/runtime/init_step_runner_test.go @@ -44,7 +44,7 @@ func TestRun_UsesGetOrInitForRightVersion(t *testing.T) { tfVersion, _ := version.NewVersion(c.version) logger := logging.NewNoopLogger() - iso := runtime.InitStepOperator{ + iso := runtime.InitStepRunner{ TerraformExecutor: terraform, DefaultTFVersion: tfVersion, } diff --git a/server/events/runtime/plan_step_operater.go b/server/events/runtime/plan_step_runner.go similarity index 93% rename from server/events/runtime/plan_step_operater.go rename to server/events/runtime/plan_step_runner.go index 30028dd48..f0e49b6ed 100644 --- a/server/events/runtime/plan_step_operater.go +++ b/server/events/runtime/plan_step_runner.go @@ -15,12 +15,12 @@ import ( const atlantisUserTFVar = "atlantis_user" const defaultWorkspace = "default" -type PlanStepOperator struct { +type PlanStepRunner struct { TerraformExecutor TerraformExec DefaultTFVersion *version.Version } -func (p *PlanStepOperator) Run(ctx models.ProjectCommandContext, extraArgs []string, path string) (string, error) { +func (p *PlanStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []string, path string) (string, error) { tfVersion := p.DefaultTFVersion if ctx.ProjectConfig != nil && ctx.ProjectConfig.TerraformVersion != nil { tfVersion = ctx.ProjectConfig.TerraformVersion @@ -55,7 +55,7 @@ func (p *PlanStepOperator) Run(ctx models.ProjectCommandContext, extraArgs []str // switchWorkspace changes the terraform workspace if necessary and will create // it if it doesn't exist. It handles differences between versions. -func (p *PlanStepOperator) switchWorkspace(ctx models.ProjectCommandContext, path string, tfVersion *version.Version) error { +func (p *PlanStepRunner) switchWorkspace(ctx models.ProjectCommandContext, path string, tfVersion *version.Version) error { // In versions less than 0.9 there is no support for workspaces. noWorkspaceSupport := MustConstraint("<0.9").Check(tfVersion) // If the user tried to set a specific workspace in the comment but their diff --git a/server/events/runtime/plan_step_operater_test.go b/server/events/runtime/plan_step_runner_test.go similarity index 87% rename from server/events/runtime/plan_step_operater_test.go rename to server/events/runtime/plan_step_runner_test.go index bf80927b6..01dce930b 100644 --- a/server/events/runtime/plan_step_operater_test.go +++ b/server/events/runtime/plan_step_runner_test.go @@ -26,7 +26,7 @@ func TestRun_NoWorkspaceIn08(t *testing.T) { tfVersion, _ := version.NewVersion("0.8") logger := logging.NewNoopLogger() workspace := "default" - s := runtime.PlanStepOperator{ + s := runtime.PlanStepRunner{ DefaultTFVersion: tfVersion, TerraformExecutor: terraform, } @@ -59,7 +59,7 @@ func TestRun_ErrWorkspaceIn08(t *testing.T) { tfVersion, _ := version.NewVersion("0.8") logger := logging.NewNoopLogger() workspace := "notdefault" - s := runtime.PlanStepOperator{ + s := runtime.PlanStepRunner{ TerraformExecutor: terraform, DefaultTFVersion: tfVersion, } @@ -107,7 +107,7 @@ func TestRun_SwitchesWorkspace(t *testing.T) { tfVersion, _ := version.NewVersion(c.tfVersion) logger := logging.NewNoopLogger() - s := runtime.PlanStepOperator{ + s := runtime.PlanStepRunner{ TerraformExecutor: terraform, DefaultTFVersion: tfVersion, } @@ -162,7 +162,7 @@ func TestRun_CreatesWorkspace(t *testing.T) { terraform := mocks.NewMockClient() tfVersion, _ := version.NewVersion(c.tfVersion) logger := logging.NewNoopLogger() - s := runtime.PlanStepOperator{ + s := runtime.PlanStepRunner{ TerraformExecutor: terraform, DefaultTFVersion: tfVersion, } @@ -201,7 +201,7 @@ func TestRun_NoWorkspaceSwitchIfNotNecessary(t *testing.T) { terraform := mocks.NewMockClient() tfVersion, _ := version.NewVersion("0.10.0") logger := logging.NewNoopLogger() - s := runtime.PlanStepOperator{ + s := runtime.PlanStepRunner{ TerraformExecutor: terraform, DefaultTFVersion: tfVersion, } @@ -243,7 +243,7 @@ func TestRun_AddsEnvVarFile(t *testing.T) { // Using version >= 0.10 here so we don't expect any env commands. tfVersion, _ := version.NewVersion("0.10.0") logger := logging.NewNoopLogger() - s := runtime.PlanStepOperator{ + s := runtime.PlanStepRunner{ TerraformExecutor: terraform, DefaultTFVersion: tfVersion, } @@ -265,3 +265,31 @@ func TestRun_AddsEnvVarFile(t *testing.T) { terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, tmpDir, expPlanArgs, tfVersion, "workspace") Equals(t, "output", output) } + +func TestRun_UsesDiffPathForProject(t *testing.T) { + // Test that if running for a project, uses a different path for the plan + // file. + RegisterMockTestingT(t) + terraform := mocks.NewMockClient() + tfVersion, _ := version.NewVersion("0.10.0") + logger := logging.NewNoopLogger() + s := runtime.PlanStepRunner{ + TerraformExecutor: terraform, + DefaultTFVersion: tfVersion, + } + When(terraform.RunCommandWithVersion(logger, "/path", []string{"workspace", "show"}, tfVersion, "workspace")).ThenReturn("workspace\n", nil) + + expPlanArgs := []string{"plan", "-refresh", "-no-color", "-out", "/path/projectname-default.tfplan", "-var", "atlantis_user=username", "extra", "args", "comment", "args"} + When(terraform.RunCommandWithVersion(logger, "/path", expPlanArgs, tfVersion, "default")).ThenReturn("output", nil) + + output, err := s.Run(models.ProjectCommandContext{ + Log: logger, + Workspace: "default", + RepoRelPath: ".", + ProjectName: "projectname", + User: models.User{Username: "username"}, + CommentArgs: []string{"comment", "args"}, + }, []string{"extra", "args"}, "/path") + Ok(t, err) + Equals(t, "output", output) +} diff --git a/server/events/runtime/approval_operator.go b/server/events/runtime/pull_approved_checker.go similarity index 67% rename from server/events/runtime/approval_operator.go rename to server/events/runtime/pull_approved_checker.go index 63731f342..049f43496 100644 --- a/server/events/runtime/approval_operator.go +++ b/server/events/runtime/pull_approved_checker.go @@ -5,11 +5,11 @@ import ( "github.com/runatlantis/atlantis/server/events/vcs" ) -type ApprovalOperator struct { +type PullApprovedChecker struct { VCSClient vcs.ClientProxy } -func (a *ApprovalOperator) IsApproved(baseRepo models.Repo, pull models.PullRequest) (bool, error) { +func (a *PullApprovedChecker) IsApproved(baseRepo models.Repo, pull models.PullRequest) (bool, error) { approved, err := a.VCSClient.PullIsApproved(baseRepo, pull) if err != nil { return false, err diff --git a/server/events/runtime/run_step_operator_test.go b/server/events/runtime/run_step_operator_test.go deleted file mode 100644 index 899327a69..000000000 --- a/server/events/runtime/run_step_operator_test.go +++ /dev/null @@ -1,3 +0,0 @@ -package runtime_test - -// todo diff --git a/server/events/runtime/run_step_operator.go b/server/events/runtime/run_step_runner.go similarity index 77% rename from server/events/runtime/run_step_operator.go rename to server/events/runtime/run_step_runner.go index 6554527be..1b406737a 100644 --- a/server/events/runtime/run_step_operator.go +++ b/server/events/runtime/run_step_runner.go @@ -9,11 +9,11 @@ import ( "github.com/runatlantis/atlantis/server/events/models" ) -// RunStepOperator runs custom commands. -type RunStepOperator struct { +// RunStepRunner runs custom commands. +type RunStepRunner struct { } -func (r *RunStepOperator) Run(ctx models.ProjectCommandContext, command []string, path string) (string, error) { +func (r *RunStepRunner) Run(ctx models.ProjectCommandContext, command []string, path string) (string, error) { if len(command) < 1 { return "", errors.New("no commands for run step") } diff --git a/server/events/runtime/run_step_runner_test.go b/server/events/runtime/run_step_runner_test.go new file mode 100644 index 000000000..36262b235 --- /dev/null +++ b/server/events/runtime/run_step_runner_test.go @@ -0,0 +1,50 @@ +package runtime_test + +import ( + "strings" + "testing" + + "github.com/runatlantis/atlantis/server/events/models" + "github.com/runatlantis/atlantis/server/events/runtime" + "github.com/runatlantis/atlantis/server/logging" + . "github.com/runatlantis/atlantis/testing" +) + +func TestRunStepRunner_Run(t *testing.T) { + cases := []struct { + Command string + ExpOut string + ExpErr string + }{ + { + Command: "echo hi", + ExpOut: "hi\n", + }, + { + Command: "echo hi >> file && cat file", + ExpOut: "hi\n", + }, + { + Command: "lkjlkj", + ExpErr: "exit status 127: running \"lkjlkj\" in", + }, + } + + r := runtime.RunStepRunner{} + ctx := models.ProjectCommandContext{ + Log: logging.NewNoopLogger(), + } + for _, c := range cases { + t.Run(c.Command, func(t *testing.T) { + tmpDir, cleanup := TempDir(t) + defer cleanup() + out, err := r.Run(ctx, strings.Split(c.Command, " "), tmpDir) + if c.ExpErr != "" { + ErrContains(t, c.ExpErr, err) + return + } + Ok(t, err) + Equals(t, c.ExpOut, out) + }) + } +} diff --git a/server/events/vcs/fixtures/fixtures.go b/server/events/vcs/fixtures/fixtures.go index e3155f199..693916203 100644 --- a/server/events/vcs/fixtures/fixtures.go +++ b/server/events/vcs/fixtures/fixtures.go @@ -15,6 +15,14 @@ package fixtures import "github.com/google/go-github/github" +var PullEvent = github.PullRequestEvent{ + Sender: &github.User{ + Login: github.String("user"), + }, + Repo: &Repo, + PullRequest: &Pull, +} + var Pull = github.PullRequest{ Head: &github.PullRequestBranch{ SHA: github.String("sha256"), diff --git a/server/events/vcs/vcs_test.go b/server/events/vcs/vcs_test.go new file mode 100644 index 000000000..5333ff26f --- /dev/null +++ b/server/events/vcs/vcs_test.go @@ -0,0 +1,19 @@ +package vcs_test + +import ( + "testing" + + "github.com/runatlantis/atlantis/server/events/vcs" + . "github.com/runatlantis/atlantis/testing" +) + +func TestStatus_String(t *testing.T) { + cases := map[vcs.CommitStatus]string{ + vcs.Pending: "pending", + vcs.Success: "success", + vcs.Failed: "failed", + } + for k, v := range cases { + Equals(t, v, k.String()) + } +} diff --git a/server/events_controller.go b/server/events_controller.go index 3db62bd96..f666e1e32 100644 --- a/server/events_controller.go +++ b/server/events_controller.go @@ -29,7 +29,7 @@ const githubHeader = "X-Github-Event" const gitlabHeader = "X-Gitlab-Event" // EventsController handles all webhook requests which signify 'events' in the -// VCS host, ex. GitHub. It's split out from Server to make testing easier. +// VCS host, ex. GitHub. type EventsController struct { CommandRunner events.CommandRunner PullCleaner events.PullCleaner @@ -51,11 +51,7 @@ type EventsController struct { // startup to support. SupportedVCSHosts []models.VCSHostType VCSClient vcs.ClientProxy - // AtlantisGithubUser is the user that atlantis is running as for Github. - AtlantisGithubUser models.User - // AtlantisGitlabUser is the user that atlantis is running as for Gitlab. - AtlantisGitlabUser models.User - TestingMode bool + TestingMode bool } // Post handles POST webhook requests. @@ -117,27 +113,20 @@ func (e *EventsController) HandleGithubCommentEvent(w http.ResponseWriter, event return } - // We pass in an empty models.Repo for headRepo because we need to do additional - // calls to get that information but we need this code path to be generic. - // Later on in CommandHandler we detect that this is a GitHub event and - // make the necessary calls to get the headRepo. - e.handleCommentEvent(w, baseRepo, models.Repo{}, user, pullNum, event.Comment.GetBody(), models.Github) + // We pass in nil for maybeHeadRepo because the head repo data isn't + // available in the GithubIssueComment event. + e.handleCommentEvent(w, baseRepo, nil, user, pullNum, event.Comment.GetBody(), models.Github) } // HandleGithubPullRequestEvent will delete any locks associated with the pull // request if the event is a pull request closed event. It's exported to make // testing easier. func (e *EventsController) HandleGithubPullRequestEvent(w http.ResponseWriter, pullEvent *github.PullRequestEvent, githubReqID string) { - pull, headRepo, err := e.Parser.ParseGithubPull(pullEvent.PullRequest) + pull, baseRepo, headRepo, user, err := e.Parser.ParseGithubPullEvent(pullEvent) if err != nil { e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing pull data: %s %s", err, githubReqID) return } - baseRepo, err := e.Parser.ParseGithubRepo(pullEvent.Repo) - if err != nil { - e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing repo data: %s %s", err, githubReqID) - return - } var eventType string switch pullEvent.GetAction() { case "opened": @@ -150,7 +139,7 @@ func (e *EventsController) HandleGithubPullRequestEvent(w http.ResponseWriter, p eventType = OtherPullEvent } e.Logger.Info("identified event as type %q", eventType) - e.handlePullRequestEvent(w, baseRepo, headRepo, pull, e.AtlantisGithubUser, eventType) + e.handlePullRequestEvent(w, baseRepo, headRepo, pull, user, eventType) } const OpenPullEvent = "opened" @@ -179,16 +168,13 @@ func (e *EventsController) handlePullRequestEvent(w http.ResponseWriter, baseRep // We use a goroutine so that this function returns and the connection is // closed. fmt.Fprintln(w, "Processing...") - // We use a Command to represent autoplanning but we set dir and - // workspace to '*' to indicate that all applicable dirs and workspaces - // should be planned. - autoplanCmd := events.NewCommand("*", nil, events.Plan, false, "*", "", true) - e.Logger.Info("executing command %s", autoplanCmd) + + e.Logger.Info("executing autoplan") if !e.TestingMode { - go e.CommandRunner.ExecuteCommand(baseRepo, headRepo, user, pull.Num, autoplanCmd) + go e.CommandRunner.RunAutoplanCommand(baseRepo, headRepo, pull, user) } else { // When testing we want to wait for everything to complete. - e.CommandRunner.ExecuteCommand(baseRepo, headRepo, user, pull.Num, autoplanCmd) + e.CommandRunner.RunAutoplanCommand(baseRepo, headRepo, pull, user) } return case ClosedPullEvent: @@ -202,7 +188,7 @@ func (e *EventsController) handlePullRequestEvent(w http.ResponseWriter, baseRep return case OtherPullEvent: // Else we ignore the event. - e.respond(w, logging.Debug, http.StatusOK, "Ignoring opened pull request event") + e.respond(w, logging.Debug, http.StatusOK, "Ignoring non-actionable pull request event") return } } @@ -236,10 +222,10 @@ func (e *EventsController) HandleGitlabCommentEvent(w http.ResponseWriter, event e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing webhook: %s", err) return } - e.handleCommentEvent(w, baseRepo, headRepo, user, event.MergeRequest.IID, event.ObjectAttributes.Note, models.Gitlab) + e.handleCommentEvent(w, baseRepo, &headRepo, user, event.MergeRequest.IID, event.ObjectAttributes.Note, models.Gitlab) } -func (e *EventsController) handleCommentEvent(w http.ResponseWriter, baseRepo models.Repo, headRepo models.Repo, user models.User, pullNum int, comment string, vcsHost models.VCSHostType) { +func (e *EventsController) handleCommentEvent(w http.ResponseWriter, baseRepo models.Repo, maybeHeadRepo *models.Repo, user models.User, pullNum int, comment string, vcsHost models.VCSHostType) { parseResult := e.CommentParser.Parse(comment, vcsHost) if parseResult.Ignore { truncated := comment @@ -278,10 +264,10 @@ func (e *EventsController) handleCommentEvent(w http.ResponseWriter, baseRepo mo // Respond with success and then actually execute the command asynchronously. // We use a goroutine so that this function returns and the connection is // closed. - go e.CommandRunner.ExecuteCommand(baseRepo, headRepo, user, pullNum, parseResult.Command) + go e.CommandRunner.RunCommentCommand(baseRepo, maybeHeadRepo, user, pullNum, parseResult.Command) } else { // When testing we want to wait for everything to complete. - e.CommandRunner.ExecuteCommand(baseRepo, headRepo, user, pullNum, parseResult.Command) + e.CommandRunner.RunCommentCommand(baseRepo, maybeHeadRepo, user, pullNum, parseResult.Command) } } @@ -289,7 +275,7 @@ func (e *EventsController) handleCommentEvent(w http.ResponseWriter, baseRepo mo // request if the event is a merge request closed event. It's exported to make // testing easier. func (e *EventsController) HandleGitlabMergeRequestEvent(w http.ResponseWriter, event gitlab.MergeEvent) { - pull, baseRepo, headRepo, err := e.Parser.ParseGitlabMergeEvent(event) + pull, baseRepo, headRepo, user, err := e.Parser.ParseGitlabMergeEvent(event) if err != nil { e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing webhook: %s", err) return @@ -306,7 +292,7 @@ func (e *EventsController) HandleGitlabMergeRequestEvent(w http.ResponseWriter, eventType = OtherPullEvent } e.Logger.Info("identified event as type %q", eventType) - e.handlePullRequestEvent(w, baseRepo, headRepo, pull, e.AtlantisGitlabUser, eventType) + e.handlePullRequestEvent(w, baseRepo, headRepo, pull, user, eventType) } // supportsHost returns true if h is in e.SupportedVCSHosts and false otherwise. diff --git a/server/events_controller_e2e_test.go b/server/events_controller_e2e_test.go index 7d9f300a7..5abb66ad3 100644 --- a/server/events_controller_e2e_test.go +++ b/server/events_controller_e2e_test.go @@ -164,7 +164,7 @@ func TestGitHubWorkflow(t *testing.T) { When(vcsClient.GetModifiedFiles(AnyRepo(), matchers.AnyModelsPullRequest())).ThenReturn(c.ModifiedFiles, nil) // First, send the open pull request event and trigger an autoplan. - pullOpenedReq := GitHubPullRequestOpenedEvent(t) + pullOpenedReq := GitHubPullRequestOpenedEvent(t, headSHA) ctrl.Post(w, pullOpenedReq) responseContains(t, w, 200, "Processing...") if c.ExpAutoplanCommentFile != "" { @@ -236,51 +236,51 @@ func setupE2E(t *testing.T) (server.EventsController, *vcsmocks.MockClientProxy, } defaultTFVersion := terraformClient.Version() - commandHandler := &events.CommandHandler{ + locker := events.NewDefaultAtlantisWorkspaceLocker() + commandRunner := &events.DefaultCommandRunner{ + ProjectCommandRunner: &events.ProjectCommandRunner{ + Locker: projectLocker, + LockURLGenerator: &mockLockURLGenerator{}, + InitStepRunner: runtime.InitStepRunner{ + TerraformExecutor: terraformClient, + DefaultTFVersion: defaultTFVersion, + }, + PlanStepRunner: runtime.PlanStepRunner{ + TerraformExecutor: terraformClient, + DefaultTFVersion: defaultTFVersion, + }, + ApplyStepRunner: runtime.ApplyStepRunner{ + TerraformExecutor: terraformClient, + }, + RunStepRunner: runtime.RunStepRunner{}, + PullApprovedChecker: runtime.PullApprovedChecker{ + VCSClient: e2eVCSClient, + }, + Workspace: atlantisWorkspace, + Webhooks: &mockWebhookSender{}, + AtlantisWorkspaceLocker: locker, + }, EventParser: eventParser, VCSClient: e2eVCSClient, GithubPullGetter: e2eGithubGetter, GitlabMergeRequestGetter: e2eGitlabGetter, CommitStatusUpdater: e2eStatusUpdater, - AtlantisWorkspaceLocker: events.NewDefaultAtlantisWorkspaceLocker(), MarkdownRenderer: &events.MarkdownRenderer{}, Logger: logger, AllowForkPRs: allowForkPRs, AllowForkPRsFlag: "allow-fork-prs", - PullRequestOperator: &events.DefaultPullRequestOperator{ - TerraformExecutor: terraformClient, - DefaultTFVersion: defaultTFVersion, - ParserValidator: &yaml.ParserValidator{}, - ProjectFinder: &events.DefaultProjectFinder{}, - VCSClient: e2eVCSClient, - Workspace: atlantisWorkspace, - ProjectOperator: events.ProjectOperator{ - Locker: projectLocker, - LockURLGenerator: &mockLockURLGenerator{}, - InitStepOperator: runtime.InitStepOperator{ - TerraformExecutor: terraformClient, - DefaultTFVersion: defaultTFVersion, - }, - PlanStepOperator: runtime.PlanStepOperator{ - TerraformExecutor: terraformClient, - DefaultTFVersion: defaultTFVersion, - }, - ApplyStepOperator: runtime.ApplyStepOperator{ - TerraformExecutor: terraformClient, - }, - RunStepOperator: runtime.RunStepOperator{}, - ApprovalOperator: runtime.ApprovalOperator{ - VCSClient: e2eVCSClient, - }, - Workspace: atlantisWorkspace, - Webhooks: &mockWebhookSender{}, - }, + ProjectCommandBuilder: &events.DefaultProjectCommandBuilder{ + ParserValidator: &yaml.ParserValidator{}, + ProjectFinder: &events.DefaultProjectFinder{}, + VCSClient: e2eVCSClient, + Workspace: atlantisWorkspace, + AtlantisWorkspaceLocker: locker, }, } ctrl := server.EventsController{ TestingMode: true, - CommandRunner: commandHandler, + CommandRunner: commandRunner, PullCleaner: &events.PullClosedExecutor{ Locker: lockingClient, VCSClient: e2eVCSClient, @@ -298,12 +298,6 @@ func setupE2E(t *testing.T) (server.EventsController, *vcsmocks.MockClientProxy, }, SupportedVCSHosts: []models.VCSHostType{models.Gitlab, models.Github}, VCSClient: e2eVCSClient, - AtlantisGithubUser: models.User{ - Username: "atlantisbot", - }, - AtlantisGitlabUser: models.User{ - Username: "atlantisbot", - }, } return ctrl, e2eVCSClient, e2eGithubGetter, atlantisWorkspace } @@ -331,10 +325,12 @@ func GitHubCommentEvent(t *testing.T, comment string) *http.Request { return req } -func GitHubPullRequestOpenedEvent(t *testing.T) *http.Request { +func GitHubPullRequestOpenedEvent(t *testing.T, headSHA string) *http.Request { requestJSON, err := ioutil.ReadFile(filepath.Join("testfixtures", "githubPullRequestOpenedEvent.json")) Ok(t, err) - req, err := http.NewRequest("POST", "/events", bytes.NewBuffer(requestJSON)) + // Replace sha with expected sha. + requestJSONStr := strings.Replace(string(requestJSON), "c31fd9ea6f557ad2ea659944c3844a059b83bc5d", headSHA, -1) + req, err := http.NewRequest("POST", "/events", bytes.NewBuffer([]byte(requestJSONStr))) Ok(t, err) req.Header.Set("Content-Type", "application/json") req.Header.Set(githubHeader, "pull_request") @@ -357,7 +353,7 @@ func GitHubPullRequestParsed(headSHA string) *github.PullRequest { headSHA = "13940d121be73f656e2132c6d7b4c8e87878ac8d" } return &github.PullRequest{ - Number: github.Int(1), + Number: github.Int(2), State: github.String("open"), HTMLURL: github.String("htmlurl"), Head: &github.PullRequestBranch{ diff --git a/server/events_controller_test.go b/server/events_controller_test.go index a7f5c81a6..225021086 100644 --- a/server/events_controller_test.go +++ b/server/events_controller_test.go @@ -16,6 +16,7 @@ package server_test import ( "bytes" "errors" + "fmt" "io/ioutil" "net/http" "net/http/httptest" @@ -267,9 +268,7 @@ func TestPost_GitlabCommentSuccess(t *testing.T) { e.Post(w, req) responseContains(t, w, http.StatusOK, "Processing...") - // wait for 200ms so goroutine is called - time.Sleep(200 * time.Millisecond) - cr.VerifyWasCalledOnce().ExecuteCommand(models.Repo{}, models.Repo{}, models.User{}, 0, nil) + cr.VerifyWasCalledOnce().RunCommentCommand(models.Repo{}, &models.Repo{}, models.User{}, 0, nil) } func TestPost_GithubCommentSuccess(t *testing.T) { @@ -281,16 +280,14 @@ func TestPost_GithubCommentSuccess(t *testing.T) { When(v.Validate(req, secret)).ThenReturn([]byte(event), nil) baseRepo := models.Repo{} user := models.User{} - cmd := events.Command{} + cmd := events.CommentCommand{} When(p.ParseGithubIssueCommentEvent(matchers.AnyPtrToGithubIssueCommentEvent())).ThenReturn(baseRepo, user, 1, nil) When(cp.Parse("", models.Github)).ThenReturn(events.CommentParseResult{Command: &cmd}) w := httptest.NewRecorder() e.Post(w, req) responseContains(t, w, http.StatusOK, "Processing...") - // wait for 200ms so goroutine is called - time.Sleep(200 * time.Millisecond) - cr.VerifyWasCalledOnce().ExecuteCommand(baseRepo, baseRepo, user, 1, &cmd) + cr.VerifyWasCalledOnce().RunCommentCommand(baseRepo, nil, user, 1, &cmd) } func TestPost_GithubPullRequestInvalid(t *testing.T) { @@ -301,45 +298,87 @@ func TestPost_GithubPullRequestInvalid(t *testing.T) { event := `{"action": "closed"}` When(v.Validate(req, secret)).ThenReturn([]byte(event), nil) - When(p.ParseGithubPull(matchers.AnyPtrToGithubPullRequest())).ThenReturn(models.PullRequest{}, models.Repo{}, errors.New("err")) + When(p.ParseGithubPullEvent(matchers.AnyPtrToGithubPullRequestEvent())).ThenReturn(models.PullRequest{}, models.Repo{}, models.Repo{}, models.User{}, errors.New("err")) w := httptest.NewRecorder() e.Post(w, req) responseContains(t, w, http.StatusBadRequest, "Error parsing pull data: err") } -func TestPost_GithubPullRequestInvalidRepo(t *testing.T) { - t.Log("when the event is a github pull request with invalid repo data we return a 400") - e, v, _, p, _, _, _, _ := setup(t) +func TestPost_GitlabMergeRequestInvalid(t *testing.T) { + t.Log("when the event is a gitlab merge request with invalid data we return a 400") + e, _, gl, p, _, _, _, _ := setup(t) req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) - req.Header.Set(githubHeader, "pull_request") - - event := `{"action": "closed"}` - When(v.Validate(req, secret)).ThenReturn([]byte(event), nil) - When(p.ParseGithubPull(matchers.AnyPtrToGithubPullRequest())).ThenReturn(models.PullRequest{}, models.Repo{}, nil) - When(p.ParseGithubRepo(matchers.AnyPtrToGithubRepository())).ThenReturn(models.Repo{}, errors.New("err")) + req.Header.Set(gitlabHeader, "value") + When(gl.ParseAndValidate(req, secret)).ThenReturn(gitlabMergeEvent, nil) + repo := models.Repo{} + pullRequest := models.PullRequest{State: models.Closed} + When(p.ParseGitlabMergeEvent(gitlabMergeEvent)).ThenReturn(pullRequest, repo, repo, models.User{}, errors.New("err")) w := httptest.NewRecorder() e.Post(w, req) - responseContains(t, w, http.StatusBadRequest, "Error parsing repo data: err") + responseContains(t, w, http.StatusBadRequest, "Error parsing webhook: err") } func TestPost_GithubPullRequestNotWhitelisted(t *testing.T) { t.Log("when the event is a github pull request to a non-whitelisted repo we return a 400") - e, v, _, p, _, _, _, _ := setup(t) + e, v, _, _, _, _, _, _ := setup(t) e.RepoWhitelistChecker = &events.RepoWhitelistChecker{Whitelist: "github.com/nevermatch"} req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req.Header.Set(githubHeader, "pull_request") event := `{"action": "closed"}` When(v.Validate(req, secret)).ThenReturn([]byte(event), nil) - When(p.ParseGithubPull(matchers.AnyPtrToGithubPullRequest())).ThenReturn(models.PullRequest{}, models.Repo{}, nil) - When(p.ParseGithubRepo(matchers.AnyPtrToGithubRepository())).ThenReturn(models.Repo{}, nil) w := httptest.NewRecorder() e.Post(w, req) responseContains(t, w, http.StatusForbidden, "Ignoring pull request event from non-whitelisted repo") } -func TestPost_GithubPullRequestErrCleaningPull(t *testing.T) { - t.Log("when the event is a pull request and we have an error calling CleanUpPull we return a 503") +func TestPost_GitlabMergeRequestNotWhitelisted(t *testing.T) { + t.Log("when the event is a gitlab merge request to a non-whitelisted repo we return a 400") + e, _, gl, p, _, _, _, _ := setup(t) + req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) + req.Header.Set(gitlabHeader, "value") + + e.RepoWhitelistChecker = &events.RepoWhitelistChecker{Whitelist: "github.com/nevermatch"} + When(gl.ParseAndValidate(req, secret)).ThenReturn(gitlabMergeEvent, nil) + repo := models.Repo{} + pullRequest := models.PullRequest{State: models.Closed} + When(p.ParseGitlabMergeEvent(gitlabMergeEvent)).ThenReturn(pullRequest, repo, repo, models.User{}, nil) + + w := httptest.NewRecorder() + e.Post(w, req) + responseContains(t, w, http.StatusForbidden, "Ignoring pull request event from non-whitelisted repo") +} + +func TestPost_GithubPullRequestUnsupportedAction(t *testing.T) { + e, v, _, _, _, _, _, _ := setup(t) + req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) + req.Header.Set(githubHeader, "pull_request") + + event := `{"action": "unsupported"}` + When(v.Validate(req, secret)).ThenReturn([]byte(event), nil) + w := httptest.NewRecorder() + e.Post(w, req) + responseContains(t, w, http.StatusOK, "Ignoring non-actionable pull request event") +} + +func TestPost_GitlabMergeRequestUnsupportedAction(t *testing.T) { + t.Log("when the event is a gitlab merge request to a non-whitelisted repo we return a 400") + e, _, gl, p, _, _, _, _ := setup(t) + req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) + req.Header.Set(gitlabHeader, "value") + gitlabMergeEvent.ObjectAttributes.Action = "unsupported" + When(gl.ParseAndValidate(req, secret)).ThenReturn(gitlabMergeEvent, nil) + repo := models.Repo{} + pullRequest := models.PullRequest{State: models.Closed} + When(p.ParseGitlabMergeEvent(gitlabMergeEvent)).ThenReturn(pullRequest, repo, repo, models.User{}, nil) + + w := httptest.NewRecorder() + e.Post(w, req) + responseContains(t, w, http.StatusOK, "Ignoring non-actionable pull request event") +} + +func TestPost_GithubPullRequestClosedErrCleaningPull(t *testing.T) { + t.Log("when the event is a closed pull request and we have an error calling CleanUpPull we return a 503") RegisterMockTestingT(t) e, v, _, p, _, c, _, _ := setup(t) req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) @@ -349,30 +388,30 @@ func TestPost_GithubPullRequestErrCleaningPull(t *testing.T) { When(v.Validate(req, secret)).ThenReturn([]byte(event), nil) repo := models.Repo{} pull := models.PullRequest{State: models.Closed} - When(p.ParseGithubPull(matchers.AnyPtrToGithubPullRequest())).ThenReturn(pull, repo, nil) - When(p.ParseGithubRepo(matchers.AnyPtrToGithubRepository())).ThenReturn(repo, nil) + When(p.ParseGithubPullEvent(matchers.AnyPtrToGithubPullRequestEvent())).ThenReturn(pull, repo, repo, models.User{}, nil) When(c.CleanUpPull(repo, pull)).ThenReturn(errors.New("cleanup err")) w := httptest.NewRecorder() e.Post(w, req) responseContains(t, w, http.StatusInternalServerError, "Error cleaning pull request: cleanup err") } -func TestPost_GitlabMergeRequestErrCleaningPull(t *testing.T) { - t.Log("when the event is a gitlab merge request and an error occurs calling CleanUpPull we return a 500") +func TestPost_GitlabMergeRequestClosedErrCleaningPull(t *testing.T) { + t.Log("when the event is a closed gitlab merge request and an error occurs calling CleanUpPull we return a 500") e, _, gl, p, _, c, _, _ := setup(t) req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req.Header.Set(gitlabHeader, "value") + gitlabMergeEvent.ObjectAttributes.Action = "close" When(gl.ParseAndValidate(req, secret)).ThenReturn(gitlabMergeEvent, nil) repo := models.Repo{} pullRequest := models.PullRequest{State: models.Closed} - When(p.ParseGitlabMergeEvent(gitlabMergeEvent)).ThenReturn(pullRequest, repo, repo, nil) + When(p.ParseGitlabMergeEvent(gitlabMergeEvent)).ThenReturn(pullRequest, repo, repo, models.User{}, nil) When(c.CleanUpPull(repo, pullRequest)).ThenReturn(errors.New("err")) w := httptest.NewRecorder() e.Post(w, req) responseContains(t, w, http.StatusInternalServerError, "Error cleaning pull request: err") } -func TestPost_GithubPullRequestSuccess(t *testing.T) { +func TestPost_GithubClosedPullRequestSuccess(t *testing.T) { t.Log("when the event is a pull request and everything works we return a 200") e, v, _, p, _, c, _, _ := setup(t) req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) @@ -382,8 +421,7 @@ func TestPost_GithubPullRequestSuccess(t *testing.T) { When(v.Validate(req, secret)).ThenReturn([]byte(event), nil) repo := models.Repo{} pull := models.PullRequest{State: models.Closed} - When(p.ParseGithubPull(matchers.AnyPtrToGithubPullRequest())).ThenReturn(pull, repo, nil) - When(p.ParseGithubRepo(matchers.AnyPtrToGithubRepository())).ThenReturn(repo, nil) + When(p.ParseGithubPullEvent(matchers.AnyPtrToGithubPullRequestEvent())).ThenReturn(pull, repo, repo, models.User{}, nil) When(c.CleanUpPull(repo, pull)).ThenReturn(nil) w := httptest.NewRecorder() e.Post(w, req) @@ -398,12 +436,68 @@ func TestPost_GitlabMergeRequestSuccess(t *testing.T) { When(gl.ParseAndValidate(req, secret)).ThenReturn(gitlabMergeEvent, nil) repo := models.Repo{} pullRequest := models.PullRequest{State: models.Closed} - When(p.ParseGitlabMergeEvent(gitlabMergeEvent)).ThenReturn(pullRequest, repo, repo, nil) + When(p.ParseGitlabMergeEvent(gitlabMergeEvent)).ThenReturn(pullRequest, repo, repo, models.User{}, nil) w := httptest.NewRecorder() e.Post(w, req) responseContains(t, w, http.StatusOK, "Pull request cleaned successfully") } +func TestPost_PullOpenedOrUpdated(t *testing.T) { + cases := []struct { + Description string + HostType models.VCSHostType + Action string + }{ + { + "github opened", + models.Github, + "opened", + }, + { + "gitlab opened", + models.Gitlab, + "open", + }, + { + "github synchronized", + models.Github, + "synchronize", + }, + { + "gitlab update", + models.Gitlab, + "update", + }, + } + + for _, c := range cases { + t.Run(c.Description, func(t *testing.T) { + e, v, gl, p, cr, _, _, _ := setup(t) + req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) + switch c.HostType { + case models.Gitlab: + req.Header.Set(gitlabHeader, "value") + gitlabMergeEvent.ObjectAttributes.Action = c.Action + When(gl.ParseAndValidate(req, secret)).ThenReturn(gitlabMergeEvent, nil) + repo := models.Repo{} + pullRequest := models.PullRequest{State: models.Closed} + When(p.ParseGitlabMergeEvent(gitlabMergeEvent)).ThenReturn(pullRequest, repo, repo, models.User{}, nil) + case models.Github: + req.Header.Set(githubHeader, "pull_request") + event := fmt.Sprintf(`{"action": "%s"}`, c.Action) + When(v.Validate(req, secret)).ThenReturn([]byte(event), nil) + repo := models.Repo{} + pull := models.PullRequest{State: models.Closed} + When(p.ParseGithubPullEvent(matchers.AnyPtrToGithubPullRequestEvent())).ThenReturn(pull, repo, repo, models.User{}, nil) + } + w := httptest.NewRecorder() + e.Post(w, req) + responseContains(t, w, http.StatusOK, "Processing...") + cr.VerifyWasCalledOnce().RunAutoplanCommand(models.Repo{}, models.Repo{}, models.PullRequest{State: models.Closed}, models.User{}) + }) + } +} + func setup(t *testing.T) (server.EventsController, *mocks.MockGithubRequestValidator, *mocks.MockGitlabRequestParserValidator, *emocks.MockEventParsing, *emocks.MockCommandRunner, *emocks.MockPullCleaner, *vcsmocks.MockClientProxy, *emocks.MockCommentParsing) { RegisterMockTestingT(t) v := mocks.NewMockGithubRequestValidator() @@ -414,6 +508,7 @@ func setup(t *testing.T) (server.EventsController, *mocks.MockGithubRequestValid c := emocks.NewMockPullCleaner() vcsmock := vcsmocks.NewMockClientProxy() e := server.EventsController{ + TestingMode: true, Logger: logging.NewNoopLogger(), GithubRequestValidator: v, Parser: p, diff --git a/server/router_test.go b/server/router_test.go new file mode 100644 index 000000000..91bf9f3fc --- /dev/null +++ b/server/router_test.go @@ -0,0 +1,27 @@ +package server_test + +import ( + "net/http" + "testing" + + "github.com/gorilla/mux" + "github.com/runatlantis/atlantis/server" + . "github.com/runatlantis/atlantis/testing" +) + +func TestRouter_GenerateLockURL(t *testing.T) { + queryParam := "queryparam" + routeName := "routename" + atlantisURL := "https://example.com" + + underlyingRouter := mux.NewRouter() + underlyingRouter.HandleFunc("/lock", func(_ http.ResponseWriter, _ *http.Request) {}).Methods("GET").Queries(queryParam, "{queryparam}").Name(routeName) + + router := &server.Router{ + AtlantisURL: atlantisURL, + LockViewRouteIDQueryParam: queryParam, + LockViewRouteName: routeName, + Underlying: underlyingRouter, + } + Equals(t, "https://example.com/lock?queryparam=myid", router.GenerateLockURL("myid")) +} diff --git a/server/server.go b/server/server.go index fe97620d5..1980ba252 100644 --- a/server/server.go +++ b/server/server.go @@ -63,7 +63,7 @@ type Server struct { AtlantisVersion string Router *mux.Router Port int - CommandHandler *events.CommandHandler + CommandRunner *events.DefaultCommandRunner Logger *logging.SimpleLogger Locker locking.Locker AtlantisURL string @@ -221,45 +221,43 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { GitlabToken: userConfig.GitlabToken, } defaultTfVersion := terraformClient.Version() - commandHandler := &events.CommandHandler{ - EventParser: eventParser, + commandRunner := &events.DefaultCommandRunner{ VCSClient: vcsClient, GithubPullGetter: githubClient, GitlabMergeRequestGetter: gitlabClient, CommitStatusUpdater: commitStatusUpdater, - AtlantisWorkspaceLocker: workspaceLocker, + EventParser: eventParser, MarkdownRenderer: markdownRenderer, Logger: logger, AllowForkPRs: userConfig.AllowForkPRs, AllowForkPRsFlag: config.AllowForkPRsFlag, - PullRequestOperator: &events.DefaultPullRequestOperator{ - TerraformExecutor: terraformClient, - DefaultTFVersion: defaultTfVersion, - ParserValidator: &yaml.ParserValidator{}, - ProjectFinder: &events.DefaultProjectFinder{}, - VCSClient: vcsClient, - Workspace: workspace, - ProjectOperator: events.ProjectOperator{ - Locker: projectLocker, - LockURLGenerator: router, - InitStepOperator: runtime.InitStepOperator{ - TerraformExecutor: terraformClient, - DefaultTFVersion: defaultTfVersion, - }, - PlanStepOperator: runtime.PlanStepOperator{ - TerraformExecutor: terraformClient, - DefaultTFVersion: defaultTfVersion, - }, - ApplyStepOperator: runtime.ApplyStepOperator{ - TerraformExecutor: terraformClient, - }, - RunStepOperator: runtime.RunStepOperator{}, - ApprovalOperator: runtime.ApprovalOperator{ - VCSClient: vcsClient, - }, - Workspace: workspace, - Webhooks: webhooksManager, + ProjectCommandBuilder: &events.DefaultProjectCommandBuilder{ + ParserValidator: &yaml.ParserValidator{}, + ProjectFinder: &events.DefaultProjectFinder{}, + VCSClient: vcsClient, + Workspace: workspace, + }, + ProjectCommandRunner: &events.ProjectCommandRunner{ + Locker: projectLocker, + LockURLGenerator: router, + InitStepRunner: runtime.InitStepRunner{ + TerraformExecutor: terraformClient, + DefaultTFVersion: defaultTfVersion, }, + PlanStepRunner: runtime.PlanStepRunner{ + TerraformExecutor: terraformClient, + DefaultTFVersion: defaultTfVersion, + }, + ApplyStepRunner: runtime.ApplyStepRunner{ + TerraformExecutor: terraformClient, + }, + RunStepRunner: runtime.RunStepRunner{}, + PullApprovedChecker: runtime.PullApprovedChecker{ + VCSClient: vcsClient, + }, + Workspace: workspace, + Webhooks: webhooksManager, + AtlantisWorkspaceLocker: workspaceLocker, }, } repoWhitelist := &events.RepoWhitelistChecker{ @@ -273,7 +271,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { LockDetailTemplate: lockTemplate, } eventsController := &EventsController{ - CommandRunner: commandHandler, + CommandRunner: commandRunner, PullCleaner: pullClosedExecutor, Parser: eventParser, CommentParser: commentParser, @@ -285,14 +283,12 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { RepoWhitelistChecker: repoWhitelist, SupportedVCSHosts: supportedVCSHosts, VCSClient: vcsClient, - AtlantisGithubUser: models.User{Username: userConfig.GithubUser}, - AtlantisGitlabUser: models.User{Username: userConfig.GitlabUser}, } return &Server{ AtlantisVersion: config.AtlantisVersion, Router: underlyingRouter, Port: userConfig.Port, - CommandHandler: commandHandler, + CommandRunner: commandRunner, Logger: logger, Locker: lockingClient, AtlantisURL: userConfig.AtlantisURL, @@ -313,7 +309,8 @@ func (s *Server) Start() error { s.Router.PathPrefix("/static/").Handler(http.FileServer(&assetfs.AssetFS{Asset: static.Asset, AssetDir: static.AssetDir, AssetInfo: static.AssetInfo})) s.Router.HandleFunc("/events", s.EventsController.Post).Methods("POST") s.Router.HandleFunc("/locks", s.LocksController.DeleteLock).Methods("DELETE").Queries("id", "{id:.*}") - s.Router.HandleFunc("/lock", s.LocksController.GetLock).Methods("GET").Queries(LockViewRouteIDQueryParam, "{id}").Name(LockViewRouteName) + s.Router.HandleFunc("/lock", s.LocksController.GetLock).Methods("GET"). + Queries(LockViewRouteIDQueryParam, fmt.Sprintf("{%s}", LockViewRouteIDQueryParam)).Name(LockViewRouteName) n := negroni.New(&negroni.Recovery{ Logger: log.New(os.Stdout, "", log.LstdFlags), PrintStack: false, diff --git a/server/server_test.go b/server/server_test.go index 54776cfe9..cec7a4fcb 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -103,6 +103,7 @@ func TestIndex_Success(t *testing.T) { } func responseContains(t *testing.T, r *httptest.ResponseRecorder, status int, bodySubstr string) { + t.Helper() Equals(t, status, r.Result().StatusCode) body, _ := ioutil.ReadAll(r.Result().Body) Assert(t, strings.Contains(string(body), bodySubstr), "exp %q to be contained in %q", bodySubstr, string(body)) diff --git a/server/testfixtures/githubPullRequestClosedEvent.json b/server/testfixtures/githubPullRequestClosedEvent.json index 1fd568761..cc281a8d7 100644 --- a/server/testfixtures/githubPullRequestClosedEvent.json +++ b/server/testfixtures/githubPullRequestClosedEvent.json @@ -1,15 +1,15 @@ { "action": "closed", - "number": 1, + "number": 2, "pull_request": { - "url": "https://api.github.com/repos/runatlantis/atlantis-tests/pulls/1", + "url": "https://api.github.com/repos/runatlantis/atlantis-tests/pulls/2", "id": 193308707, "node_id": "MDExOlB1bGxSZXF1ZXN0MTkzMzA4NzA3", - "html_url": "https://github.com/runatlantis/atlantis-tests/pull/1", - "diff_url": "https://github.com/runatlantis/atlantis-tests/pull/1.diff", - "patch_url": "https://github.com/runatlantis/atlantis-tests/pull/1.patch", - "issue_url": "https://api.github.com/repos/runatlantis/atlantis-tests/issues/1", - "number": 1, + "html_url": "https://github.com/runatlantis/atlantis-tests/pull/2", + "diff_url": "https://github.com/runatlantis/atlantis-tests/pull/2.diff", + "patch_url": "https://github.com/runatlantis/atlantis-tests/pull/2.patch", + "issue_url": "https://api.github.com/repos/runatlantis/atlantis-tests/issues/2", + "number": 2, "state": "closed", "locked": false, "title": "Add new project layouts", @@ -53,10 +53,10 @@ ], "milestone": null, - "commits_url": "https://api.github.com/repos/runatlantis/atlantis-tests/pulls/1/commits", - "review_comments_url": "https://api.github.com/repos/runatlantis/atlantis-tests/pulls/1/comments", + "commits_url": "https://api.github.com/repos/runatlantis/atlantis-tests/pulls/2/commits", + "review_comments_url": "https://api.github.com/repos/runatlantis/atlantis-tests/pulls/2/comments", "review_comment_url": "https://api.github.com/repos/runatlantis/atlantis-tests/pulls/comments{/number}", - "comments_url": "https://api.github.com/repos/runatlantis/atlantis-tests/issues/1/comments", + "comments_url": "https://api.github.com/repos/runatlantis/atlantis-tests/issues/2/comments", "statuses_url": "https://api.github.com/repos/runatlantis/atlantis-tests/statuses/5e2d140b2d74bf61675677f01dc947ae8512e18e", "head": { "label": "runatlantis:atlantisyaml", @@ -308,25 +308,25 @@ }, "_links": { "self": { - "href": "https://api.github.com/repos/runatlantis/atlantis-tests/pulls/1" + "href": "https://api.github.com/repos/runatlantis/atlantis-tests/pulls/2" }, "html": { - "href": "https://github.com/runatlantis/atlantis-tests/pull/1" + "href": "https://github.com/runatlantis/atlantis-tests/pull/2" }, "issue": { - "href": "https://api.github.com/repos/runatlantis/atlantis-tests/issues/1" + "href": "https://api.github.com/repos/runatlantis/atlantis-tests/issues/2" }, "comments": { - "href": "https://api.github.com/repos/runatlantis/atlantis-tests/issues/1/comments" + "href": "https://api.github.com/repos/runatlantis/atlantis-tests/issues/2/comments" }, "review_comments": { - "href": "https://api.github.com/repos/runatlantis/atlantis-tests/pulls/1/comments" + "href": "https://api.github.com/repos/runatlantis/atlantis-tests/pulls/2/comments" }, "review_comment": { "href": "https://api.github.com/repos/runatlantis/atlantis-tests/pulls/comments{/number}" }, "commits": { - "href": "https://api.github.com/repos/runatlantis/atlantis-tests/pulls/1/commits" + "href": "https://api.github.com/repos/runatlantis/atlantis-tests/pulls/2/commits" }, "statuses": { "href": "https://api.github.com/repos/runatlantis/atlantis-tests/statuses/5e2d140b2d74bf61675677f01dc947ae8512e18e" diff --git a/server/testfixtures/githubPullRequestOpenedEvent.json b/server/testfixtures/githubPullRequestOpenedEvent.json index 0b969438e..03ee106b5 100644 --- a/server/testfixtures/githubPullRequestOpenedEvent.json +++ b/server/testfixtures/githubPullRequestOpenedEvent.json @@ -12,7 +12,7 @@ "number": 2, "state": "open", "locked": false, - "title": "Noyaml", + "title": "branch", "user": { "login": "runatlantis", "id": 1034429, @@ -59,8 +59,8 @@ "comments_url": "https://api.github.com/repos/runatlantis/atlantis-tests/issues/2/comments", "statuses_url": "https://api.github.com/repos/runatlantis/atlantis-tests/statuses/c31fd9ea6f557ad2ea659944c3844a059b83bc5d", "head": { - "label": "runatlantis:noyaml", - "ref": "noyaml", + "label": "runatlantis:branch", + "ref": "branch", "sha": "c31fd9ea6f557ad2ea659944c3844a059b83bc5d", "user": { "login": "runatlantis", diff --git a/server/testfixtures/test-repos/tfvars-yaml/exp-output-apply-default.txt.act b/server/testfixtures/test-repos/tfvars-yaml/exp-output-apply-default.txt.act deleted file mode 100644 index aa4f73472..000000000 --- a/server/testfixtures/test-repos/tfvars-yaml/exp-output-apply-default.txt.act +++ /dev/null @@ -1,7 +0,0 @@ -Ran Apply in dir: `.` workspace: `default` -**Apply Error** -``` -no plan found at path "." and workspace "default"–did you run plan? -``` - - diff --git a/server/testfixtures/test-repos/tfvars-yaml/exp-output-autoplan.txt.act b/server/testfixtures/test-repos/tfvars-yaml/exp-output-autoplan.txt.act deleted file mode 100644 index 0da9bc5a5..000000000 --- a/server/testfixtures/test-repos/tfvars-yaml/exp-output-autoplan.txt.act +++ /dev/null @@ -1,61 +0,0 @@ -Ran Plan for 2 projects: -1. workspace: `default` path: `.` -1. workspace: `default` path: `.` - -### 1. workspace: `default` path: `.` -**Plan Error** -``` -exit status 1: running "sh -c terraform init -no-color -backend-config=default.backend.tfvars" in "/var/folders/z1/4z__7jv12y7f9yz__nwy55z40000gp/T/201347935/repos/runatlantis/atlantis-tests/1/default": - -Initializing the backend... - -Successfully configured the backend "local"! Terraform will automatically -use this backend unless the backend configuration changes. - -Initializing provider plugins... -- Checking for available provider plugins on https://releases.hashicorp.com... - -Error installing provider "null": Get https://releases.hashicorp.com/terraform-provider-null/: net/http: TLS handshake timeout. - -Terraform analyses the configuration and state and automatically downloads -plugins for the providers used. However, when attempting to download this -plugin an unexpected error occured. - -This may be caused if for some reason Terraform is unable to reach the -plugin repository. The repository may be unreachable if access is blocked -by a firewall. - -If automatic installation is not possible or desirable in your environment, -you may alternatively manually install plugins by downloading a suitable -distribution package and placing the plugin's executable file in the -following directory: - terraform.d/plugins/darwin_amd64 - - -``` - ---- -### 2. workspace: `default` path: `.` -```diff -Refreshing Terraform state in-memory prior to plan... -The refreshed state will be used to calculate this plan, but will not be -persisted to local or remote state storage. - - ------------------------------------------------------------------------- - -An execution plan has been generated and is shown below. -Resource actions are indicated with the following symbols: - + create - -Terraform will perform the following actions: - -+ null_resource.simple - id: -Plan: 1 to add, 0 to change, 0 to destroy. - -``` - -* To **discard** this plan click [here](lock-url). ---- -