Files
atlantis/server/events_controller.go
Nish Krishnan 7ac8106f8a Add structured logger (#54) (#1467)
This is two changes:

Replacing all usages of SimpleLogger with it's interface SimpleLogging which makes it easier to swap out loggers going forward
Nukes SimpleLogger in favor of StructuredLogger which allows us to better analyze logs in our log management system we choose.
2021-04-06 12:02:25 -07:00

556 lines
24 KiB
Go

// 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 server
import (
"fmt"
"io/ioutil"
"net/http"
"strings"
"github.com/google/go-github/v31/github"
"github.com/mcdafydd/go-azuredevops/azuredevops"
"github.com/microcosm-cc/bluemonday"
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/events"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/events/vcs"
"github.com/runatlantis/atlantis/server/events/vcs/bitbucketcloud"
"github.com/runatlantis/atlantis/server/events/vcs/bitbucketserver"
"github.com/runatlantis/atlantis/server/logging"
gitlab "github.com/xanzy/go-gitlab"
)
const githubHeader = "X-Github-Event"
const gitlabHeader = "X-Gitlab-Event"
const azuredevopsHeader = "Request-Id"
// bitbucketEventTypeHeader is the same in both cloud and server.
const bitbucketEventTypeHeader = "X-Event-Key"
const bitbucketCloudRequestIDHeader = "X-Request-UUID"
const bitbucketServerRequestIDHeader = "X-Request-ID"
const bitbucketServerSignatureHeader = "X-Hub-Signature"
// EventsController handles all webhook requests which signify 'events' in the
// VCS host, ex. GitHub.
type EventsController struct {
CommandRunner events.CommandRunner
PullCleaner events.PullCleaner
Logger logging.SimpleLogging
Parser events.EventParsing
CommentParser events.CommentParsing
ApplyDisabled bool
// GithubWebhookSecret is the secret added to this webhook via the GitHub
// UI that identifies this call as coming from GitHub. If empty, no
// request validation is done.
GithubWebhookSecret []byte
GithubRequestValidator GithubRequestValidator
GitlabRequestParserValidator GitlabRequestParserValidator
// GitlabWebhookSecret is the secret added to this webhook via the GitLab
// UI that identifies this call as coming from GitLab. If empty, no
// request validation is done.
GitlabWebhookSecret []byte
RepoAllowlistChecker *events.RepoAllowlistChecker
// SilenceAllowlistErrors controls whether we write an error comment on
// pull requests from non-allowlisted repos.
SilenceAllowlistErrors bool
// SupportedVCSHosts is which VCS hosts Atlantis was configured upon
// startup to support.
SupportedVCSHosts []models.VCSHostType
VCSClient vcs.Client
TestingMode bool
// BitbucketWebhookSecret is the secret added to this webhook via the Bitbucket
// UI that identifies this call as coming from Bitbucket. If empty, no
// request validation is done.
BitbucketWebhookSecret []byte
// AzureDevopsWebhookUser is the Basic authentication username added to this
// webhook via the Azure DevOps UI that identifies this call as coming from your
// Azure DevOps Team Project. If empty, no request validation is done.
// For more information, see https://docs.microsoft.com/en-us/azure/devops/service-hooks/services/webhooks?view=azure-devops
AzureDevopsWebhookBasicUser []byte
// AzureDevopsWebhookPassword is the Basic authentication password added to this
// webhook via the Azure DevOps UI that identifies this call as coming from your
// Azure DevOps Team Project. If empty, no request validation is done.
AzureDevopsWebhookBasicPassword []byte
AzureDevopsRequestValidator AzureDevopsRequestValidator
}
// Post handles POST webhook requests.
func (e *EventsController) Post(w http.ResponseWriter, r *http.Request) {
if r.Header.Get(githubHeader) != "" {
if !e.supportsHost(models.Github) {
e.respond(w, logging.Debug, http.StatusBadRequest, "Ignoring request since not configured to support GitHub")
return
}
e.Logger.Debug("handling GitHub post")
e.handleGithubPost(w, r)
return
} else if r.Header.Get(gitlabHeader) != "" {
if !e.supportsHost(models.Gitlab) {
e.respond(w, logging.Debug, http.StatusBadRequest, "Ignoring request since not configured to support GitLab")
return
}
e.Logger.Debug("handling GitLab post")
e.handleGitlabPost(w, r)
return
} else if r.Header.Get(bitbucketEventTypeHeader) != "" {
// Bitbucket Cloud and Server use the same event type header but they
// use different request ID headers.
if r.Header.Get(bitbucketCloudRequestIDHeader) != "" {
if !e.supportsHost(models.BitbucketCloud) {
e.respond(w, logging.Debug, http.StatusBadRequest, "Ignoring request since not configured to support Bitbucket Cloud")
return
}
e.Logger.Debug("handling Bitbucket Cloud post")
e.handleBitbucketCloudPost(w, r)
return
} else if r.Header.Get(bitbucketServerRequestIDHeader) != "" {
if !e.supportsHost(models.BitbucketServer) {
e.respond(w, logging.Debug, http.StatusBadRequest, "Ignoring request since not configured to support Bitbucket Server")
return
}
e.Logger.Debug("handling Bitbucket Server post")
e.handleBitbucketServerPost(w, r)
return
}
} else if r.Header.Get(azuredevopsHeader) != "" {
if !e.supportsHost(models.AzureDevops) {
e.respond(w, logging.Debug, http.StatusBadRequest, "Ignoring request since not configured to support AzureDevops")
return
}
e.Logger.Debug("handling AzureDevops post")
e.handleAzureDevopsPost(w, r)
return
}
e.respond(w, logging.Debug, http.StatusBadRequest, "Ignoring request")
}
func (e *EventsController) handleGithubPost(w http.ResponseWriter, r *http.Request) {
// Validate the request against the optional webhook secret.
payload, err := e.GithubRequestValidator.Validate(r, e.GithubWebhookSecret)
if err != nil {
e.respond(w, logging.Warn, http.StatusBadRequest, err.Error())
return
}
e.Logger.Debug("request valid")
githubReqID := "X-Github-Delivery=" + r.Header.Get("X-Github-Delivery")
event, _ := github.ParseWebHook(github.WebHookType(r), payload)
switch event := event.(type) {
case *github.IssueCommentEvent:
e.Logger.Debug("handling as comment event")
e.HandleGithubCommentEvent(w, event, githubReqID)
case *github.PullRequestEvent:
e.Logger.Debug("handling as pull request event")
e.HandleGithubPullRequestEvent(w, event, githubReqID)
default:
e.respond(w, logging.Debug, http.StatusOK, "Ignoring unsupported event %s", githubReqID)
}
}
func (e *EventsController) handleBitbucketCloudPost(w http.ResponseWriter, r *http.Request) {
eventType := r.Header.Get(bitbucketEventTypeHeader)
reqID := r.Header.Get(bitbucketCloudRequestIDHeader)
defer r.Body.Close() // nolint: errcheck
body, err := ioutil.ReadAll(r.Body)
if err != nil {
e.respond(w, logging.Error, http.StatusBadRequest, "Unable to read body: %s %s=%s", err, bitbucketCloudRequestIDHeader, reqID)
return
}
switch eventType {
case bitbucketcloud.PullCreatedHeader, bitbucketcloud.PullUpdatedHeader, bitbucketcloud.PullFulfilledHeader, bitbucketcloud.PullRejectedHeader:
e.Logger.Debug("handling as pull request state changed event")
e.handleBitbucketCloudPullRequestEvent(w, eventType, body, reqID)
return
case bitbucketcloud.PullCommentCreatedHeader:
e.Logger.Debug("handling as comment created event")
e.HandleBitbucketCloudCommentEvent(w, body, reqID)
return
default:
e.respond(w, logging.Debug, http.StatusOK, "Ignoring unsupported event type %s %s=%s", eventType, bitbucketCloudRequestIDHeader, reqID)
}
}
func (e *EventsController) handleBitbucketServerPost(w http.ResponseWriter, r *http.Request) {
eventType := r.Header.Get(bitbucketEventTypeHeader)
reqID := r.Header.Get(bitbucketServerRequestIDHeader)
sig := r.Header.Get(bitbucketServerSignatureHeader)
defer r.Body.Close() // nolint: errcheck
body, err := ioutil.ReadAll(r.Body)
if err != nil {
e.respond(w, logging.Error, http.StatusBadRequest, "Unable to read body: %s %s=%s", err, bitbucketServerRequestIDHeader, reqID)
return
}
if eventType == bitbucketserver.DiagnosticsPingHeader {
// Specially handle the diagnostics:ping event because Bitbucket Server
// doesn't send the signature with this event for some reason.
e.respond(w, logging.Info, http.StatusOK, "Successfully received %s event %s=%s", eventType, bitbucketServerRequestIDHeader, reqID)
return
}
if len(e.BitbucketWebhookSecret) > 0 {
if err := bitbucketserver.ValidateSignature(body, sig, e.BitbucketWebhookSecret); err != nil {
e.respond(w, logging.Warn, http.StatusBadRequest, errors.Wrap(err, "request did not pass validation").Error())
return
}
}
switch eventType {
case bitbucketserver.PullCreatedHeader, bitbucketserver.PullMergedHeader, bitbucketserver.PullDeclinedHeader, bitbucketserver.PullDeletedHeader:
e.Logger.Debug("handling as pull request state changed event")
e.handleBitbucketServerPullRequestEvent(w, eventType, body, reqID)
return
case bitbucketserver.PullCommentCreatedHeader:
e.Logger.Debug("handling as comment created event")
e.HandleBitbucketServerCommentEvent(w, body, reqID)
return
default:
e.respond(w, logging.Debug, http.StatusOK, "Ignoring unsupported event type %s %s=%s", eventType, bitbucketServerRequestIDHeader, reqID)
}
}
func (e *EventsController) handleAzureDevopsPost(w http.ResponseWriter, r *http.Request) {
// Validate the request against the optional basic auth username and password.
payload, err := e.AzureDevopsRequestValidator.Validate(r, e.AzureDevopsWebhookBasicUser, e.AzureDevopsWebhookBasicPassword)
if err != nil {
e.respond(w, logging.Warn, http.StatusUnauthorized, err.Error())
return
}
e.Logger.Debug("request valid")
azuredevopsReqID := "Request-Id=" + r.Header.Get("Request-Id")
event, err := azuredevops.ParseWebHook(payload)
if err != nil {
e.respond(w, logging.Error, http.StatusBadRequest, "Failed parsing webhook: %v %s", err, azuredevopsReqID)
return
}
switch event.PayloadType {
case azuredevops.PullRequestCommentedEvent:
e.Logger.Debug("handling as pull request commented event")
e.HandleAzureDevopsPullRequestCommentedEvent(w, event, azuredevopsReqID)
case azuredevops.PullRequestEvent:
e.Logger.Debug("handling as pull request event")
e.HandleAzureDevopsPullRequestEvent(w, event, azuredevopsReqID)
default:
e.respond(w, logging.Debug, http.StatusOK, "Ignoring unsupported event: %v %s", event.PayloadType, azuredevopsReqID)
}
}
// HandleGithubCommentEvent handles comment events from GitHub where Atlantis
// commands can come from. It's exported to make testing easier.
func (e *EventsController) HandleGithubCommentEvent(w http.ResponseWriter, event *github.IssueCommentEvent, githubReqID string) {
if event.GetAction() != "created" {
e.respond(w, logging.Debug, http.StatusOK, "Ignoring comment event since action was not created %s", githubReqID)
return
}
baseRepo, user, pullNum, err := e.Parser.ParseGithubIssueCommentEvent(event)
if err != nil {
e.respond(w, logging.Error, http.StatusBadRequest, "Failed parsing event: %v %s", err, githubReqID)
return
}
// We pass in nil for maybeHeadRepo because the head repo data isn't
// available in the GithubIssueComment event.
e.handleCommentEvent(w, baseRepo, nil, nil, user, pullNum, event.Comment.GetBody(), models.Github)
}
// HandleBitbucketCloudCommentEvent handles comment events from Bitbucket.
func (e *EventsController) HandleBitbucketCloudCommentEvent(w http.ResponseWriter, body []byte, reqID string) {
pull, baseRepo, headRepo, user, comment, err := e.Parser.ParseBitbucketCloudPullCommentEvent(body)
if err != nil {
e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing pull data: %s %s=%s", err, bitbucketCloudRequestIDHeader, reqID)
return
}
e.handleCommentEvent(w, baseRepo, &headRepo, &pull, user, pull.Num, comment, models.BitbucketCloud)
}
// HandleBitbucketServerCommentEvent handles comment events from Bitbucket.
func (e *EventsController) HandleBitbucketServerCommentEvent(w http.ResponseWriter, body []byte, reqID string) {
pull, baseRepo, headRepo, user, comment, err := e.Parser.ParseBitbucketServerPullCommentEvent(body)
if err != nil {
e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing pull data: %s %s=%s", err, bitbucketCloudRequestIDHeader, reqID)
return
}
e.handleCommentEvent(w, baseRepo, &headRepo, &pull, user, pull.Num, comment, models.BitbucketCloud)
}
func (e *EventsController) handleBitbucketCloudPullRequestEvent(w http.ResponseWriter, eventType string, body []byte, reqID string) {
pull, baseRepo, headRepo, user, err := e.Parser.ParseBitbucketCloudPullEvent(body)
if err != nil {
e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing pull data: %s %s=%s", err, bitbucketCloudRequestIDHeader, reqID)
return
}
pullEventType := e.Parser.GetBitbucketCloudPullEventType(eventType)
e.Logger.Info("identified event as type %q", pullEventType.String())
e.handlePullRequestEvent(w, baseRepo, headRepo, pull, user, pullEventType)
}
func (e *EventsController) handleBitbucketServerPullRequestEvent(w http.ResponseWriter, eventType string, body []byte, reqID string) {
pull, baseRepo, headRepo, user, err := e.Parser.ParseBitbucketServerPullEvent(body)
if err != nil {
e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing pull data: %s %s=%s", err, bitbucketServerRequestIDHeader, reqID)
return
}
pullEventType := e.Parser.GetBitbucketServerPullEventType(eventType)
e.Logger.Info("identified event as type %q", pullEventType.String())
e.handlePullRequestEvent(w, baseRepo, headRepo, pull, user, pullEventType)
}
// 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, pullEventType, 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
}
e.Logger.Info("identified event as type %q", pullEventType.String())
e.handlePullRequestEvent(w, baseRepo, headRepo, pull, user, pullEventType)
}
func (e *EventsController) handlePullRequestEvent(w http.ResponseWriter, baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User, eventType models.PullRequestEventType) {
if !e.RepoAllowlistChecker.IsAllowlisted(baseRepo.FullName, baseRepo.VCSHost.Hostname) {
// If the repo isn't allowlisted and we receive an opened pull request
// event we comment back on the pull request that the repo isn't
// allowlisted. This is because the user might be expecting Atlantis to
// autoplan. For other events, we just ignore them.
if eventType == models.OpenedPullEvent {
e.commentNotAllowlisted(baseRepo, pull.Num)
}
e.respond(w, logging.Debug, http.StatusForbidden,
"Ignoring pull request event from non-allowlisted repo \"%s/%s\"",
baseRepo.VCSHost.Hostname, baseRepo.FullName)
return
}
switch eventType {
case models.OpenedPullEvent, models.UpdatedPullEvent:
// If the pull request was opened or updated, we will try to autoplan.
// Respond with success and then actually execute the command asynchronously.
// We use a goroutine so that this function returns and the connection is
// closed.
fmt.Fprintln(w, "Processing...")
e.Logger.Info("executing autoplan")
if !e.TestingMode {
go e.CommandRunner.RunAutoplanCommand(baseRepo, headRepo, pull, user)
} else {
// When testing we want to wait for everything to complete.
e.CommandRunner.RunAutoplanCommand(baseRepo, headRepo, pull, user)
}
return
case models.ClosedPullEvent:
// If the pull request was closed, we delete locks.
if err := e.PullCleaner.CleanUpPull(baseRepo, pull); err != nil {
e.respond(w, logging.Error, http.StatusInternalServerError, "Error cleaning pull request: %s", err)
return
}
e.Logger.Info("deleted locks and workspace for repo %s, pull %d", baseRepo.FullName, pull.Num)
fmt.Fprintln(w, "Pull request cleaned successfully")
return
case models.OtherPullEvent:
// Else we ignore the event.
e.respond(w, logging.Debug, http.StatusOK, "Ignoring non-actionable pull request event")
return
}
}
func (e *EventsController) handleGitlabPost(w http.ResponseWriter, r *http.Request) {
event, err := e.GitlabRequestParserValidator.ParseAndValidate(r, e.GitlabWebhookSecret)
if err != nil {
e.respond(w, logging.Warn, http.StatusBadRequest, err.Error())
return
}
e.Logger.Debug("request valid")
switch event := event.(type) {
case gitlab.MergeCommentEvent:
e.Logger.Debug("handling as comment event")
e.HandleGitlabCommentEvent(w, event)
case gitlab.MergeEvent:
e.Logger.Debug("handling as pull request event")
e.HandleGitlabMergeRequestEvent(w, event)
case gitlab.CommitCommentEvent:
e.Logger.Debug("comments on commits are not supported, only comments on merge requests")
e.respond(w, logging.Debug, http.StatusOK, "Ignoring comment on commit event")
default:
e.respond(w, logging.Debug, http.StatusOK, "Ignoring unsupported event")
}
}
// HandleGitlabCommentEvent handles comment events from GitLab where Atlantis
// commands can come from. It's exported to make testing easier.
func (e *EventsController) HandleGitlabCommentEvent(w http.ResponseWriter, event gitlab.MergeCommentEvent) {
// todo: can gitlab return the pull request here too?
baseRepo, headRepo, user, err := e.Parser.ParseGitlabMergeRequestCommentEvent(event)
if err != nil {
e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing webhook: %s", err)
return
}
e.handleCommentEvent(w, baseRepo, &headRepo, nil, user, event.MergeRequest.IID, event.ObjectAttributes.Note, models.Gitlab)
}
func (e *EventsController) handleCommentEvent(w http.ResponseWriter, baseRepo models.Repo, maybeHeadRepo *models.Repo, maybePull *models.PullRequest, user models.User, pullNum int, comment string, vcsHost models.VCSHostType) {
parseResult := e.CommentParser.Parse(comment, vcsHost)
if parseResult.Ignore {
truncated := comment
truncateLen := 40
if len(truncated) > truncateLen {
truncated = comment[:truncateLen] + "..."
}
e.respond(w, logging.Debug, http.StatusOK, "Ignoring non-command comment: %q", truncated)
return
}
e.Logger.Info("parsed comment as %s", parseResult.Command)
// At this point we know it's a command we're not supposed to ignore, so now
// we check if this repo is allowed to run commands in the first place.
if !e.RepoAllowlistChecker.IsAllowlisted(baseRepo.FullName, baseRepo.VCSHost.Hostname) {
e.commentNotAllowlisted(baseRepo, pullNum)
e.respond(w, logging.Warn, http.StatusForbidden, "Repo not allowlisted")
return
}
// If the command isn't valid or doesn't require processing, ex.
// "atlantis help" then we just comment back immediately.
// We do this here rather than earlier because we need access to the pull
// variable to comment back on the pull request.
if parseResult.CommentResponse != "" {
if err := e.VCSClient.CreateComment(baseRepo, pullNum, parseResult.CommentResponse, ""); err != nil {
e.Logger.Err("unable to comment on pull request: %s", err)
}
e.respond(w, logging.Info, http.StatusOK, "Commenting back on pull request")
return
}
e.Logger.Debug("executing command")
fmt.Fprintln(w, "Processing...")
if !e.TestingMode {
// 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.RunCommentCommand(baseRepo, maybeHeadRepo, maybePull, user, pullNum, parseResult.Command)
} else {
// When testing we want to wait for everything to complete.
e.CommandRunner.RunCommentCommand(baseRepo, maybeHeadRepo, maybePull, user, pullNum, parseResult.Command)
}
}
// HandleGitlabMergeRequestEvent will delete any locks associated with the pull
// 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, pullEventType, baseRepo, headRepo, user, err := e.Parser.ParseGitlabMergeRequestEvent(event)
if err != nil {
e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing webhook: %s", err)
return
}
e.Logger.Info("identified event as type %q", pullEventType.String())
e.handlePullRequestEvent(w, baseRepo, headRepo, pull, user, pullEventType)
}
// HandleAzureDevopsPullRequestCommentedEvent handles comment events from Azure DevOps where Atlantis
// commands can come from. It's exported to make testing easier.
// Sometimes we may want data from the parent azuredevops.Event struct, so we handle type checking here.
// Requires Resource Version 2.0 of the Pull Request Commented On webhook payload.
func (e *EventsController) HandleAzureDevopsPullRequestCommentedEvent(w http.ResponseWriter, event *azuredevops.Event, azuredevopsReqID string) {
resource, ok := event.Resource.(*azuredevops.GitPullRequestWithComment)
if !ok || event.PayloadType != azuredevops.PullRequestCommentedEvent {
e.respond(w, logging.Error, http.StatusBadRequest, "Event.Resource is nil or received bad event type %v; %s", event.Resource, azuredevopsReqID)
return
}
if resource.Comment == nil {
e.respond(w, logging.Debug, http.StatusOK, "Ignoring comment event since no comment is linked to payload; %s", azuredevopsReqID)
return
}
strippedComment := bluemonday.StrictPolicy().SanitizeBytes([]byte(*resource.Comment.Content))
if resource.PullRequest == nil {
e.respond(w, logging.Debug, http.StatusOK, "Ignoring comment event since no pull request is linked to payload; %s", azuredevopsReqID)
return
}
createdBy := resource.PullRequest.GetCreatedBy()
user := models.User{Username: createdBy.GetUniqueName()}
baseRepo, err := e.Parser.ParseAzureDevopsRepo(resource.PullRequest.GetRepository())
if err != nil {
e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing pull request repository field: %s; %s", err, azuredevopsReqID)
return
}
e.handleCommentEvent(w, baseRepo, nil, nil, user, resource.PullRequest.GetPullRequestID(), string(strippedComment), models.AzureDevops)
}
// HandleAzureDevopsPullRequestEvent 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) HandleAzureDevopsPullRequestEvent(w http.ResponseWriter, event *azuredevops.Event, azuredevopsReqID string) {
prText := event.Message.GetText()
ignoreEvents := []string{
"changed the reviewer list",
"approved pull request",
"has approved and left suggestions",
"is waiting for the author",
"rejected pull request",
"voted on pull request",
}
for _, s := range ignoreEvents {
if strings.Contains(prText, s) {
msg := fmt.Sprintf("pull request updated event is not a supported type [%s]", s)
e.respond(w, logging.Debug, http.StatusOK, "%s: %s", msg, azuredevopsReqID)
return
}
}
pull, pullEventType, baseRepo, headRepo, user, err := e.Parser.ParseAzureDevopsPullEvent(*event)
if err != nil {
e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing pull data: %s %s", err, azuredevopsReqID)
return
}
e.Logger.Info("identified event as type %q", pullEventType.String())
e.handlePullRequestEvent(w, baseRepo, headRepo, pull, user, pullEventType)
}
// supportsHost returns true if h is in e.SupportedVCSHosts and false otherwise.
func (e *EventsController) supportsHost(h models.VCSHostType) bool {
for _, supported := range e.SupportedVCSHosts {
if h == supported {
return true
}
}
return false
}
func (e *EventsController) respond(w http.ResponseWriter, lvl logging.LogLevel, code int, format string, args ...interface{}) {
response := fmt.Sprintf(format, args...)
e.Logger.Log(lvl, response)
w.WriteHeader(code)
fmt.Fprintln(w, response)
}
// commentNotAllowlisted comments on the pull request that the repo is not
// allowlisted unless allowlist error comments are disabled.
func (e *EventsController) commentNotAllowlisted(baseRepo models.Repo, pullNum int) {
if e.SilenceAllowlistErrors {
return
}
errMsg := "```\nError: This repo is not allowlisted for Atlantis.\n```"
if err := e.VCSClient.CreateComment(baseRepo, pullNum, errMsg, ""); err != nil {
e.Logger.Err("unable to comment on pull request: %s", err)
}
}