diff --git a/cmd/server.go b/cmd/server.go index 1bb136a32..c319de1e4 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -23,7 +23,6 @@ import ( homedir "github.com/mitchellh/go-homedir" "github.com/pkg/errors" "github.com/runatlantis/atlantis/server" - //"github.com/runatlantis/atlantis/server/events/vcs/azuredevops" "github.com/runatlantis/atlantis/server/events/vcs/bitbucketcloud" "github.com/runatlantis/atlantis/server/events/yaml/valid" "github.com/runatlantis/atlantis/server/logging" @@ -37,7 +36,8 @@ import ( // 3. Add your flag's description etc. to the stringFlags, intFlags, or boolFlags slices. const ( // Flag names. - ADHostnameFlag = "azuredevops-hostname" + ADOrgFlag = "azuredevops-org" + ADProjectFlag = "azuredevops-project" ADTokenFlag = "azuredevops-token" ADUserFlag = "azuredevops-user" ADWebhookSecretFlag = "azuredevops-webhook-secret" // nolint: gosec @@ -75,7 +75,7 @@ const ( TFETokenFlag = "tfe-token" // Flag defaults. - DefaultADHostname = "visualstudio.com" + DefaultADOrg = "" DefaultCheckoutStrategy = "branch" DefaultBitbucketBaseURL = bitbucketcloud.BaseURL DefaultDataDir = "~/.atlantis" @@ -86,9 +86,13 @@ const ( ) var stringFlags = map[string]stringFlag{ - ADHostnameFlag: { - description: "Hostname of your Azure Devops installation.", - defaultValue: DefaultADHostname, + ADOrgFlag: { + description: "Organization name of your Azure Devops instance.", + defaultValue: "", + }, + ADProjectFlag: { + description: "Project name in your Azure Devops insance.", + defaultValue: "", }, ADUserFlag: { description: "Azure Devops username of API user.", @@ -420,8 +424,8 @@ func (s *ServerCmd) setDefaults(c *server.UserConfig) { if c.DataDir == "" { c.DataDir = DefaultDataDir } - if c.AzureDevopsHostname == "" { - c.AzureDevopsHostname = DefaultADHostname + if c.AzureDevopsOrg == "" { + c.AzureDevopsOrg = DefaultADOrg } if c.GithubHostname == "" { c.GithubHostname = DefaultGHHostname @@ -458,10 +462,11 @@ func (s *ServerCmd) validate(userConfig server.UserConfig) error { // 1. github user and token set // 2. gitlab user and token set // 3. bitbucket user and token set - // 4. azuredevops user and token set + // 4. azuredevops user, token, org and project set // 5. any combination of the above - vcsErr := fmt.Errorf("--%s/--%s or --%s/--%s or --%s/--%s or --%s/--%s must be set", GHUserFlag, GHTokenFlag, GitlabUserFlag, GitlabTokenFlag, BitbucketUserFlag, BitbucketTokenFlag, ADUserFlag, ADTokenFlag) - if ((userConfig.GithubUser == "") != (userConfig.GithubToken == "")) || ((userConfig.GitlabUser == "") != (userConfig.GitlabToken == "")) || ((userConfig.BitbucketUser == "") != (userConfig.BitbucketToken == "")) || ((userConfig.AzureDevopsUser == "") != (userConfig.AzureDevopsToken == "")) { + vcsErr := fmt.Errorf("--%s/--%s or --%s/--%s or --%s/--%s or --%s/--%s/--%s/--%s must be set", GHUserFlag, GHTokenFlag, GitlabUserFlag, GitlabTokenFlag, BitbucketUserFlag, BitbucketTokenFlag, ADUserFlag, ADTokenFlag, ADOrgFlag, ADProjectFlag) + if ((userConfig.GithubUser == "") != (userConfig.GithubToken == "")) || ((userConfig.GitlabUser == "") != (userConfig.GitlabToken == "")) || ((userConfig.BitbucketUser == "") != (userConfig.BitbucketToken == "")) || ((userConfig.AzureDevopsUser == "") != (userConfig.AzureDevopsToken == "") || + (userConfig.AzureDevopsOrg == "") != (userConfig.AzureDevopsProject == "")) { return vcsErr } // At this point, we know that there can't be a single user/token without @@ -481,10 +486,6 @@ func (s *ServerCmd) validate(userConfig server.UserConfig) error { return fmt.Errorf("--%s cannot be specified for Bitbucket Cloud because it is not supported by Bitbucket", BitbucketWebhookSecretFlag) } - if userConfig.AzureDevopsUser == DefaultADHostname && userConfig.AzureDevopsToken != "" { - return fmt.Errorf("--%s cannot be specified for Azure Devops because it is not supported by Microsoft", BitbucketWebhookSecretFlag) - } - parsed, err := url.Parse(userConfig.BitbucketBaseURL) if err != nil { return fmt.Errorf("error parsing --%s flag value %q: %s", BitbucketWebhookSecretFlag, userConfig.BitbucketBaseURL, err) @@ -556,8 +557,11 @@ func (s *ServerCmd) securityWarnings(userConfig *server.UserConfig) { if userConfig.BitbucketUser != "" && userConfig.BitbucketBaseURL == DefaultBitbucketBaseURL && !s.SilenceOutput { s.Logger.Warn("Bitbucket Cloud does not support webhook secrets. This could allow attackers to spoof requests from Bitbucket. Ensure you are whitelisting Bitbucket IPs") } - if userConfig.AzureDevopsUser != "" && userConfig.AzureDevopsHostname == DefaultADHostname && !s.SilenceOutput { - s.Logger.Warn("Azure Devops does not support webhook secrets without org identifier. This could allow attackers to spoof requests from Azure. Ensure you are whitelisting Azure Devops IPs") + if userConfig.AzureDevopsUser != "" && userConfig.AzureDevopsWebhookSecret == "" && !s.SilenceOutput { + s.Logger.Warn("No Azure Devops webhook secret set. This could allow attackers to spoof requests from Azure Devops.") + } + if userConfig.AzureDevopsUser != "" && userConfig.AzureDevopsWebhookSecret == "" && !s.SilenceOutput { + s.Logger.Warn("No Azure Devops webhook secret set. This could allow attackers to spoof requests from Azure Devops.") } } diff --git a/cmd/server_test.go b/cmd/server_test.go index 8a30e4921..ce1890eff 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -379,7 +379,8 @@ func TestExecute_Defaults(t *testing.T) { Equals(t, "https://api.bitbucket.org", passedConfig.BitbucketBaseURL) Equals(t, "bitbucket-token", passedConfig.BitbucketToken) Equals(t, "bitbucket-user", passedConfig.BitbucketUser) - Equals(t, "https://dev.azure.org", passedConfig.ADBaseURL) + Equals(t, "azuredevops-org", passedConfig.AzureDevopsOrg) + Equals(t, "azuredevops-project", passedConfig.AzureDevopsProject) Equals(t, "azuredevops-token", passedConfig.AzureDevopsToken) Equals(t, "azuredevops-user", passedConfig.AzureDevopsUser) Equals(t, "", passedConfig.BitbucketWebhookSecret) diff --git a/server/events/vcs/azuredevops/azuredevops.go b/server/events/vcs/azuredevops/azuredevops.go deleted file mode 100644 index 313007735..000000000 --- a/server/events/vcs/azuredevops/azuredevops.go +++ /dev/null @@ -1,6 +0,0 @@ -// Package bitbucketcloud holds code for Bitbucket Cloud aka (bitbucket.org). -// It is separate from bitbucketserver because Bitbucket Server has different -// APIs. -package azuredevops - -const BaseURL = "https://visualstudio.com" diff --git a/server/events/vcs/azuredevops/client.go b/server/events/vcs/azuredevops/client.go deleted file mode 100644 index 9f25fad62..000000000 --- a/server/events/vcs/azuredevops/client.go +++ /dev/null @@ -1,225 +0,0 @@ -package azuredevops - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "net/http" - - "github.com/pkg/errors" - "github.com/runatlantis/atlantis/server/events/models" - "gopkg.in/go-playground/validator.v9" -) - -type Client struct { - HTTPClient *http.Client - Username string - Password string - BaseURL string - AtlantisURL string -} - -// NewClient builds a Azure Devops cloud client. atlantisURL is the -// URL for Atlantis that will be linked to from the build status icons. This -// linking is annoying because we don't have anywhere good to link but a URL is -// required. -func NewClient(httpClient *http.Client, username string, password string, atlantisURL string) *Client { - if httpClient == nil { - httpClient = http.DefaultClient - } - return &Client{ - HTTPClient: httpClient, - Username: username, - Password: password, - BaseURL: BaseURL, - AtlantisURL: atlantisURL, - } -} - -// GetModifiedFiles returns the names of files that were modified in the merge request. -// The names include the path to the file from the repo root, ex. parent/child/file.txt. -func (b *Client) GetModifiedFiles(repo models.Repo, pull models.PullRequest) ([]string, error) { - var files []string - - nextPageURL := fmt.Sprintf("%s/2.0/repositories/%s/pullrequests/%d/diffstat", b.BaseURL, repo.FullName, pull.Num) - // We'll only loop 1000 times as a safety measure. - maxLoops := 1000 - for i := 0; i < maxLoops; i++ { - resp, err := b.makeRequest("GET", nextPageURL, nil) - if err != nil { - return nil, err - } - var diffStat DiffStat - if err := json.Unmarshal(resp, &diffStat); err != nil { - return nil, errors.Wrapf(err, "Could not parse response %q", string(resp)) - } - if err := validator.New().Struct(diffStat); err != nil { - return nil, errors.Wrapf(err, "API response %q was missing fields", string(resp)) - } - for _, v := range diffStat.Values { - if v.Old != nil { - files = append(files, *v.Old.Path) - } - if v.New != nil { - files = append(files, *v.New.Path) - } - } - if diffStat.Next == nil || *diffStat.Next == "" { - break - } - nextPageURL = *diffStat.Next - } - - // Now ensure all files are unique. - hash := make(map[string]bool) - var unique []string - for _, f := range files { - if !hash[f] { - unique = append(unique, f) - hash[f] = true - } - } - return unique, nil -} - -// CreateComment creates a comment on the merge request. -func (b *Client) CreateComment(repo models.Repo, pullNum int, comment string) error { - // NOTE: I tried to find the maximum size of a comment for Azure Devops.org but - // I got up to 200k chars without issue so for now I'm not going to bother - // to detect this. - bodyBytes, err := json.Marshal(map[string]map[string]string{"content": { - "raw": comment, - }}) - if err != nil { - return errors.Wrap(err, "json encoding") - } - path := fmt.Sprintf("%s/2.0/repositories/%s/pullrequests/%d/comments", b.BaseURL, repo.FullName, pullNum) - _, err = b.makeRequest("POST", path, bytes.NewBuffer(bodyBytes)) - return err -} - -// PullIsApproved returns true if the merge request was approved. -func (b *Client) PullIsApproved(repo models.Repo, pull models.PullRequest) (bool, error) { - path := fmt.Sprintf("%s/2.0/repositories/%s/pullrequests/%d", b.BaseURL, repo.FullName, pull.Num) - resp, err := b.makeRequest("GET", path, nil) - if err != nil { - return false, err - } - var pullResp PullRequest - if err := json.Unmarshal(resp, &pullResp); err != nil { - return false, errors.Wrapf(err, "Could not parse response %q", string(resp)) - } - if err := validator.New().Struct(pullResp); err != nil { - return false, errors.Wrapf(err, "API response %q was missing fields", string(resp)) - } - for _, participant := range pullResp.Participants { - // Azure Devops allows the author to approve their own pull request. This - // defeats the purpose of approvals so we don't count that approval. - if *participant.Approved && *participant.User.Username != pull.Author { - return true, nil - } - } - return false, nil -} - -// PullIsMergeable returns true if the merge request has no conflicts and can be merged. -func (b *Client) PullIsMergeable(repo models.Repo, pull models.PullRequest) (bool, error) { - // NOTE: The 1.0 API is deprecated, but the 2.0 API does not provide this endpoint. - path := fmt.Sprintf("%s/1.0/repositories/%s/pullrequests/%d/conflict-status", b.BaseURL, repo.FullName, pull.Num) - resp, err := b.makeRequest("GET", path, nil) - if err != nil { - return false, err - } - var conflictStatus ConflictStatus - if err := json.Unmarshal(resp, &conflictStatus); err != nil { - return false, errors.Wrapf(err, "Could not parse response %q", string(resp)) - } - if err := validator.New().Struct(conflictStatus); err != nil { - return false, errors.Wrapf(err, "API response %q was missing fields", string(resp)) - } - if !*conflictStatus.MergeImpossible && !*conflictStatus.IsConflicted { - return true, nil - } - return false, nil -} - -// UpdateStatus updates the status of a commit. -func (b *Client) UpdateStatus(repo models.Repo, pull models.PullRequest, status models.CommitStatus, src string, description string, url string) error { - bbState := "FAILED" - switch status { - case models.PendingCommitStatus: - bbState = "INPROGRESS" - case models.SuccessCommitStatus: - bbState = "SUCCESSFUL" - case models.FailedCommitStatus: - bbState = "FAILED" - } - - // URL is a required field for Azure Devops statuses. We default to the - // Atlantis server's URL. - if url == "" { - url = b.AtlantisURL - } - - bodyBytes, err := json.Marshal(map[string]string{ - "key": src, - "url": url, - "state": bbState, - "description": description, - }) - - path := fmt.Sprintf("%s/2.0/repositories/%s/commit/%s/statuses/build", b.BaseURL, repo.FullName, pull.HeadCommit) - if err != nil { - return errors.Wrap(err, "json encoding") - } - _, err = b.makeRequest("POST", path, bytes.NewBuffer(bodyBytes)) - return err -} - -// MergePull merges the pull request. -func (b *Client) MergePull(pull models.PullRequest) error { - path := fmt.Sprintf("%s/2.0/repositories/%s/pullrequests/%d/merge", b.BaseURL, pull.BaseRepo.FullName, pull.Num) - _, err := b.makeRequest("POST", path, nil) - return err -} - -// prepRequest adds auth and necessary headers. -func (b *Client) prepRequest(method string, path string, body io.Reader) (*http.Request, error) { - req, err := http.NewRequest(method, path, body) - if err != nil { - return nil, err - } - req.SetBasicAuth(b.Username, b.Password) - if body != nil { - req.Header.Add("Content-Type", "application/json") - } - // 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") - return req, nil -} - -func (b *Client) makeRequest(method string, path string, reqBody io.Reader) ([]byte, error) { - req, err := b.prepRequest(method, path, reqBody) - if err != nil { - return nil, errors.Wrap(err, "constructing request") - } - resp, err := b.HTTPClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() // nolint: errcheck - requestStr := fmt.Sprintf("%s %s", method, path) - - if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { - respBody, _ := ioutil.ReadAll(resp.Body) - return nil, fmt.Errorf("making request %q unexpected status code: %d, body: %s", requestStr, resp.StatusCode, string(respBody)) - } - respBody, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, errors.Wrapf(err, "reading response from request %q", requestStr) - } - return respBody, nil -} diff --git a/server/events/vcs/azuredevops/client_test.go b/server/events/vcs/azuredevops/client_test.go deleted file mode 100644 index d8c23ff43..000000000 --- a/server/events/vcs/azuredevops/client_test.go +++ /dev/null @@ -1,215 +0,0 @@ -package azuredevops_test - -import ( - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "path/filepath" - "testing" - - "github.com/runatlantis/atlantis/server/events/models" - "github.com/runatlantis/atlantis/server/events/vcs/azuredevops" - . "github.com/runatlantis/atlantis/testing" -) - -// Should follow pagination properly. -func TestClient_GetModifiedFilesPagination(t *testing.T) { - respTemplate := ` -{ - "pagelen": 1, - "values": [ - { - "type": "diffstat", - "status": "modified", - "lines_removed": 1, - "lines_added": 2, - "old": { - "path": "%s", - "type": "commit_file", - "links": { - "self": { - "href": "https://visualstudio.com/org/2.0/repositories/bitbucket/geordi/src/e1749643d655d7c7014001a6c0f58abaf42ad850/setup.py" - } - } - }, - "new": { - "path": "%s", - "type": "commit_file", - "links": { - "self": { - "href": "https://visualstudio.com/org/2.0/repositories/bitbucket/geordi/src/d222fa235229c55dad20b190b0b571adf737d5a6/setup.py" - } - } - } - } - ], - "page": 1, - "size": 1 -` - firstResp := fmt.Sprintf(respTemplate, "file1.txt", "file2.txt") - secondResp := fmt.Sprintf(respTemplate, "file2.txt", "file3.txt") - var serverURL string - - testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.RequestURI { - // The first request should hit this URL. - case "/2.0/repositories/owner/repo/pullrequests/1/diffstat": - resp := firstResp + fmt.Sprintf(`,"next": "%s/2.0/repositories/owner/repo/pullrequests/1/diffstat?page=2"}`, serverURL) - w.Write([]byte(resp)) // nolint: errcheck - return - // The second should hit this URL. - case "/2.0/repositories/owner/repo/pullrequests/1/diffstat?page=2": - w.Write([]byte(secondResp + "}")) // nolint: errcheck - default: - t.Errorf("got unexpected request at %q", r.RequestURI) - http.Error(w, "not found", http.StatusNotFound) - return - } - })) - defer testServer.Close() - - serverURL = testServer.URL - client := azuredevops.NewClient(http.DefaultClient, "user", "pass", "runatlantis.io") - client.BaseURL = testServer.URL - - files, err := client.GetModifiedFiles(models.Repo{ - FullName: "owner/repo", - Owner: "owner", - Name: "repo", - CloneURL: "", - SanitizedCloneURL: "", - VCSHost: models.VCSHost{ - Type: models.AzureDevops, - Hostname: "visualstudio.com", - }, - }, models.PullRequest{ - Num: 1, - }) - Ok(t, err) - Equals(t, []string{"file1.txt", "file2.txt", "file3.txt"}, files) -} - -// If the "old" key in the list of files is nil we shouldn't error. -func TestClient_GetModifiedFilesOldNil(t *testing.T) { - resp := ` -{ - "pagelen": 500, - "values": [ - { - "status": "added", - "old": null, - "lines_removed": 0, - "lines_added": 2, - "new": { - "path": "parent/child/file1.txt", - "type": "commit_file", - "links": { - "self": { - "href": "https://visualstudio.com/2.0/repositories/lkysow/atlantis-example/src/1ed8205eec00dab4f1c0a8c486a4492c98c51f8e/main.tf" - } - } - }, - "type": "diffstat" - } - ], - "page": 1, - "size": 1 -}` - - testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.RequestURI { - // The first request should hit this URL. - case "/2.0/repositories/owner/repo/pullrequests/1/diffstat": - w.Write([]byte(resp)) // nolint: errcheck - return - default: - t.Errorf("got unexpected request at %q", r.RequestURI) - http.Error(w, "not found", http.StatusNotFound) - return - } - })) - defer testServer.Close() - - client := azuredevops.NewClient(http.DefaultClient, "user", "pass", "runatlantis.io") - client.BaseURL = testServer.URL - - files, err := client.GetModifiedFiles(models.Repo{ - FullName: "owner/repo", - Owner: "owner", - Name: "repo", - CloneURL: "", - SanitizedCloneURL: "", - VCSHost: models.VCSHost{ - Type: models.AzureDevops, - Hostname: "visualstudio.com", - }, - }, models.PullRequest{ - Num: 1, - }) - Ok(t, err) - Equals(t, []string{"parent/child/file1.txt"}, files) -} - -func TestClient_PullIsApproved(t *testing.T) { - cases := []struct { - description string - testdata string - exp bool - }{ - { - "no approvers", - "pull-unapproved.json", - false, - }, - { - "approver is the author", - "pull-approved-by-author.json", - false, - }, - { - "single approver", - "pull-approved.json", - true, - }, - { - "two approvers one author", - "pull-approved-multiple.json", - true, - }, - } - - for _, c := range cases { - t.Run(c.description, func(t *testing.T) { - json, err := ioutil.ReadFile(filepath.Join("testdata", c.testdata)) - Ok(t, err) - testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.RequestURI { - // The first request should hit this URL. - case "/2.0/repositories/owner/repo/pullrequests/1": - w.Write(json) // nolint: errcheck - return - default: - t.Errorf("got unexpected request at %q", r.RequestURI) - http.Error(w, "not found", http.StatusNotFound) - return - } - })) - defer testServer.Close() - - client := azuredevops.NewClient(http.DefaultClient, "user", "pass", "runatlantis.io") - client.BaseURL = testServer.URL - - 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, - HeadBranch: "branch", - Author: "author", - BaseRepo: repo, - }) - Ok(t, err) - Equals(t, c.exp, approved) - }) - } -} diff --git a/server/events/vcs/azuredevops/models.go b/server/events/vcs/azuredevops/models.go deleted file mode 100644 index 25b7859c4..000000000 --- a/server/events/vcs/azuredevops/models.go +++ /dev/null @@ -1,88 +0,0 @@ -package azuredevops - -const ( - PullCreatedHeader = "git.pullrequest.created" - PullUpdatedHeader = "git.pullrequest.updated" - PullFulfilledHeader = "git.pullrequest.merged" - PullRejectedHeader = "pullrequest:rejected" - PullCommentCreatedHeader = "workitem.commented" -) - -type CommentEvent struct { - CommonEventData - Comment *Comment `json:"comment,omitempty" validate:"required"` -} - -type PullRequestEvent struct { - CommonEventData -} - -type CommonEventData struct { - Actor *Actor `json:"actor,omitempty" validate:"required"` - Repository *Repository `json:"repository,omitempty" validate:"required"` - PullRequest *PullRequest `json:"pullrequest,omitempty" validate:"required"` -} - -type DiffStat struct { - Values []DiffStatValue `json:"values,omitempty" validate:"required"` - Next *string `json:"next,omitempty"` -} -type DiffStatValue struct { - // Old is the old file, this can be null. - Old *DiffStatFile `json:"old,omitempty"` - // New is the new file, this can be null. - New *DiffStatFile `json:"new,omitempty"` -} -type DiffStatFile struct { - Path *string `json:"path,omitempty" validate:"required"` -} - -type Actor struct { - Nickname *string `json:"nickname,omitempty" validate:"required"` -} -type Repository struct { - FullName *string `json:"full_name,omitempty" validate:"required"` - Links Links `json:"links,omitempty" validate:"required"` -} -type PullRequest struct { - ID *int `json:"id,omitempty" validate:"required"` - Source *BranchMeta `json:"source,omitempty" validate:"required"` - Destination *BranchMeta `json:"destination,omitempty" validate:"required"` - Participants []Participant `json:"participants,omitempty" validate:"required"` - Links *Links `json:"links,omitempty" validate:"required"` - State *string `json:"state,omitempty" validate:"required"` -} -type Links struct { - HTML *Link `json:"html,omitempty" validate:"required"` -} -type Link struct { - HREF *string `json:"href,omitempty" validate:"required"` -} -type Participant struct { - Approved *bool `json:"approved,omitempty" validate:"required"` - User *struct { - Username *string `json:"username,omitempty" validate:"required"` - } `json:"user,omitempty" validate:"required"` -} -type BranchMeta struct { - Repository *Repository `json:"repository,omitempty" validate:"required"` - Commit *Commit `json:"commit,omitempty" validate:"required"` - Branch *Branch `json:"branch,omitempty" validate:"required"` -} -type Branch struct { - Name *string `json:"name,omitempty" validate:"required"` -} -type Commit struct { - Hash *string `json:"hash,omitempty" validate:"required"` -} -type Comment struct { - Content *CommentContent `json:"content,omitempty" validate:"required"` -} -type CommentContent struct { - Raw *string `json:"raw,omitempty" validate:"required"` -} - -type ConflictStatus struct { - MergeImpossible *bool `json:"mergeimpossible,omitempty" validate:"required"` - IsConflicted *bool `json:"isconflicted,omitempty" validate:"required"` -} diff --git a/server/events/vcs/azuredevops/testdata/pull-approved-by-author.json b/server/events/vcs/azuredevops/testdata/pull-approved-by-author.json deleted file mode 100644 index 87e1b13b5..000000000 --- a/server/events/vcs/azuredevops/testdata/pull-approved-by-author.json +++ /dev/null @@ -1,166 +0,0 @@ -{ - "type": "pullrequest", - "description": "main.tf edited online with Bitbucket", - "links": { - "decline": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/decline" - }, - "commits": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/commits" - }, - "self": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5" - }, - "comments": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/comments" - }, - "merge": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/merge" - }, - "html": { - "href": "https://bitbucket.org/lkysow/atlantis-example/pull-requests/5" - }, - "activity": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/activity" - }, - "diff": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/diff" - }, - "approve": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/approve" - }, - "statuses": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/statuses" - } - }, - "title": "main.tf edited online with Bitbucket", - "close_source_branch": true, - "reviewers": [], - "id": 5, - "destination": { - "commit": { - "hash": "fe607a7f5172", - "type": "commit", - "links": { - "self": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/fe607a7f5172" - }, - "html": { - "href": "https://bitbucket.org/lkysow/atlantis-example/commits/fe607a7f5172" - } - } - }, - "repository": { - "links": { - "self": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example" - }, - "html": { - "href": "https://bitbucket.org/lkysow/atlantis-example" - }, - "avatar": { - "href": "https://bytebucket.org/ravatar/%7B94189367-116b-436a-9f77-2314b97a6067%7D?ts=default" - } - }, - "type": "repository", - "name": "atlantis-example", - "full_name": "lkysow/atlantis-example", - "uuid": "{94189367-116b-436a-9f77-2314b97a6067}" - }, - "branch": { - "name": "master" - } - }, - "created_on": "2018-07-25T12:23:21.100810+00:00", - "summary": { - "raw": "main.tf edited online with Bitbucket", - "markup": "markdown", - "html": "

main.tf edited online with Bitbucket

", - "type": "rendered" - }, - "source": { - "commit": { - "hash": "3428957ade18", - "type": "commit", - "links": { - "self": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/3428957ade18" - }, - "html": { - "href": "https://bitbucket.org/lkysow/atlantis-example/commits/3428957ade18" - } - } - }, - "repository": { - "links": { - "self": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example" - }, - "html": { - "href": "https://bitbucket.org/lkysow/atlantis-example" - }, - "avatar": { - "href": "https://bytebucket.org/ravatar/%7B94189367-116b-436a-9f77-2314b97a6067%7D?ts=default" - } - }, - "type": "repository", - "name": "atlantis-example", - "full_name": "lkysow/atlantis-example", - "uuid": "{94189367-116b-436a-9f77-2314b97a6067}" - }, - "branch": { - "name": "lkysow/maintf-edited-online-with-bitbucket-1532521398289" - } - }, - "comment_count": 3, - "state": "OPEN", - "task_count": 0, - "participants": [ - { - "role": "PARTICIPANT", - "participated_on": "2018-07-28T00:06:42.255492+00:00", - "type": "participant", - "approved": true, - "user": { - "username": "author", - "display_name": "Luke", - "account_id": "557058:dc3817de-68b5-45cd-b81c-5c39d2560090", - "links": { - "self": { - "href": "https://api.bitbucket.org/2.0/users/lkysow" - }, - "html": { - "href": "https://bitbucket.org/lkysow/" - }, - "avatar": { - "href": "https://bitbucket.org/account/lkysow/avatar/" - } - }, - "type": "user", - "uuid": "{bf34a99b-8a11-452c-8fbc-bdffc340e584}" - } - } - ], - "reason": "", - "updated_on": "2018-07-28T00:06:42.257659+00:00", - "author": { - "username": "lkysow", - "display_name": "Luke", - "account_id": "557058:dc3817de-68b5-45cd-b81c-5c39d2560090", - "links": { - "self": { - "href": "https://api.bitbucket.org/2.0/users/lkysow" - }, - "html": { - "href": "https://bitbucket.org/lkysow/" - }, - "avatar": { - "href": "https://bitbucket.org/account/lkysow/avatar/" - } - }, - "type": "user", - "uuid": "{bf34a99b-8a11-452c-8fbc-bdffc340e584}" - }, - "merge_commit": null, - "closed_by": null -} \ No newline at end of file diff --git a/server/events/vcs/azuredevops/testdata/pull-approved-multiple.json b/server/events/vcs/azuredevops/testdata/pull-approved-multiple.json deleted file mode 100644 index 32ae46622..000000000 --- a/server/events/vcs/azuredevops/testdata/pull-approved-multiple.json +++ /dev/null @@ -1,190 +0,0 @@ -{ - "type": "pullrequest", - "description": "main.tf edited online with Bitbucket", - "links": { - "decline": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/decline" - }, - "commits": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/commits" - }, - "self": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5" - }, - "comments": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/comments" - }, - "merge": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/merge" - }, - "html": { - "href": "https://bitbucket.org/lkysow/atlantis-example/pull-requests/5" - }, - "activity": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/activity" - }, - "diff": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/diff" - }, - "approve": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/approve" - }, - "statuses": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/statuses" - } - }, - "title": "main.tf edited online with Bitbucket", - "close_source_branch": true, - "reviewers": [], - "id": 5, - "destination": { - "commit": { - "hash": "fe607a7f5172", - "type": "commit", - "links": { - "self": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/fe607a7f5172" - }, - "html": { - "href": "https://bitbucket.org/lkysow/atlantis-example/commits/fe607a7f5172" - } - } - }, - "repository": { - "links": { - "self": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example" - }, - "html": { - "href": "https://bitbucket.org/lkysow/atlantis-example" - }, - "avatar": { - "href": "https://bytebucket.org/ravatar/%7B94189367-116b-436a-9f77-2314b97a6067%7D?ts=default" - } - }, - "type": "repository", - "name": "atlantis-example", - "full_name": "lkysow/atlantis-example", - "uuid": "{94189367-116b-436a-9f77-2314b97a6067}" - }, - "branch": { - "name": "master" - } - }, - "created_on": "2018-07-25T12:23:21.100810+00:00", - "summary": { - "raw": "main.tf edited online with Bitbucket", - "markup": "markdown", - "html": "

main.tf edited online with Bitbucket

", - "type": "rendered" - }, - "source": { - "commit": { - "hash": "3428957ade18", - "type": "commit", - "links": { - "self": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/3428957ade18" - }, - "html": { - "href": "https://bitbucket.org/lkysow/atlantis-example/commits/3428957ade18" - } - } - }, - "repository": { - "links": { - "self": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example" - }, - "html": { - "href": "https://bitbucket.org/lkysow/atlantis-example" - }, - "avatar": { - "href": "https://bytebucket.org/ravatar/%7B94189367-116b-436a-9f77-2314b97a6067%7D?ts=default" - } - }, - "type": "repository", - "name": "atlantis-example", - "full_name": "lkysow/atlantis-example", - "uuid": "{94189367-116b-436a-9f77-2314b97a6067}" - }, - "branch": { - "name": "lkysow/maintf-edited-online-with-bitbucket-1532521398289" - } - }, - "comment_count": 3, - "state": "OPEN", - "task_count": 0, - "participants": [ - { - "role": "PARTICIPANT", - "participated_on": "2018-07-28T00:06:42.255492+00:00", - "type": "participant", - "approved": true, - "user": { - "username": "author", - "display_name": "Luke", - "account_id": "557058:dc3817de-68b5-45cd-b81c-5c39d2560090", - "links": { - "self": { - "href": "https://api.bitbucket.org/2.0/users/lkysow" - }, - "html": { - "href": "https://bitbucket.org/lkysow/" - }, - "avatar": { - "href": "https://bitbucket.org/account/lkysow/avatar/" - } - }, - "type": "user", - "uuid": "{bf34a99b-8a11-452c-8fbc-bdffc340e584}" - } - }, - { - "role": "PARTICIPANT", - "participated_on": "2018-07-28T00:06:42.255492+00:00", - "type": "participant", - "approved": true, - "user": { - "username": "approver", - "display_name": "Luke", - "account_id": "557058:dc3817de-68b5-45cd-b81c-5c39d2560090", - "links": { - "self": { - "href": "https://api.bitbucket.org/2.0/users/lkysow" - }, - "html": { - "href": "https://bitbucket.org/lkysow/" - }, - "avatar": { - "href": "https://bitbucket.org/account/lkysow/avatar/" - } - }, - "type": "user", - "uuid": "{bf34a99b-8a11-452c-8fbc-bdffc340e584}" - } - } - ], - "reason": "", - "updated_on": "2018-07-28T00:06:42.257659+00:00", - "author": { - "username": "lkysow", - "display_name": "Luke", - "account_id": "557058:dc3817de-68b5-45cd-b81c-5c39d2560090", - "links": { - "self": { - "href": "https://api.bitbucket.org/2.0/users/lkysow" - }, - "html": { - "href": "https://bitbucket.org/lkysow/" - }, - "avatar": { - "href": "https://bitbucket.org/account/lkysow/avatar/" - } - }, - "type": "user", - "uuid": "{bf34a99b-8a11-452c-8fbc-bdffc340e584}" - }, - "merge_commit": null, - "closed_by": null -} \ No newline at end of file diff --git a/server/events/vcs/azuredevops/testdata/pull-approved.json b/server/events/vcs/azuredevops/testdata/pull-approved.json deleted file mode 100644 index c40340d28..000000000 --- a/server/events/vcs/azuredevops/testdata/pull-approved.json +++ /dev/null @@ -1,160 +0,0 @@ -{ - "type": "pullrequest", - "description": "main.tf edited online with Bitbucket", - "links": { - "decline": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/decline" - }, - "commits": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/commits" - }, - "self": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5" - }, - "comments": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/comments" - }, - "merge": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/merge" - }, - "html": { - "href": "https://bitbucket.org/lkysow/atlantis-example/pull-requests/5" - }, - "activity": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/activity" - }, - "diff": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/diff" - }, - "approve": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/approve" - }, - "statuses": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/statuses" - } - }, - "title": "main.tf edited online with Bitbucket", - "close_source_branch": true, - "reviewers": [], - "id": 5, - "destination": { - "commit": { - "hash": "fe607a7f5172", - "type": "commit", - "links": { - "self": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/fe607a7f5172" - }, - "html": { - "href": "https://bitbucket.org/lkysow/atlantis-example/commits/fe607a7f5172" - } - } - }, - "repository": { - "links": { - "self": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example" - }, - "html": { - "href": "https://bitbucket.org/lkysow/atlantis-example" - } - }, - "type": "repository", - "name": "atlantis-example", - "full_name": "lkysow/atlantis-example", - "uuid": "{94189367-116b-436a-9f77-2314b97a6067}" - }, - "branch": { - "name": "master" - } - }, - "created_on": "2018-07-25T12:23:21.100810+00:00", - "summary": { - "raw": "main.tf edited online with Bitbucket", - "markup": "markdown", - "html": "

main.tf edited online with Bitbucket

", - "type": "rendered" - }, - "source": { - "commit": { - "hash": "3428957ade18", - "type": "commit", - "links": { - "self": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/3428957ade18" - }, - "html": { - "href": "https://bitbucket.org/lkysow/atlantis-example/commits/3428957ade18" - } - } - }, - "repository": { - "links": { - "self": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example" - }, - "html": { - "href": "https://bitbucket.org/lkysow/atlantis-example" - } - }, - "type": "repository", - "name": "atlantis-example", - "full_name": "lkysow/atlantis-example", - "uuid": "{94189367-116b-436a-9f77-2314b97a6067}" - }, - "branch": { - "name": "lkysow/maintf-edited-online-with-bitbucket-1532521398289" - } - }, - "comment_count": 3, - "state": "OPEN", - "task_count": 0, - "participants": [ - { - "role": "PARTICIPANT", - "participated_on": "2018-07-28T00:06:42.255492+00:00", - "type": "participant", - "approved": true, - "user": { - "username": "approver", - "display_name": "Luke", - "account_id": "557058:dc3817de-68b5-45cd-b81c-5c39d2560090", - "links": { - "self": { - "href": "https://api.bitbucket.org/2.0/users/lkysow" - }, - "html": { - "href": "https://bitbucket.org/lkysow/" - }, - "avatar": { - "href": "https://bitbucket.org/account/lkysow/avatar/" - } - }, - "type": "user", - "uuid": "{bf34a99b-8a11-452c-8fbc-bdffc340e584}" - } - } - ], - "reason": "", - "updated_on": "2018-07-28T00:06:42.257659+00:00", - "author": { - "username": "lkysow", - "display_name": "Luke", - "account_id": "557058:dc3817de-68b5-45cd-b81c-5c39d2560090", - "links": { - "self": { - "href": "https://api.bitbucket.org/2.0/users/lkysow" - }, - "html": { - "href": "https://bitbucket.org/lkysow/" - }, - "avatar": { - "href": "https://bitbucket.org/account/lkysow/avatar/" - } - }, - "type": "user", - "uuid": "{bf34a99b-8a11-452c-8fbc-bdffc340e584}" - }, - "merge_commit": null, - "closed_by": null -} \ No newline at end of file diff --git a/server/events/vcs/azuredevops/testdata/pull-unapproved.json b/server/events/vcs/azuredevops/testdata/pull-unapproved.json deleted file mode 100644 index 9d8de84ac..000000000 --- a/server/events/vcs/azuredevops/testdata/pull-unapproved.json +++ /dev/null @@ -1,135 +0,0 @@ -{ - "type": "pullrequest", - "description": "main.tf edited online with Bitbucket", - "links": { - "decline": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/decline" - }, - "commits": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/commits" - }, - "self": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5" - }, - "comments": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/comments" - }, - "merge": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/merge" - }, - "html": { - "href": "https://bitbucket.org/lkysow/atlantis-example/pull-requests/5" - }, - "activity": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/activity" - }, - "diff": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/diff" - }, - "approve": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/approve" - }, - "statuses": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/5/statuses" - } - }, - "title": "main.tf edited online with Bitbucket", - "close_source_branch": true, - "reviewers": [], - "id": 5, - "destination": { - "commit": { - "hash": "fe607a7f5172", - "type": "commit", - "links": { - "self": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/fe607a7f5172" - }, - "html": { - "href": "https://bitbucket.org/lkysow/atlantis-example/commits/fe607a7f5172" - } - } - }, - "repository": { - "links": { - "self": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example" - }, - "html": { - "href": "https://bitbucket.org/lkysow/atlantis-example" - } - }, - "type": "repository", - "name": "atlantis-example", - "full_name": "lkysow/atlantis-example", - "uuid": "{94189367-116b-436a-9f77-2314b97a6067}" - }, - "branch": { - "name": "master" - } - }, - "created_on": "2018-07-25T12:23:21.100810+00:00", - "summary": { - "raw": "main.tf edited online with Bitbucket", - "markup": "markdown", - "html": "

main.tf edited online with Bitbucket

", - "type": "rendered" - }, - "source": { - "commit": { - "hash": "3428957ade18", - "type": "commit", - "links": { - "self": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/3428957ade18" - }, - "html": { - "href": "https://bitbucket.org/lkysow/atlantis-example/commits/3428957ade18" - } - } - }, - "repository": { - "links": { - "self": { - "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example" - }, - "html": { - "href": "https://bitbucket.org/lkysow/atlantis-example" - } - }, - "type": "repository", - "name": "atlantis-example", - "full_name": "lkysow/atlantis-example", - "uuid": "{94189367-116b-436a-9f77-2314b97a6067}" - }, - "branch": { - "name": "lkysow/maintf-edited-online-with-bitbucket-1532521398289" - } - }, - "comment_count": 3, - "state": "OPEN", - "task_count": 0, - "participants": [], - "reason": "", - "updated_on": "2018-07-28T00:06:42.257659+00:00", - "author": { - "username": "lkysow", - "display_name": "Luke", - "account_id": "557058:dc3817de-68b5-45cd-b81c-5c39d2560090", - "links": { - "self": { - "href": "https://api.bitbucket.org/2.0/users/lkysow" - }, - "html": { - "href": "https://bitbucket.org/lkysow/" - }, - "avatar": { - "href": "https://bitbucket.org/account/lkysow/avatar/" - } - }, - "type": "user", - "uuid": "{bf34a99b-8a11-452c-8fbc-bdffc340e584}" - }, - "merge_commit": null, - "closed_by": null -} \ No newline at end of file diff --git a/server/server.go b/server/server.go index beb5feca1..709e548df 100644 --- a/server/server.go +++ b/server/server.go @@ -32,6 +32,7 @@ import ( "github.com/runatlantis/atlantis/server/events/db" "github.com/runatlantis/atlantis/server/events/yaml/valid" + "github.com/benmatselby/go-azuredevops/azuredevops" assetfs "github.com/elazarl/go-bindata-assetfs" "github.com/gorilla/mux" "github.com/pkg/errors" @@ -41,7 +42,6 @@ import ( "github.com/runatlantis/atlantis/server/events/runtime" "github.com/runatlantis/atlantis/server/events/terraform" "github.com/runatlantis/atlantis/server/events/vcs" - "github.com/runatlantis/atlantis/server/events/vcs/azuredevops" "github.com/runatlantis/atlantis/server/events/vcs/bitbucketcloud" "github.com/runatlantis/atlantis/server/events/vcs/bitbucketserver" "github.com/runatlantis/atlantis/server/events/webhooks" @@ -237,6 +237,8 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { BitbucketServerURL: userConfig.BitbucketBaseURL, AzureDevopsUser: userConfig.AzureDevopsUser, AzureDevopsToken: userConfig.AzureDevopsToken, + AzureDevopsOrg: userConfig.AzureDevopsOrg, + AzureDevopsProject: userConfig.AzureDevopsProject, } commentParser := &events.CommentParser{ GithubUser: userConfig.GithubUser, diff --git a/server/user_config.go b/server/user_config.go index beb61ded9..22b400f9d 100644 --- a/server/user_config.go +++ b/server/user_config.go @@ -10,8 +10,8 @@ type UserConfig struct { 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"` + AzureDevopsOrg string `mapstructure:"azuredevops-org"` + AzureDevopsProject string `mapstructure:"azuredevops-project"` AzureDevopsToken string `mapstructure:"azuredevops-token"` AzureDevopsUser string `mapstructure:"azuredevops-user"` AzureDevopsWebhookSecret string `mapstructure:"azuredevops-webhook-secret"`