This commit is contained in:
Luke Kysow
2018-06-21 18:18:54 +01:00
parent d359131a29
commit d3ec832eff
57 changed files with 1776 additions and 1124 deletions

View File

@@ -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

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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

View File

@@ -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)
//}

View File

@@ -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),
}
}

View File

@@ -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 {

View File

@@ -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()))
})
}
}

View File

@@ -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.

View File

@@ -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": {

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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+"<details><summary>Log</summary>\n <p>\n\n```\nlog```\n</p></details>\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+"<details><summary>Log</summary>\n <p>\n\n```\nlog```\n</p></details>\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+"<details><summary>Log</summary>\n <p>\n\n```\nlog```\n</p></details>\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+"<details><summary>Log</summary>\n <p>\n\n```\nlog```\n</p></details>\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+"<details><summary>Log</summary>\n <p>\n\n```\nlog```\n</p></details>\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+"<details><summary>Log</summary>\n <p>\n\n```\nlog```\n</p></details>\n", s)
}
})
}
})
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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())
}

View File

@@ -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)

View File

@@ -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 cloneddid 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{
{

View File

@@ -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

View File

@@ -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 != "" {

View File

@@ -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")

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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

View File

@@ -1,3 +0,0 @@
package runtime_test
// todo

View File

@@ -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")
}

View File

@@ -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)
})
}
}

View File

@@ -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"),

View File

@@ -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())
}
}

View File

@@ -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.

View File

@@ -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{

View File

@@ -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,

27
server/router_test.go Normal file
View File

@@ -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"))
}

View File

@@ -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,

View File

@@ -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))

View File

@@ -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"

View File

@@ -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",

View File

@@ -1,7 +0,0 @@
Ran Apply in dir: `.` workspace: `default`
**Apply Error**
```
no plan found at path "." and workspace "default"did you run plan?
```

View File

@@ -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: <computed>
Plan: 1 to add, 0 to change, 0 to destroy.
```
* To **discard** this plan click [here](lock-url).
---