// 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 import ( "fmt" "github.com/google/go-github/github" "github.com/lkysow/go-gitlab" "github.com/pkg/errors" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/events/vcs" "github.com/runatlantis/atlantis/server/logging" "github.com/runatlantis/atlantis/server/recovery" ) //go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_command_runner.go CommandRunner // 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. // 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) } //go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_github_pull_getter.go GithubPullGetter // GithubPullGetter makes API calls to get pull requests. type GithubPullGetter interface { // GetPullRequest gets the pull request with id pullNum for the repo. GetPullRequest(repo models.Repo, pullNum int) (*github.PullRequest, error) } //go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_gitlab_merge_request_getter.go GitlabMergeRequestGetter // GitlabMergeRequestGetter makes API calls to get merge requests. type GitlabMergeRequestGetter interface { // GetMergeRequest gets the pull request with the id pullNum for the repo. GetMergeRequest(repoFullName string, pullNum int) (*gitlab.MergeRequest, error) } // CommandHandler is the first step when processing a comment command. type CommandHandler 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. AllowForkPRs bool // 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 } // 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) { log := c.buildLogger(baseRepo.FullName, pullNum) var err error var pull models.PullRequest switch baseRepo.VCSHost.Type { case models.Github: pull, headRepo, err = c.getGithubData(baseRepo, pullNum) case models.Gitlab: pull, err = c.getGitlabData(baseRepo, pullNum) default: err = errors.New("Unknown VCS type, this is a bug!") } if err != nil { log.Err(err.Error()) return } ctx := &CommandContext{ User: user, Log: log, Pull: pull, HeadRepo: headRepo, Command: cmd, BaseRepo: baseRepo, } c.run(ctx) } func (c *CommandHandler) 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") } ghPull, err := c.GithubPullGetter.GetPullRequest(baseRepo, pullNum) 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) if err != nil { return pull, repo, errors.Wrap(err, "extracting required fields from comment data") } return pull, repo, nil } func (c *CommandHandler) getGitlabData(baseRepo models.Repo, pullNum int) (models.PullRequest, error) { if c.GitlabMergeRequestGetter == nil { return models.PullRequest{}, errors.New("Atlantis not configured to support GitLab") } mr, err := c.GitlabMergeRequestGetter.GetMergeRequest(baseRepo.FullName, pullNum) if err != nil { return models.PullRequest{}, errors.Wrap(err, "making merge request API call to GitLab") } pull := c.EventParser.ParseGitlabMergeRequest(mr, baseRepo) return pull, nil } func (c *CommandHandler) 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) 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 } 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 } if err := c.CommitStatusUpdater.Update(ctx.BaseRepo, ctx.Pull, vcs.Pending, ctx.Command); 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}) return } 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) } func (c *CommandHandler) updatePull(ctx *CommandContext, res CommandResponse) { // Log if we got any errors or failures. if res.Error != nil { ctx.Log.Err(res.Error.Error()) } else if res.Failure != "" { ctx.Log.Warn(res.Failure) } // Update the pull request's status icon and comment back. if err := c.CommitStatusUpdater.UpdateProjectResult(ctx, 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) 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) { if err := recover(); err != nil { stack := recovery.Stack(3) c.VCSClient.CreateComment(ctx.BaseRepo, ctx.Pull.Num, // nolint: errcheck fmt.Sprintf("**Error: goroutine panic. This is a bug.**\n```\n%s\n%s```", err, stack)) ctx.Log.Err("PANIC: %s\n%s", err, stack) } }