diff --git a/Gopkg.toml b/Gopkg.toml index c29d48de7..db303bb4a 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -98,7 +98,7 @@ [[constraint]] branch = "master" - name = "github.com/benmatselby/go-azuredevops/azuredevops" + name = "github.com/benmatselby/go-azuredevops" [[constraint]] name = "github.com/go-test/deep" diff --git a/cmd/server.go b/cmd/server.go index 0b6c85f2a..1bb136a32 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -75,7 +75,7 @@ const ( TFETokenFlag = "tfe-token" // Flag defaults. - DefaultADHostname = "linkconsulting.visualstudio.com" + DefaultADHostname = "visualstudio.com" DefaultCheckoutStrategy = "branch" DefaultBitbucketBaseURL = bitbucketcloud.BaseURL DefaultDataDir = "~/.atlantis" diff --git a/cmd/server_test.go b/cmd/server_test.go index 1a3ca064d..8a30e4921 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -380,8 +380,8 @@ func TestExecute_Defaults(t *testing.T) { Equals(t, "bitbucket-token", passedConfig.BitbucketToken) Equals(t, "bitbucket-user", passedConfig.BitbucketUser) Equals(t, "https://dev.azure.org", passedConfig.ADBaseURL) - Equals(t, "azuredevops-token", passedConfig.AzuredevopsToken) - Equals(t, "azuredevops-user", passedConfig.AzuredevopsUser) + Equals(t, "azuredevops-token", passedConfig.AzureDevopsToken) + Equals(t, "azuredevops-user", passedConfig.AzureDevopsUser) Equals(t, "", passedConfig.BitbucketWebhookSecret) Equals(t, "info", passedConfig.LogLevel) Equals(t, 4141, passedConfig.Port) diff --git a/server/events/comment_parser.go b/server/events/comment_parser.go index f7b1223d2..8e81ce531 100644 --- a/server/events/comment_parser.go +++ b/server/events/comment_parser.go @@ -65,9 +65,10 @@ type CommentBuilder interface { // CommentParser implements CommentParsing type CommentParser struct { - GithubUser string - GitlabUser string - BitbucketUser string + GithubUser string + GitlabUser string + BitbucketUser string + AzureDevopsUser string } // CommentParseResult describes the result of parsing a comment as a command. @@ -125,6 +126,8 @@ func (e *CommentParser) Parse(comment string, vcsHost models.VCSHostType) Commen vcsUser = e.GitlabUser case models.BitbucketCloud, models.BitbucketServer: vcsUser = e.BitbucketUser + case models.AzureDevops: + vcsUser = e.AzureDevopsUser } executableNames := []string{"run", atlantisExecutable, "@" + vcsUser} if !e.stringInSlice(args[0], executableNames) { diff --git a/server/events/comment_parser_test.go b/server/events/comment_parser_test.go index 460b3c2dd..3012fcd6e 100644 --- a/server/events/comment_parser_test.go +++ b/server/events/comment_parser_test.go @@ -644,9 +644,10 @@ func TestBuildPlanApplyComment(t *testing.T) { func TestParse_VCSUsername(t *testing.T) { cp := events.CommentParser{ - GithubUser: "gh", - GitlabUser: "gl", - BitbucketUser: "bb", + GithubUser: "gh", + GitlabUser: "gl", + BitbucketUser: "bb", + AzureDevopsUser: "ad", } cases := []struct { vcs models.VCSHostType @@ -668,6 +669,10 @@ func TestParse_VCSUsername(t *testing.T) { vcs: models.BitbucketCloud, user: "bb", }, + { + vcs: models.AzureDevops, + user: "ad", + }, } for _, c := range cases { diff --git a/server/events/event_parser.go b/server/events/event_parser.go index cc3492924..bec50fb3a 100644 --- a/server/events/event_parser.go +++ b/server/events/event_parser.go @@ -245,6 +245,8 @@ type EventParser struct { BitbucketUser string BitbucketToken string BitbucketServerURL string + AzureDevopsUser string + AzureDevopsToken string } // GetBitbucketCloudPullEventType returns the type of the pull request diff --git a/server/events/event_parser_test.go b/server/events/event_parser_test.go index 9d8ae71e0..8d39bd047 100644 --- a/server/events/event_parser_test.go +++ b/server/events/event_parser_test.go @@ -38,6 +38,8 @@ var parser = events.EventParser{ BitbucketUser: "bitbucket-user", BitbucketToken: "bitbucket-token", BitbucketServerURL: "http://mycorp.com:7490", + AzureDevopsUser: "azuredevops-user", + AzureDevopsToken: "azuredevops-token", } func TestParseGithubRepo(t *testing.T) { diff --git a/server/events/models/models.go b/server/events/models/models.go index effd1f7a9..684d9df05 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -282,7 +282,6 @@ func (h VCSHostType) String() string { return "BitbucketCloud" case BitbucketServer: return "BitbucketServer" - } case AzureDevops: return "AzureDevops" } diff --git a/server/events/vcs/azuredevops/azuredevops.go b/server/events/vcs/azuredevops/azuredevops.go index 9f38e6f41..313007735 100644 --- a/server/events/vcs/azuredevops/azuredevops.go +++ b/server/events/vcs/azuredevops/azuredevops.go @@ -3,4 +3,4 @@ // APIs. package azuredevops -const BaseURL = "https://dev.azure.com" +const BaseURL = "https://visualstudio.com" diff --git a/server/events/vcs/azuredevops/client.go b/server/events/vcs/azuredevops/client.go index 138261574..9f25fad62 100644 --- a/server/events/vcs/azuredevops/client.go +++ b/server/events/vcs/azuredevops/client.go @@ -197,7 +197,7 @@ func (b *Client) prepRequest(method string, path string, body io.Reader) (*http. } // Add this header to disable CSRF checks. // See https://confluence.atlassian.com/cloudkb/xsrf-check-failed-when-calling-cloud-apis-826874382.html - req.Header.Add("X-Atlassian-Token", "no-check") + //req.Header.Add("X-Atlassian-Token", "no-check") return req, nil } diff --git a/server/events/vcs/azuredevops/client_test.go b/server/events/vcs/azuredevops/client_test.go index 265209ce6..d8c23ff43 100644 --- a/server/events/vcs/azuredevops/client_test.go +++ b/server/events/vcs/azuredevops/client_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/runatlantis/atlantis/server/events/models" + "github.com/runatlantis/atlantis/server/events/vcs/azuredevops" . "github.com/runatlantis/atlantis/testing" ) @@ -28,7 +29,7 @@ func TestClient_GetModifiedFilesPagination(t *testing.T) { "type": "commit_file", "links": { "self": { - "href": "https://dev.azure.com/2.0/repositories/bitbucket/geordi/src/e1749643d655d7c7014001a6c0f58abaf42ad850/setup.py" + "href": "https://visualstudio.com/org/2.0/repositories/bitbucket/geordi/src/e1749643d655d7c7014001a6c0f58abaf42ad850/setup.py" } } }, @@ -37,7 +38,7 @@ func TestClient_GetModifiedFilesPagination(t *testing.T) { "type": "commit_file", "links": { "self": { - "href": "https://dev.azure.com/2.0/repositories/bitbucket/geordi/src/d222fa235229c55dad20b190b0b571adf737d5a6/setup.py" + "href": "https://visualstudio.com/org/2.0/repositories/bitbucket/geordi/src/d222fa235229c55dad20b190b0b571adf737d5a6/setup.py" } } } @@ -69,7 +70,7 @@ func TestClient_GetModifiedFilesPagination(t *testing.T) { defer testServer.Close() serverURL = testServer.URL - client := bitbucketcloud.NewClient(http.DefaultClient, "user", "pass", "runatlantis.io") + client := azuredevops.NewClient(http.DefaultClient, "user", "pass", "runatlantis.io") client.BaseURL = testServer.URL files, err := client.GetModifiedFiles(models.Repo{ @@ -79,8 +80,8 @@ func TestClient_GetModifiedFilesPagination(t *testing.T) { CloneURL: "", SanitizedCloneURL: "", VCSHost: models.VCSHost{ - Type: models.BitbucketCloud, - Hostname: "dev.azure.com", + Type: models.AzureDevops, + Hostname: "visualstudio.com", }, }, models.PullRequest{ Num: 1, @@ -105,7 +106,7 @@ func TestClient_GetModifiedFilesOldNil(t *testing.T) { "type": "commit_file", "links": { "self": { - "href": "https://dev.azure.com/2.0/repositories/lkysow/atlantis-example/src/1ed8205eec00dab4f1c0a8c486a4492c98c51f8e/main.tf" + "href": "https://visualstudio.com/2.0/repositories/lkysow/atlantis-example/src/1ed8205eec00dab4f1c0a8c486a4492c98c51f8e/main.tf" } } }, @@ -130,7 +131,7 @@ func TestClient_GetModifiedFilesOldNil(t *testing.T) { })) defer testServer.Close() - client := bitbucketcloud.NewClient(http.DefaultClient, "user", "pass", "runatlantis.io") + client := azuredevops.NewClient(http.DefaultClient, "user", "pass", "runatlantis.io") client.BaseURL = testServer.URL files, err := client.GetModifiedFiles(models.Repo{ @@ -140,8 +141,8 @@ func TestClient_GetModifiedFilesOldNil(t *testing.T) { CloneURL: "", SanitizedCloneURL: "", VCSHost: models.VCSHost{ - Type: models.BitbucketCloud, - Hostname: "dev.azure.com", + Type: models.AzureDevops, + Hostname: "visualstudio.com", }, }, models.PullRequest{ Num: 1, @@ -196,10 +197,10 @@ func TestClient_PullIsApproved(t *testing.T) { })) defer testServer.Close() - client := bitbucketcloud.NewClient(http.DefaultClient, "user", "pass", "runatlantis.io") + client := azuredevops.NewClient(http.DefaultClient, "user", "pass", "runatlantis.io") client.BaseURL = testServer.URL - repo, err := models.NewRepo(models.BitbucketServer, "owner/repo", "https://dev.azure.com/owner/repo.git", "user", "token") + repo, err := models.NewRepo(models.AzureDevops, "owner/repo", "https://visualstudio.com/owner/repo.git", "user", "token") Ok(t, err) approved, err := client.PullIsApproved(repo, models.PullRequest{ Num: 1, diff --git a/server/events/vcs/azuredevops/models.go b/server/events/vcs/azuredevops/models.go index fa99a95a2..25b7859c4 100644 --- a/server/events/vcs/azuredevops/models.go +++ b/server/events/vcs/azuredevops/models.go @@ -1,11 +1,11 @@ package azuredevops const ( - PullCreatedHeader = "pullrequest:created" - PullUpdatedHeader = "pullrequest:updated" - PullFulfilledHeader = "pullrequest:fulfilled" + PullCreatedHeader = "git.pullrequest.created" + PullUpdatedHeader = "git.pullrequest.updated" + PullFulfilledHeader = "git.pullrequest.merged" PullRejectedHeader = "pullrequest:rejected" - PullCommentCreatedHeader = "pullrequest:comment_created" + PullCommentCreatedHeader = "workitem.commented" ) type CommentEvent struct { diff --git a/server/events/vcs/proxy.go b/server/events/vcs/proxy.go index c2824846c..976eb3e85 100644 --- a/server/events/vcs/proxy.go +++ b/server/events/vcs/proxy.go @@ -47,7 +47,7 @@ func NewClientProxy(githubClient Client, gitlabClient Client, bitbucketCloudClie models.Gitlab: gitlabClient, models.BitbucketCloud: bitbucketCloudClient, models.BitbucketServer: bitbucketServerClient, - models.AzureDevops: azuredevopsClient + models.AzureDevops: azuredevopsClient, }, } } diff --git a/server/events_controller.go b/server/events_controller.go index 46160063a..8f196d772 100644 --- a/server/events_controller.go +++ b/server/events_controller.go @@ -31,6 +31,7 @@ import ( const githubHeader = "X-Github-Event" const gitlabHeader = "X-Gitlab-Event" +const azuredevopsHeader = "X-AzureDevops-Event" // bitbucketEventTypeHeader is the same in both cloud and server. const bitbucketEventTypeHeader = "X-Event-Key" @@ -69,6 +70,10 @@ type EventsController struct { // UI that identifies this call as coming from Bitbucket. If empty, no // request validation is done. BitbucketWebhookSecret []byte + // AzureDevopsWebhookSecret is the secret added to this webhook via the Azure + // Devops UI that identifies this call as coming from Bitbucket. If empty, no + // request validation is done. + AzureDevopsWebhookSecret []byte } // Post handles POST webhook requests. @@ -108,6 +113,14 @@ func (e *EventsController) Post(w http.ResponseWriter, r *http.Request) { 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") @@ -195,6 +208,44 @@ func (e *EventsController) handleBitbucketServerPost(w http.ResponseWriter, r *h } } +func (e *EventsController) handleAzureDevopsPost(w http.ResponseWriter, r *http.Request) {} + +/*func (e *EventsController) handleAzureDevopsPost(w http.ResponseWriter, r *http.Request) { + eventType := r.Header.Get(azuredevopsEventTypeHeader) + reqID := r.Header.Get(azuredevopsRequestIDHeader) + sig := r.Header.Get(azuredevopsSignatureHeader) + 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, azuredevopsRequestIDHeader, reqID) + return + } + if eventType == azuredevops.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, azuredevopsRequestIDHeader, reqID) + return + } + if len(e.AzureDevopsWebhookSecret) > 0 { + if err := azuredevops.ValidateSignature(body, sig, e.AzureDevopsWebhookSecret); err != nil { + e.respond(w, logging.Warn, http.StatusBadRequest, errors.Wrap(err, "request did not pass validation").Error()) + return + } + } + switch eventType { + case azuredevops.PullCreatedHeader, azuredevops.PullMergedHeader, azuredevops.PullDeclinedHeader, azuredevops.PullDeletedHeader: + e.Logger.Debug("handling as pull request state changed event") + e.handleBitbucketServerPullRequestEvent(w, eventType, body, reqID) + return + case azuredevops.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) + } +}*/ + // 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) { diff --git a/server/server.go b/server/server.go index 740690af2..beb5feca1 100644 --- a/server/server.go +++ b/server/server.go @@ -235,11 +235,14 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { BitbucketUser: userConfig.BitbucketUser, BitbucketToken: userConfig.BitbucketToken, BitbucketServerURL: userConfig.BitbucketBaseURL, + AzureDevopsUser: userConfig.AzureDevopsUser, + AzureDevopsToken: userConfig.AzureDevopsToken, } commentParser := &events.CommentParser{ - GithubUser: userConfig.GithubUser, - GitlabUser: userConfig.GitlabUser, - BitbucketUser: userConfig.BitbucketUser, + GithubUser: userConfig.GithubUser, + GitlabUser: userConfig.GitlabUser, + BitbucketUser: userConfig.BitbucketUser, + AzureDevopsUser: userConfig.AzureDevopsUser, } defaultTfVersion := terraformClient.DefaultVersion() pendingPlanFinder := &events.DefaultPendingPlanFinder{} @@ -324,6 +327,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { SupportedVCSHosts: supportedVCSHosts, VCSClient: vcsClient, BitbucketWebhookSecret: []byte(userConfig.BitbucketWebhookSecret), + AzureDevopsWebhookSecret: []byte(userConfig.AzureDevopsWebhookSecret), } return &Server{ AtlantisVersion: config.AtlantisVersion, diff --git a/server/user_config.go b/server/user_config.go index e401ee6c2..beb61ded9 100644 --- a/server/user_config.go +++ b/server/user_config.go @@ -6,29 +6,34 @@ import "github.com/runatlantis/atlantis/server/logging" // The mapstructure tags correspond to flags in cmd/server.go and are used when // the config is parsed from a YAML file. type UserConfig struct { - AllowForkPRs bool `mapstructure:"allow-fork-prs"` - AllowRepoConfig bool `mapstructure:"allow-repo-config"` - AtlantisURL string `mapstructure:"atlantis-url"` - Automerge bool `mapstructure:"automerge"` - BitbucketBaseURL string `mapstructure:"bitbucket-base-url"` - BitbucketToken string `mapstructure:"bitbucket-token"` - BitbucketUser string `mapstructure:"bitbucket-user"` - BitbucketWebhookSecret string `mapstructure:"bitbucket-webhook-secret"` - CheckoutStrategy string `mapstructure:"checkout-strategy"` - DataDir string `mapstructure:"data-dir"` - GithubHostname string `mapstructure:"gh-hostname"` - GithubToken string `mapstructure:"gh-token"` - GithubUser string `mapstructure:"gh-user"` - GithubWebhookSecret string `mapstructure:"gh-webhook-secret"` - GitlabHostname string `mapstructure:"gitlab-hostname"` - GitlabToken string `mapstructure:"gitlab-token"` - GitlabUser string `mapstructure:"gitlab-user"` - GitlabWebhookSecret string `mapstructure:"gitlab-webhook-secret"` - LogLevel string `mapstructure:"log-level"` - Port int `mapstructure:"port"` - RepoConfig string `mapstructure:"repo-config"` - RepoConfigJSON string `mapstructure:"repo-config-json"` - RepoWhitelist string `mapstructure:"repo-whitelist"` + AllowForkPRs bool `mapstructure:"allow-fork-prs"` + AllowRepoConfig bool `mapstructure:"allow-repo-config"` + AtlantisURL string `mapstructure:"atlantis-url"` + Automerge bool `mapstructure:"automerge"` + AzureDevopsBaseURL string `mapstructure:"azuredevops-base-url"` + AzureDevopsHostname string `mapstructure:"azuredevops-hostname"` + AzureDevopsToken string `mapstructure:"azuredevops-token"` + AzureDevopsUser string `mapstructure:"azuredevops-user"` + AzureDevopsWebhookSecret string `mapstructure:"azuredevops-webhook-secret"` + BitbucketBaseURL string `mapstructure:"bitbucket-base-url"` + BitbucketToken string `mapstructure:"bitbucket-token"` + BitbucketUser string `mapstructure:"bitbucket-user"` + BitbucketWebhookSecret string `mapstructure:"bitbucket-webhook-secret"` + CheckoutStrategy string `mapstructure:"checkout-strategy"` + DataDir string `mapstructure:"data-dir"` + GithubHostname string `mapstructure:"gh-hostname"` + GithubToken string `mapstructure:"gh-token"` + GithubUser string `mapstructure:"gh-user"` + GithubWebhookSecret string `mapstructure:"gh-webhook-secret"` + GitlabHostname string `mapstructure:"gitlab-hostname"` + GitlabToken string `mapstructure:"gitlab-token"` + GitlabUser string `mapstructure:"gitlab-user"` + GitlabWebhookSecret string `mapstructure:"gitlab-webhook-secret"` + LogLevel string `mapstructure:"log-level"` + Port int `mapstructure:"port"` + RepoConfig string `mapstructure:"repo-config"` + RepoConfigJSON string `mapstructure:"repo-config-json"` + RepoWhitelist string `mapstructure:"repo-whitelist"` // RequireApproval is whether to require pull request approval before // allowing terraform apply's to be run. RequireApproval bool `mapstructure:"require-approval"`