- switch to go-azuredevops package

- remove ADHostname
- add ADOrg and ADProject
This commit is contained in:
David McPike
2019-04-07 01:57:54 -05:00
parent 08d47de047
commit a17e565ba6
12 changed files with 28 additions and 1206 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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": "<p>main.tf edited online with Bitbucket</p>",
"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
}

View File

@@ -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": "<p>main.tf edited online with Bitbucket</p>",
"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
}

View File

@@ -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": "<p>main.tf edited online with Bitbucket</p>",
"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
}

View File

@@ -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": "<p>main.tf edited online with Bitbucket</p>",
"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
}

View File

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

View File

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