From d1a78cfa51e03e807b6265656311b0d9a1f96412 Mon Sep 17 00:00:00 2001 From: Luke Kysow Date: Mon, 23 Jul 2018 13:34:29 +0200 Subject: [PATCH 01/11] Switch to map instead of switch --- server/events/vcs/proxy.go | 57 ++++++++------------------------------ 1 file changed, 12 insertions(+), 45 deletions(-) diff --git a/server/events/vcs/proxy.go b/server/events/vcs/proxy.go index 01a2c5f483..55c2b50b91 100644 --- a/server/events/vcs/proxy.go +++ b/server/events/vcs/proxy.go @@ -14,7 +14,6 @@ package vcs import ( - "github.com/pkg/errors" "github.com/runatlantis/atlantis/server/events/models" ) @@ -32,9 +31,9 @@ type ClientProxy interface { // DefaultClientProxy proxies calls to the correct VCS client depending on which // VCS host is required. type DefaultClientProxy struct { - GithubClient Client - GitlabClient Client - BitbucketClient Client + // clients maps from the vcs host type to the client that implements the + // api for that host type, ex. github -> github client. + clients map[models.VCSHostType]Client } func NewDefaultClientProxy(githubClient Client, gitlabClient Client, bitbucketClient Client) *DefaultClientProxy { @@ -48,58 +47,26 @@ func NewDefaultClientProxy(githubClient Client, gitlabClient Client, bitbucketCl bitbucketClient = &NotConfiguredVCSClient{} } return &DefaultClientProxy{ - GitlabClient: gitlabClient, - GithubClient: githubClient, - BitbucketClient: bitbucketClient, + clients: map[models.VCSHostType]Client{ + models.Github: githubClient, + models.Gitlab: gitlabClient, + models.Bitbucket: bitbucketClient, + }, } } -var invalidVCSErr = errors.New("Invalid VCS Host. This is a bug!") - func (d *DefaultClientProxy) GetModifiedFiles(repo models.Repo, pull models.PullRequest) ([]string, error) { - switch repo.VCSHost.Type { - case models.Github: - return d.GithubClient.GetModifiedFiles(repo, pull) - case models.Gitlab: - return d.GitlabClient.GetModifiedFiles(repo, pull) - case models.Bitbucket: - return d.BitbucketClient.GetModifiedFiles(repo, pull) - } - return nil, invalidVCSErr + return d.clients[repo.VCSHost.Type].GetModifiedFiles(repo, pull) } func (d *DefaultClientProxy) CreateComment(repo models.Repo, pullNum int, comment string) error { - switch repo.VCSHost.Type { - case models.Github: - return d.GithubClient.CreateComment(repo, pullNum, comment) - case models.Gitlab: - return d.GitlabClient.CreateComment(repo, pullNum, comment) - case models.Bitbucket: - return d.BitbucketClient.CreateComment(repo, pullNum, comment) - } - return invalidVCSErr + return d.clients[repo.VCSHost.Type].CreateComment(repo, pullNum, comment) } func (d *DefaultClientProxy) PullIsApproved(repo models.Repo, pull models.PullRequest) (bool, error) { - switch repo.VCSHost.Type { - case models.Github: - return d.GithubClient.PullIsApproved(repo, pull) - case models.Gitlab: - return d.GitlabClient.PullIsApproved(repo, pull) - case models.Bitbucket: - return d.BitbucketClient.PullIsApproved(repo, pull) - } - return false, invalidVCSErr + return d.clients[repo.VCSHost.Type].PullIsApproved(repo, pull) } func (d *DefaultClientProxy) UpdateStatus(repo models.Repo, pull models.PullRequest, state models.CommitStatus, description string) error { - switch repo.VCSHost.Type { - case models.Github: - return d.GithubClient.UpdateStatus(repo, pull, state, description) - case models.Gitlab: - return d.GitlabClient.UpdateStatus(repo, pull, state, description) - case models.Bitbucket: - return d.BitbucketClient.UpdateStatus(repo, pull, state, description) - } - return invalidVCSErr + return d.clients[repo.VCSHost.Type].UpdateStatus(repo, pull, state, description) } From c21e577e51bd7a5568faf06ef130e30877b86c65 Mon Sep 17 00:00:00 2001 From: Luke Kysow Date: Mon, 23 Jul 2018 13:35:46 +0200 Subject: [PATCH 02/11] Add bitbucket cloud/server events --- .../bitbucket-cloud-pull-event-rejected.json | 233 ++++++++++++++++++ .../bitbucket-server-comment-event.json | 105 ++++++++ .../bitbucket-server-pull-event-created.json | 84 +++++++ 3 files changed, 422 insertions(+) create mode 100644 server/events/testdata/bitbucket-cloud-pull-event-rejected.json create mode 100644 server/events/testdata/bitbucket-server-comment-event.json create mode 100644 server/events/testdata/bitbucket-server-pull-event-created.json diff --git a/server/events/testdata/bitbucket-cloud-pull-event-rejected.json b/server/events/testdata/bitbucket-cloud-pull-event-rejected.json new file mode 100644 index 0000000000..5ee1c603e2 --- /dev/null +++ b/server/events/testdata/bitbucket-cloud-pull-event-rejected.json @@ -0,0 +1,233 @@ +{ + "pullrequest": { + "merge_commit": null, + "description": "main.tf edited online with Bitbucket", + "links": { + "decline": { + "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/3/decline" + }, + "commits": { + "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/3/commits" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/3" + }, + "comments": { + "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/3/comments" + }, + "merge": { + "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/3/merge" + }, + "html": { + "href": "https://bitbucket.org/lkysow/atlantis-example/pull-requests/3" + }, + "activity": { + "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/3/activity" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/3/diff" + }, + "approve": { + "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/3/approve" + }, + "statuses": { + "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/3/statuses" + } + }, + "title": "main.tf edited online with Bitbucket", + "close_source_branch": false, + "reviewers": [], + "destination": { + "commit": { + "hash": "c21506eeea5f", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/c21506eeea5f" + } + } + }, + "branch": { + "name": "master" + }, + "repository": { + "full_name": "lkysow/atlantis-example", + "type": "repository", + "name": "atlantis-example", + "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" + } + }, + "uuid": "{94189367-116b-436a-9f77-2314b97a6067}" + } + }, + "comment_count": 1, + "closed_by": { + "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}" + }, + "summary": { + "raw": "main.tf edited online with Bitbucket", + "markup": "markdown", + "html": "

main.tf edited online with Bitbucket

", + "type": "rendered" + }, + "source": { + "commit": { + "hash": "ff06f4002ff8", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/ff06f4002ff8" + } + } + }, + "branch": { + "name": "lkysow/maintf-edited-online-with-bitbucket-1532029760658" + }, + "repository": { + "full_name": "lkysow/atlantis-example", + "type": "repository", + "name": "atlantis-example", + "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" + } + }, + "uuid": "{94189367-116b-436a-9f77-2314b97a6067}" + } + }, + "created_on": "2018-07-19T19:49:24.172710+00:00", + "state": "DECLINED", + "task_count": 0, + "participants": [ + { + "type": "participant", + "user": { + "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}" + }, + "role": "PARTICIPANT", + "approved": false, + "participated_on": "2018-07-19T19:49:29.565800+00:00" + } + ], + "reason": "Declined", + "updated_on": "2018-07-21T21:06:38.096401+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}" + }, + "type": "pullrequest", + "id": 3 + }, + "actor": { + "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}" + }, + "repository": { + "scm": "git", + "website": "", + "name": "atlantis-example", + "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" + } + }, + "full_name": "lkysow/atlantis-example", + "owner": { + "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}" + }, + "type": "repository", + "is_private": false, + "uuid": "{94189367-116b-436a-9f77-2314b97a6067}" + } +} \ No newline at end of file diff --git a/server/events/testdata/bitbucket-server-comment-event.json b/server/events/testdata/bitbucket-server-comment-event.json new file mode 100644 index 0000000000..c188a79bb4 --- /dev/null +++ b/server/events/testdata/bitbucket-server-comment-event.json @@ -0,0 +1,105 @@ +{ + "eventKey": "pr:comment:added", + "date": "2018-07-21T23:20:30+0200", + "actor": { + "name": "lkysow", + "emailAddress": "lkysow@gmail.com", + "id": 1, + "displayName": "Luke Kysow", + "active": true, + "slug": "lkysow", + "type": "NORMAL" + }, + "pullRequest": { + "id": 1, + "version": 0, + "title": "Null resource", + "state": "OPEN", + "open": true, + "closed": false, + "createdDate": 1532207977313, + "updatedDate": 1532207977313, + "fromRef": { + "id": "refs/heads/branch", + "displayId": "branch", + "latestCommit": "bfb1af1ba9c2a2fa84cd61af67e6e1b60a22e060", + "repository": { + "slug": "atlantis-example", + "id": 1, + "name": "atlantis-example", + "scmId": "git", + "state": "AVAILABLE", + "statusMessage": "Available", + "forkable": true, + "project": { + "key": "AT", + "id": 1, + "name": "atlantis", + "public": false, + "type": "NORMAL" + }, + "public": false + } + }, + "toRef": { + "id": "refs/heads/master", + "displayId": "master", + "latestCommit": "3d1f26bc1c8eeb5ad94e247c92072717b9de6aa0", + "repository": { + "slug": "atlantis-example", + "id": 1, + "name": "atlantis-example", + "scmId": "git", + "state": "AVAILABLE", + "statusMessage": "Available", + "forkable": true, + "project": { + "key": "AT", + "id": 1, + "name": "atlantis", + "public": false, + "type": "NORMAL" + }, + "public": false + } + }, + "locked": false, + "author": { + "user": { + "name": "lkysow", + "emailAddress": "lkysow@gmail.com", + "id": 1, + "displayName": "Luke Kysow", + "active": true, + "slug": "lkysow", + "type": "NORMAL" + }, + "role": "AUTHOR", + "approved": false, + "status": "UNAPPROVED" + }, + "reviewers": [], + "participants": [] + }, + "comment": { + "properties": { + "repositoryId": 1 + }, + "id": 1, + "version": 0, + "text": "atlantis plan", + "author": { + "name": "lkysow", + "emailAddress": "lkysow@gmail.com", + "id": 1, + "displayName": "Luke Kysow", + "active": true, + "slug": "lkysow", + "type": "NORMAL" + }, + "createdDate": 1532208030682, + "updatedDate": 1532208030682, + "comments": [], + "tasks": [] + } +} \ No newline at end of file diff --git a/server/events/testdata/bitbucket-server-pull-event-created.json b/server/events/testdata/bitbucket-server-pull-event-created.json new file mode 100644 index 0000000000..1292c0a0db --- /dev/null +++ b/server/events/testdata/bitbucket-server-pull-event-created.json @@ -0,0 +1,84 @@ +{ + "eventKey": "pr:opened", + "date": "2018-07-21T23:19:37+0200", + "actor": { + "name": "lkysow", + "emailAddress": "lkysow@gmail.com", + "id": 1, + "displayName": "Luke Kysow", + "active": true, + "slug": "lkysow", + "type": "NORMAL" + }, + "pullRequest": { + "id": 1, + "version": 0, + "title": "Null resource", + "state": "OPEN", + "open": true, + "closed": false, + "createdDate": 1532207977313, + "updatedDate": 1532207977313, + "fromRef": { + "id": "refs/heads/branch", + "displayId": "branch", + "latestCommit": "bfb1af1ba9c2a2fa84cd61af67e6e1b60a22e060", + "repository": { + "slug": "atlantis-example", + "id": 1, + "name": "atlantis-example", + "scmId": "git", + "state": "AVAILABLE", + "statusMessage": "Available", + "forkable": true, + "project": { + "key": "AT", + "id": 1, + "name": "atlantis", + "public": false, + "type": "NORMAL" + }, + "public": false + } + }, + "toRef": { + "id": "refs/heads/master", + "displayId": "master", + "latestCommit": "3d1f26bc1c8eeb5ad94e247c92072717b9de6aa0", + "repository": { + "slug": "atlantis-example", + "id": 1, + "name": "atlantis-example", + "scmId": "git", + "state": "AVAILABLE", + "statusMessage": "Available", + "forkable": true, + "project": { + "key": "AT", + "id": 1, + "name": "atlantis", + "public": false, + "type": "NORMAL" + }, + "public": false + } + }, + "locked": false, + "author": { + "user": { + "name": "lkysow", + "emailAddress": "lkysow@gmail.com", + "id": 1, + "displayName": "Luke Kysow", + "active": true, + "slug": "lkysow", + "type": "NORMAL" + }, + "role": "AUTHOR", + "approved": false, + "status": "UNAPPROVED" + }, + "reviewers": [], + "participants": [] + } +} \ No newline at end of file From 718619c045d33dca6876047e15fe768ddc034832 Mon Sep 17 00:00:00 2001 From: Luke Kysow Date: Mon, 23 Jul 2018 13:37:20 +0200 Subject: [PATCH 03/11] Rename Bitbucket to BitbucketCloud --- server/events/command_runner.go | 2 +- server/events/event_parser.go | 18 ++++++++-------- server/events/event_parser_test.go | 12 +++++------ server/events/mocks/mock_event_parsing.go | 16 +++++++------- server/events/models/models.go | 9 +++++--- server/events/models/models_test.go | 2 +- .../vcs/bitbucketcloud/bitbucketcloud.go | 4 ++++ .../{bitbucket => bitbucketcloud}/client.go | 4 ++-- .../client_test.go | 12 +++++------ .../{bitbucket => bitbucketcloud}/models.go | 2 +- server/events/vcs/proxy.go | 16 ++++++++------ server/events_controller.go | 21 +++++++++++-------- server/events_controller_e2e_test.go | 2 +- server/server.go | 10 ++++----- 14 files changed, 72 insertions(+), 58 deletions(-) create mode 100644 server/events/vcs/bitbucketcloud/bitbucketcloud.go rename server/events/vcs/{bitbucket => bitbucketcloud}/client.go (98%) rename server/events/vcs/{bitbucket => bitbucketcloud}/client_test.go (91%) rename server/events/vcs/{bitbucket => bitbucketcloud}/models.go (99%) diff --git a/server/events/command_runner.go b/server/events/command_runner.go index 7fa7a8af68..842d38f09e 100644 --- a/server/events/command_runner.go +++ b/server/events/command_runner.go @@ -132,7 +132,7 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead pull, headRepo, err = c.getGithubData(baseRepo, pullNum) case models.Gitlab: pull, err = c.getGitlabData(baseRepo, pullNum) - case models.Bitbucket: + case models.BitbucketCloud: if maybePull == nil { err = errors.New("pull request should not be nil, this is a bug!") } diff --git a/server/events/event_parser.go b/server/events/event_parser.go index 40f0082d9c..020af5c768 100644 --- a/server/events/event_parser.go +++ b/server/events/event_parser.go @@ -24,7 +24,7 @@ import ( "github.com/lkysow/go-gitlab" "github.com/pkg/errors" "github.com/runatlantis/atlantis/server/events/models" - "github.com/runatlantis/atlantis/server/events/vcs/bitbucket" + "github.com/runatlantis/atlantis/server/events/vcs/bitbucketcloud" "gopkg.in/go-playground/validator.v9" ) @@ -123,7 +123,7 @@ type EventParsing interface { ParseGitlabMergeRequest(mr *gitlab.MergeRequest, baseRepo models.Repo) models.PullRequest ParseBitbucketCloudPullEvent(body []byte) (pull models.PullRequest, baseRepo models.Repo, headRepo models.Repo, user models.User, err error) ParseBitbucketCloudCommentEvent(body []byte) (pull models.PullRequest, baseRepo models.Repo, headRepo models.Repo, user models.User, comment string, err error) - GetBitbucketEventType(eventTypeHeader string) models.PullRequestEventType + GetBitbucketCloudEventType(eventTypeHeader string) models.PullRequestEventType } type EventParser struct { @@ -135,9 +135,9 @@ type EventParser struct { BitbucketToken string } -// GetBitbucketEventType translates the bitbucket header name into a pull +// GetBitbucketCloudEventType translates the bitbucket header name into a pull // request event type. -func (e *EventParser) GetBitbucketEventType(eventTypeHeader string) models.PullRequestEventType { +func (e *EventParser) GetBitbucketCloudEventType(eventTypeHeader string) models.PullRequestEventType { switch eventTypeHeader { case "pullrequest:created": return models.OpenedPullEvent @@ -150,7 +150,7 @@ func (e *EventParser) GetBitbucketEventType(eventTypeHeader string) models.PullR } func (e *EventParser) ParseBitbucketCloudCommentEvent(body []byte) (pull models.PullRequest, baseRepo models.Repo, headRepo models.Repo, user models.User, comment string, err error) { - var event bitbucket.CommentEvent + var event bitbucketcloud.CommentEvent if err = json.Unmarshal(body, &event); err != nil { err = errors.Wrap(err, "parsing json") return @@ -163,7 +163,7 @@ func (e *EventParser) ParseBitbucketCloudCommentEvent(body []byte) (pull models. return } -func (e *EventParser) parseCommonBitbucketEventData(event bitbucket.CommonEventData) (pull models.PullRequest, baseRepo models.Repo, headRepo models.Repo, user models.User, err error) { +func (e *EventParser) parseCommonBitbucketEventData(event bitbucketcloud.CommonEventData) (pull models.PullRequest, baseRepo models.Repo, headRepo models.Repo, user models.User, err error) { var prState models.PullRequestState switch *event.PullRequest.State { case "OPEN": @@ -180,7 +180,7 @@ func (e *EventParser) parseCommonBitbucketEventData(event bitbucket.CommonEventD } headRepo, err = models.NewRepo( - models.Bitbucket, + models.BitbucketCloud, *event.PullRequest.Source.Repository.FullName, *event.PullRequest.Source.Repository.Links.HTML.HREF, e.BitbucketUser, @@ -189,7 +189,7 @@ func (e *EventParser) parseCommonBitbucketEventData(event bitbucket.CommonEventD return } baseRepo, err = models.NewRepo( - models.Bitbucket, + models.BitbucketCloud, *event.Repository.FullName, *event.Repository.Links.HTML.HREF, e.BitbucketUser, @@ -214,7 +214,7 @@ func (e *EventParser) parseCommonBitbucketEventData(event bitbucket.CommonEventD } func (e *EventParser) ParseBitbucketCloudPullEvent(body []byte) (pull models.PullRequest, baseRepo models.Repo, headRepo models.Repo, user models.User, err error) { - var event bitbucket.PullRequestEvent + var event bitbucketcloud.PullRequestEvent if err = json.Unmarshal(body, &event); err != nil { err = errors.Wrap(err, "parsing json") return diff --git a/server/events/event_parser_test.go b/server/events/event_parser_test.go index dabec68f1d..fa81ffc88a 100644 --- a/server/events/event_parser_test.go +++ b/server/events/event_parser_test.go @@ -556,7 +556,7 @@ func TestParseBitbucketCloudCommentEvent_ValidEvent(t *testing.T) { SanitizedCloneURL: "https://bitbucket.org/lkysow/atlantis-example.git", VCSHost: models.VCSHost{ Hostname: "bitbucket.org", - Type: models.Bitbucket, + Type: models.BitbucketCloud, }, } Equals(t, expBaseRepo, baseRepo) @@ -577,7 +577,7 @@ func TestParseBitbucketCloudCommentEvent_ValidEvent(t *testing.T) { SanitizedCloneURL: "https://bitbucket.org/lkysow-fork/atlantis-example.git", VCSHost: models.VCSHost{ Hostname: "bitbucket.org", - Type: models.Bitbucket, + Type: models.BitbucketCloud, }, }, headRepo) Equals(t, models.User{ @@ -641,7 +641,7 @@ func TestParseBitbucketCloudPullEvent_ValidEvent(t *testing.T) { SanitizedCloneURL: "https://bitbucket.org/lkysow/atlantis-example.git", VCSHost: models.VCSHost{ Hostname: "bitbucket.org", - Type: models.Bitbucket, + Type: models.BitbucketCloud, }, } Equals(t, expBaseRepo, baseRepo) @@ -662,7 +662,7 @@ func TestParseBitbucketCloudPullEvent_ValidEvent(t *testing.T) { SanitizedCloneURL: "https://bitbucket.org/lkysow-fork/atlantis-example.git", VCSHost: models.VCSHost{ Hostname: "bitbucket.org", - Type: models.Bitbucket, + Type: models.BitbucketCloud, }, }, headRepo) Equals(t, models.User{ @@ -670,7 +670,7 @@ func TestParseBitbucketCloudPullEvent_ValidEvent(t *testing.T) { }, user) } -func TestGetBitbucketEventType(t *testing.T) { +func TestGetBitbucketCloudEventType(t *testing.T) { cases := []struct { header string exp models.PullRequestEventType @@ -698,7 +698,7 @@ func TestGetBitbucketEventType(t *testing.T) { } for _, c := range cases { t.Run(c.header, func(t *testing.T) { - act := parser.GetBitbucketEventType(c.header) + act := parser.GetBitbucketCloudEventType(c.header) Equals(t, c.exp, act) }) } diff --git a/server/events/mocks/mock_event_parsing.go b/server/events/mocks/mock_event_parsing.go index 4da2d4dee2..9395fb8486 100644 --- a/server/events/mocks/mock_event_parsing.go +++ b/server/events/mocks/mock_event_parsing.go @@ -244,9 +244,9 @@ func (mock *MockEventParsing) ParseBitbucketCloudCommentEvent(body []byte) (mode return ret0, ret1, ret2, ret3, ret4, ret5 } -func (mock *MockEventParsing) GetBitbucketEventType(eventTypeHeader string) models.PullRequestEventType { +func (mock *MockEventParsing) GetBitbucketCloudEventType(eventTypeHeader string) models.PullRequestEventType { params := []pegomock.Param{eventTypeHeader} - result := pegomock.GetGenericMockFrom(mock).Invoke("GetBitbucketEventType", params, []reflect.Type{reflect.TypeOf((*models.PullRequestEventType)(nil)).Elem()}) + result := pegomock.GetGenericMockFrom(mock).Invoke("GetBitbucketCloudEventType", params, []reflect.Type{reflect.TypeOf((*models.PullRequestEventType)(nil)).Elem()}) var ret0 models.PullRequestEventType if len(result) != 0 { if result[0] != nil { @@ -521,23 +521,23 @@ func (c *EventParsing_ParseBitbucketCloudCommentEvent_OngoingVerification) GetAl return } -func (verifier *VerifierEventParsing) GetBitbucketEventType(eventTypeHeader string) *EventParsing_GetBitbucketEventType_OngoingVerification { +func (verifier *VerifierEventParsing) GetBitbucketCloudEventType(eventTypeHeader string) *EventParsing_GetBitbucketCloudEventType_OngoingVerification { params := []pegomock.Param{eventTypeHeader} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GetBitbucketEventType", params) - return &EventParsing_GetBitbucketEventType_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GetBitbucketCloudEventType", params) + return &EventParsing_GetBitbucketCloudEventType_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } -type EventParsing_GetBitbucketEventType_OngoingVerification struct { +type EventParsing_GetBitbucketCloudEventType_OngoingVerification struct { mock *MockEventParsing methodInvocations []pegomock.MethodInvocation } -func (c *EventParsing_GetBitbucketEventType_OngoingVerification) GetCapturedArguments() string { +func (c *EventParsing_GetBitbucketCloudEventType_OngoingVerification) GetCapturedArguments() string { eventTypeHeader := c.GetAllCapturedArguments() return eventTypeHeader[len(eventTypeHeader)-1] } -func (c *EventParsing_GetBitbucketEventType_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { +func (c *EventParsing_GetBitbucketCloudEventType_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) if len(params) > 0 { _param0 = make([]string, len(params[0])) diff --git a/server/events/models/models.go b/server/events/models/models.go index e26e3fd068..ee2d327f81 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -237,7 +237,8 @@ type VCSHostType int const ( Github VCSHostType = iota Gitlab - Bitbucket + BitbucketCloud + BitbucketServer ) func (h VCSHostType) String() string { @@ -246,8 +247,10 @@ func (h VCSHostType) String() string { return "Github" case Gitlab: return "Gitlab" - case Bitbucket: - return "Bitbucket" + case BitbucketCloud: + return "BitbucketCloud" + case BitbucketServer: + return "BitbucketServer" } return "" } diff --git a/server/events/models/models_test.go b/server/events/models/models_test.go index 712b990729..22faa3a384 100644 --- a/server/events/models/models_test.go +++ b/server/events/models/models_test.go @@ -61,7 +61,7 @@ func TestNewRepo_FullNameWrongFormat(t *testing.T) { // If the clone url doesn't end with .git it is appended func TestNewRepo_MissingDotGit(t *testing.T) { - repo, err := models.NewRepo(models.Bitbucket, "owner/repo", "https://bitbucket.org/owner/repo", "u", "p") + repo, err := models.NewRepo(models.BitbucketCloud, "owner/repo", "https://bitbucket.org/owner/repo", "u", "p") Ok(t, err) Equals(t, repo.CloneURL, "https://u:p@bitbucket.org/owner/repo.git") Equals(t, repo.SanitizedCloneURL, "https://bitbucket.org/owner/repo.git") diff --git a/server/events/vcs/bitbucketcloud/bitbucketcloud.go b/server/events/vcs/bitbucketcloud/bitbucketcloud.go new file mode 100644 index 0000000000..267a723a9c --- /dev/null +++ b/server/events/vcs/bitbucketcloud/bitbucketcloud.go @@ -0,0 +1,4 @@ +// Package bitbucketcloud holds code for Bitbucket Cloud aka (bitbucket.org). +// It is separate from bitbucketserver because Bitbucket Server has different +// APIs. +package bitbucketcloud diff --git a/server/events/vcs/bitbucket/client.go b/server/events/vcs/bitbucketcloud/client.go similarity index 98% rename from server/events/vcs/bitbucket/client.go rename to server/events/vcs/bitbucketcloud/client.go index c4ddd9900e..66fcd21ea4 100644 --- a/server/events/vcs/bitbucket/client.go +++ b/server/events/vcs/bitbucketcloud/client.go @@ -1,4 +1,4 @@ -package bitbucket +package bitbucketcloud import ( "bytes" @@ -29,7 +29,7 @@ type Client struct { AtlantisURL string } -// NewClient builds a bitbucket client. Returns an error if the baseURL is +// NewClient builds a bitbucket cloud client. Returns an error if the baseURL is // malformed. httpClient is the client to use to make the requests, username // and password are used as basic auth in the requests, baseURL is the API's // baseURL, ex. https://api.bitbucket.org. Don't include the API version, ex. diff --git a/server/events/vcs/bitbucket/client_test.go b/server/events/vcs/bitbucketcloud/client_test.go similarity index 91% rename from server/events/vcs/bitbucket/client_test.go rename to server/events/vcs/bitbucketcloud/client_test.go index 3464948019..283632069f 100644 --- a/server/events/vcs/bitbucket/client_test.go +++ b/server/events/vcs/bitbucketcloud/client_test.go @@ -1,4 +1,4 @@ -package bitbucket_test +package bitbucketcloud_test import ( "fmt" @@ -7,7 +7,7 @@ import ( "testing" "github.com/runatlantis/atlantis/server/events/models" - "github.com/runatlantis/atlantis/server/events/vcs/bitbucket" + "github.com/runatlantis/atlantis/server/events/vcs/bitbucketcloud" . "github.com/runatlantis/atlantis/testing" ) @@ -68,7 +68,7 @@ func TestClient_GetModifiedFilesPagination(t *testing.T) { defer testServer.Close() serverURL = testServer.URL - client, err := bitbucket.NewClient(http.DefaultClient, "user", "pass", serverURL, "runatlantis.io") + client, err := bitbucketcloud.NewClient(http.DefaultClient, "user", "pass", serverURL, "runatlantis.io") Ok(t, err) files, err := client.GetModifiedFiles(models.Repo{ @@ -78,7 +78,7 @@ func TestClient_GetModifiedFilesPagination(t *testing.T) { CloneURL: "", SanitizedCloneURL: "", VCSHost: models.VCSHost{ - Type: models.Bitbucket, + Type: models.BitbucketCloud, Hostname: "bitbucket.org", }, }, models.PullRequest{ @@ -129,7 +129,7 @@ func TestClient_GetModifiedFilesOldNil(t *testing.T) { })) defer testServer.Close() - client, err := bitbucket.NewClient(http.DefaultClient, "user", "pass", testServer.URL, "runatlantis.io") + client, err := bitbucketcloud.NewClient(http.DefaultClient, "user", "pass", testServer.URL, "runatlantis.io") Ok(t, err) files, err := client.GetModifiedFiles(models.Repo{ @@ -139,7 +139,7 @@ func TestClient_GetModifiedFilesOldNil(t *testing.T) { CloneURL: "", SanitizedCloneURL: "", VCSHost: models.VCSHost{ - Type: models.Bitbucket, + Type: models.BitbucketCloud, Hostname: "bitbucket.org", }, }, models.PullRequest{ diff --git a/server/events/vcs/bitbucket/models.go b/server/events/vcs/bitbucketcloud/models.go similarity index 99% rename from server/events/vcs/bitbucket/models.go rename to server/events/vcs/bitbucketcloud/models.go index 83bf398df5..f53f589422 100644 --- a/server/events/vcs/bitbucket/models.go +++ b/server/events/vcs/bitbucketcloud/models.go @@ -1,4 +1,4 @@ -package bitbucket +package bitbucketcloud type CommentEvent struct { CommonEventData diff --git a/server/events/vcs/proxy.go b/server/events/vcs/proxy.go index 55c2b50b91..4779ccd90b 100644 --- a/server/events/vcs/proxy.go +++ b/server/events/vcs/proxy.go @@ -36,21 +36,25 @@ type DefaultClientProxy struct { clients map[models.VCSHostType]Client } -func NewDefaultClientProxy(githubClient Client, gitlabClient Client, bitbucketClient Client) *DefaultClientProxy { +func NewDefaultClientProxy(githubClient Client, gitlabClient Client, bitbucketCloudClient Client, bitbucketServerClient Client) *DefaultClientProxy { if githubClient == nil { githubClient = &NotConfiguredVCSClient{} } if gitlabClient == nil { gitlabClient = &NotConfiguredVCSClient{} } - if bitbucketClient == nil { - bitbucketClient = &NotConfiguredVCSClient{} + if bitbucketCloudClient == nil { + bitbucketCloudClient = &NotConfiguredVCSClient{} + } + if bitbucketServerClient == nil { + bitbucketServerClient = &NotConfiguredVCSClient{} } return &DefaultClientProxy{ clients: map[models.VCSHostType]Client{ - models.Github: githubClient, - models.Gitlab: gitlabClient, - models.Bitbucket: bitbucketClient, + models.Github: githubClient, + models.Gitlab: gitlabClient, + models.BitbucketCloud: bitbucketCloudClient, + models.BitbucketServer: bitbucketServerClient, }, } } diff --git a/server/events_controller.go b/server/events_controller.go index 3a54cdeee9..4c376842ff 100644 --- a/server/events_controller.go +++ b/server/events_controller.go @@ -28,8 +28,11 @@ import ( const githubHeader = "X-Github-Event" const gitlabHeader = "X-Gitlab-Event" + +// bitbucketEventTypeHeader is the same in both cloud and server. const bitbucketEventTypeHeader = "X-Event-Key" -const bitbucketRequestIDHeader = "X-Request-UUID" +const bitbucketCloudRequestIDHeader = "X-Request-UUID" +const bitbucketServerRequestIDHeader = "X-Request-ID" // EventsController handles all webhook requests which signify 'events' in the // VCS host, ex. GitHub. @@ -76,7 +79,7 @@ func (e *EventsController) Post(w http.ResponseWriter, r *http.Request) { e.handleGitlabPost(w, r) return } else if r.Header.Get(bitbucketEventTypeHeader) != "" { - if !e.supportsHost(models.Bitbucket) { + if !e.supportsHost(models.BitbucketCloud) { e.respond(w, logging.Debug, http.StatusBadRequest, "Ignoring request since not configured to support Bitbucket") return } @@ -112,11 +115,11 @@ func (e *EventsController) handleGithubPost(w http.ResponseWriter, r *http.Reque func (e *EventsController) handleBitbucketPost(w http.ResponseWriter, r *http.Request) { eventType := r.Header.Get(bitbucketEventTypeHeader) - reqID := r.Header.Get(bitbucketRequestIDHeader) + reqID := r.Header.Get(bitbucketCloudRequestIDHeader) defer r.Body.Close() // nolint: errcheck body, err := ioutil.ReadAll(r.Body) if err != nil { - e.respond(w, logging.Error, http.StatusBadRequest, "Unable to read body: %s %s=%s", err, bitbucketRequestIDHeader, reqID) + e.respond(w, logging.Error, http.StatusBadRequest, "Unable to read body: %s %s=%s", err, bitbucketCloudRequestIDHeader, reqID) return } switch eventType { @@ -129,7 +132,7 @@ func (e *EventsController) handleBitbucketPost(w http.ResponseWriter, r *http.Re e.HandleBitbucketCommentEvent(w, body, reqID) return default: - e.respond(w, logging.Debug, http.StatusOK, "Ignoring unsupported event type %s %s=%s", eventType, bitbucketRequestIDHeader, reqID) + e.respond(w, logging.Debug, http.StatusOK, "Ignoring unsupported event type %s %s=%s", eventType, bitbucketCloudRequestIDHeader, reqID) } } @@ -156,19 +159,19 @@ func (e *EventsController) HandleGithubCommentEvent(w http.ResponseWriter, event func (e *EventsController) HandleBitbucketCommentEvent(w http.ResponseWriter, body []byte, reqID string) { pull, baseRepo, headRepo, user, comment, err := e.Parser.ParseBitbucketCloudCommentEvent(body) if err != nil { - e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing pull data: %s %s=%s", err, bitbucketRequestIDHeader, reqID) + e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing pull data: %s %s=%s", err, bitbucketCloudRequestIDHeader, reqID) return } - e.handleCommentEvent(w, baseRepo, &headRepo, &pull, user, pull.Num, comment, models.Bitbucket) + e.handleCommentEvent(w, baseRepo, &headRepo, &pull, user, pull.Num, comment, models.BitbucketCloud) } func (e *EventsController) HandleBitbucketPullRequestEvent(w http.ResponseWriter, eventType string, body []byte, reqID string) { pull, baseRepo, headRepo, user, err := e.Parser.ParseBitbucketCloudPullEvent(body) if err != nil { - e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing pull data: %s %s=%s", err, bitbucketRequestIDHeader, reqID) + e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing pull data: %s %s=%s", err, bitbucketCloudRequestIDHeader, reqID) return } - pullEventType := e.Parser.GetBitbucketEventType(eventType) + pullEventType := e.Parser.GetBitbucketCloudEventType(eventType) e.Logger.Info("identified event as type %q", pullEventType.String()) e.handlePullRequestEvent(w, baseRepo, headRepo, pull, user, pullEventType) } diff --git a/server/events_controller_e2e_test.go b/server/events_controller_e2e_test.go index 75f64fbbd2..fa39a7cc28 100644 --- a/server/events_controller_e2e_test.go +++ b/server/events_controller_e2e_test.go @@ -306,7 +306,7 @@ func setupE2E(t *testing.T) (server.EventsController, *vcsmocks.MockClientProxy, GitlabRequestParserValidator: &server.DefaultGitlabRequestParserValidator{}, GitlabWebHookSecret: nil, RepoWhitelistChecker: repoWhitelistChecker, - SupportedVCSHosts: []models.VCSHostType{models.Gitlab, models.Github, models.Bitbucket}, + SupportedVCSHosts: []models.VCSHostType{models.Gitlab, models.Github, models.BitbucketCloud}, VCSClient: e2eVCSClient, } return ctrl, e2eVCSClient, e2eGithubGetter, workingDir diff --git a/server/server.go b/server/server.go index 4637133ea3..3fb862f59c 100644 --- a/server/server.go +++ b/server/server.go @@ -40,7 +40,7 @@ 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/bitbucket" + "github.com/runatlantis/atlantis/server/events/vcs/bitbucketcloud" "github.com/runatlantis/atlantis/server/events/webhooks" "github.com/runatlantis/atlantis/server/events/yaml" "github.com/runatlantis/atlantis/server/logging" @@ -137,7 +137,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { var supportedVCSHosts []models.VCSHostType var githubClient *vcs.GithubClient var gitlabClient *vcs.GitlabClient - var bitbucketClient *bitbucket.Client + var bitbucketClient *bitbucketcloud.Client if userConfig.GithubUser != "" { supportedVCSHosts = append(supportedVCSHosts, models.Github) var err error @@ -168,9 +168,9 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { } } if userConfig.BitbucketUser != "" { - supportedVCSHosts = append(supportedVCSHosts, models.Bitbucket) + supportedVCSHosts = append(supportedVCSHosts, models.BitbucketCloud) var err error - bitbucketClient, err = bitbucket.NewClient( + bitbucketClient, err = bitbucketcloud.NewClient( http.DefaultClient, userConfig.BitbucketUser, userConfig.BitbucketToken, @@ -196,7 +196,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { if err != nil { return nil, errors.Wrap(err, "initializing webhooks") } - vcsClient := vcs.NewDefaultClientProxy(githubClient, gitlabClient, bitbucketClient) + vcsClient := vcs.NewDefaultClientProxy(githubClient, gitlabClient, bitbucketClient, bitbucketClient) commitStatusUpdater := &events.DefaultCommitStatusUpdater{Client: vcsClient} terraformClient, err := terraform.NewClient(userConfig.DataDir) // The flag.Lookup call is to detect if we're running in a unit test. If we From a4c48f5d4720a45b0e485ec4925beb825df8d817 Mon Sep 17 00:00:00 2001 From: Luke Kysow Date: Mon, 23 Jul 2018 19:42:44 +0200 Subject: [PATCH 04/11] Add provisional Bitbucket Server support --- cmd/server.go | 9 +- cmd/server_test.go | 13 - server/events/command_runner.go | 6 +- server/events/event_parser.go | 125 +++++++++- server/events/event_parser_test.go | 4 +- server/events/mocks/mock_event_parsing.go | 153 ++++++++++++ server/events/models/models.go | 20 +- .../bitbucket-server-get-pull-changes.json | 66 ++++++ .../testdata/bitbucket-server-get-pull.json | 156 ++++++++++++ .../bitbucket-server-pull-event-declined.json | 85 +++++++ .../bitbucket-server-pull-event-merged.json | 117 +++++++++ .../vcs/bitbucketcloud/bitbucketcloud.go | 2 + server/events/vcs/bitbucketcloud/client.go | 44 ++-- .../events/vcs/bitbucketcloud/client_test.go | 8 +- server/events/vcs/bitbucketcloud/models.go | 8 + .../vcs/bitbucketserver/bitbucketserver.go | 1 + server/events/vcs/bitbucketserver/client.go | 222 ++++++++++++++++++ server/events/vcs/bitbucketserver/models.go | 66 ++++++ server/events/vcs/vcs.go | 1 + server/events_controller.go | 84 ++++++- server/server.go | 49 ++-- 21 files changed, 1135 insertions(+), 104 deletions(-) create mode 100644 server/events/testdata/bitbucket-server-get-pull-changes.json create mode 100644 server/events/testdata/bitbucket-server-get-pull.json create mode 100644 server/events/testdata/bitbucket-server-pull-event-declined.json create mode 100644 server/events/testdata/bitbucket-server-pull-event-merged.json create mode 100644 server/events/vcs/bitbucketserver/bitbucketserver.go create mode 100644 server/events/vcs/bitbucketserver/client.go create mode 100644 server/events/vcs/bitbucketserver/models.go create mode 100644 server/events/vcs/vcs.go diff --git a/cmd/server.go b/cmd/server.go index c3a88f1c9c..a6451c8062 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -22,6 +22,7 @@ import ( "github.com/mitchellh/go-homedir" "github.com/pkg/errors" "github.com/runatlantis/atlantis/server" + "github.com/runatlantis/atlantis/server/events/vcs/bitbucketcloud" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -56,7 +57,7 @@ const ( SSLKeyFileFlag = "ssl-key-file" // Flag defaults. - DefaultBitbucketHostname = "bitbucket.org" + DefaultBitbucketHostname = bitbucketcloud.Hostname DefaultDataDir = "~/.atlantis" DefaultGHHostname = "github.com" DefaultGitlabHostname = "gitlab.com" @@ -82,7 +83,7 @@ var stringFlags = []stringFlag{ }, { name: BitbucketHostnameFlag, - description: "Currently not supported! We only support bitbucket cloud (aka bitbucket.org) at this time.", + description: "Hostname and port of your Bitbucket Server (aka Stash) installation. If using Bitbucket Cloud (bitbucket.org), no need to set.", defaultValue: DefaultBitbucketHostname, }, { @@ -364,10 +365,6 @@ func (s *ServerCmd) validate(userConfig server.UserConfig) error { return fmt.Errorf("--%s and --%s are both required for ssl", SSLKeyFileFlag, SSLCertFileFlag) } - if userConfig.BitbucketHostname != DefaultBitbucketHostname { - return fmt.Errorf("--%s is currently not allowed because we only support bitbucket cloud", BitbucketHostnameFlag) - } - // The following combinations are valid. // 1. github user and token set // 2. gitlab user and token set diff --git a/cmd/server_test.go b/cmd/server_test.go index 11116cd062..017c87a6a6 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -305,19 +305,6 @@ func TestExecute_ValidateVCSConfig(t *testing.T) { } } -// Currently we only support bitbucket cloud so we shouldn't allow setting of -// the bitbucket hostname flag. -func TestExecute_BitbucketHostname(t *testing.T) { - c := setup(map[string]interface{}{ - cmd.BitbucketTokenFlag: "bitbucket-token", - cmd.BitbucketUserFlag: "bitbucket-token", - cmd.BitbucketHostnameFlag: "hostname", - cmd.RepoWhitelistFlag: "*", - }) - err := c.Execute() - ErrEquals(t, "--bitbucket-hostname is currently not allowed because we only support bitbucket cloud", err) -} - func TestExecute_Defaults(t *testing.T) { t.Log("Should set the defaults for all unspecified flags.") c := setup(map[string]interface{}{ diff --git a/server/events/command_runner.go b/server/events/command_runner.go index 842d38f09e..54c260378d 100644 --- a/server/events/command_runner.go +++ b/server/events/command_runner.go @@ -132,7 +132,7 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead pull, headRepo, err = c.getGithubData(baseRepo, pullNum) case models.Gitlab: pull, err = c.getGitlabData(baseRepo, pullNum) - case models.BitbucketCloud: + case models.BitbucketCloud, models.BitbucketServer: if maybePull == nil { err = errors.New("pull request should not be nil, this is a bug!") } @@ -253,7 +253,9 @@ func (c *DefaultCommandRunner) updatePull(ctx *CommandContext, command CommandIn ctx.Log.Warn("unable to update commit status: %s", err) } comment := c.MarkdownRenderer.Render(res, command.CommandName(), ctx.Log.History.String(), command.IsVerbose()) - c.VCSClient.CreateComment(ctx.BaseRepo, ctx.Pull.Num, comment) // nolint: errcheck + if err := c.VCSClient.CreateComment(ctx.BaseRepo, ctx.Pull.Num, comment); err != nil { + ctx.Log.Err("unable to comment: %s", err) + } } // logPanics logs and creates a comment on the pull request for panics. diff --git a/server/events/event_parser.go b/server/events/event_parser.go index 020af5c768..2e6b9ba69d 100644 --- a/server/events/event_parser.go +++ b/server/events/event_parser.go @@ -25,6 +25,7 @@ import ( "github.com/pkg/errors" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/events/vcs/bitbucketcloud" + "github.com/runatlantis/atlantis/server/events/vcs/bitbucketserver" "gopkg.in/go-playground/validator.v9" ) @@ -124,26 +125,30 @@ type EventParsing interface { ParseBitbucketCloudPullEvent(body []byte) (pull models.PullRequest, baseRepo models.Repo, headRepo models.Repo, user models.User, err error) ParseBitbucketCloudCommentEvent(body []byte) (pull models.PullRequest, baseRepo models.Repo, headRepo models.Repo, user models.User, comment string, err error) GetBitbucketCloudEventType(eventTypeHeader string) models.PullRequestEventType + ParseBitbucketServerPullEvent(body []byte) (pull models.PullRequest, baseRepo models.Repo, headRepo models.Repo, user models.User, err error) + ParseBitbucketServerCommentEvent(body []byte) (pull models.PullRequest, baseRepo models.Repo, headRepo models.Repo, user models.User, comment string, err error) + GetBitbucketServerEventType(eventTypeHeader string) models.PullRequestEventType } type EventParser struct { - GithubUser string - GithubToken string - GitlabUser string - GitlabToken string - BitbucketUser string - BitbucketToken string + GithubUser string + GithubToken string + GitlabUser string + GitlabToken string + BitbucketUser string + BitbucketToken string + BitbucketServerURL string } // GetBitbucketCloudEventType translates the bitbucket header name into a pull // request event type. func (e *EventParser) GetBitbucketCloudEventType(eventTypeHeader string) models.PullRequestEventType { switch eventTypeHeader { - case "pullrequest:created": + case bitbucketcloud.PullCreatedHeader: return models.OpenedPullEvent - case "pullrequest:updated": + case bitbucketcloud.PullUpdatedHeader: return models.UpdatedPullEvent - case "pullrequest:fulfilled", "pullrequest:rejected": + case bitbucketcloud.PullFulfilledHeader, bitbucketcloud.PullRejectedHeader: return models.ClosedPullEvent } return models.OtherPullEvent @@ -156,14 +161,15 @@ func (e *EventParser) ParseBitbucketCloudCommentEvent(body []byte) (pull models. return } if err = validator.New().Struct(event); err != nil { + err = errors.Wrapf(err, "API response %q was missing fields", string(body)) return } - pull, baseRepo, headRepo, user, err = e.parseCommonBitbucketEventData(event.CommonEventData) + pull, baseRepo, headRepo, user, err = e.parseCommonBitbucketCloudEventData(event.CommonEventData) comment = *event.Comment.Content.Raw return } -func (e *EventParser) parseCommonBitbucketEventData(event bitbucketcloud.CommonEventData) (pull models.PullRequest, baseRepo models.Repo, headRepo models.Repo, user models.User, err error) { +func (e *EventParser) parseCommonBitbucketCloudEventData(event bitbucketcloud.CommonEventData) (pull models.PullRequest, baseRepo models.Repo, headRepo models.Repo, user models.User, err error) { var prState models.PullRequestState switch *event.PullRequest.State { case "OPEN": @@ -220,9 +226,10 @@ func (e *EventParser) ParseBitbucketCloudPullEvent(body []byte) (pull models.Pul return } if err = validator.New().Struct(event); err != nil { + err = errors.Wrapf(err, "API response %q was missing fields", string(body)) return } - pull, baseRepo, headRepo, user, err = e.parseCommonBitbucketEventData(event.CommonEventData) + pull, baseRepo, headRepo, user, err = e.parseCommonBitbucketCloudEventData(event.CommonEventData) return } @@ -423,3 +430,97 @@ func (e *EventParser) ParseGitlabMergeRequest(mr *gitlab.MergeRequest, baseRepo BaseRepo: baseRepo, } } + +func (e *EventParser) GetBitbucketServerEventType(eventTypeHeader string) models.PullRequestEventType { + switch eventTypeHeader { + case bitbucketserver.PullCreatedHeader: + return models.OpenedPullEvent + case bitbucketserver.PullFulfilledHeader, bitbucketserver.PullDeclinedHeader: + return models.ClosedPullEvent + } + return models.OtherPullEvent +} + +func (e *EventParser) ParseBitbucketServerCommentEvent(body []byte) (pull models.PullRequest, baseRepo models.Repo, headRepo models.Repo, user models.User, comment string, err error) { + var event bitbucketserver.CommentEvent + if err = json.Unmarshal(body, &event); err != nil { + err = errors.Wrap(err, "parsing json") + return + } + if err = validator.New().Struct(event); err != nil { + err = errors.Wrapf(err, "API response %q was missing fields", string(body)) + return + } + pull, baseRepo, headRepo, user, err = e.parseCommonBitbucketServerEventData(event.CommonEventData) + comment = *event.Comment.Text + return +} + +func (e *EventParser) parseCommonBitbucketServerEventData(event bitbucketserver.CommonEventData) (pull models.PullRequest, baseRepo models.Repo, headRepo models.Repo, user models.User, err error) { + var prState models.PullRequestState + switch *event.PullRequest.State { + case "OPEN": + prState = models.OpenPullState + case "MERGED": + prState = models.ClosedPullState + case "DECLINED": + prState = models.ClosedPullState + default: + err = fmt.Errorf("unable to determine pull request state from %q, this is a bug!", *event.PullRequest.State) + return + } + + headRepoSlug := *event.PullRequest.FromRef.Repository.Slug + headRepoFullname := fmt.Sprintf("%s/%s", *event.PullRequest.FromRef.Repository.Project.Name, headRepoSlug) + headRepoCloneURL := fmt.Sprintf("%s/scm/%s/%s.git", e.BitbucketServerURL, strings.ToLower(*event.PullRequest.FromRef.Repository.Project.Key), headRepoSlug) + headRepo, err = models.NewRepo( + models.BitbucketServer, + headRepoFullname, + headRepoCloneURL, + e.BitbucketUser, + e.BitbucketToken) + if err != nil { + return + } + + baseRepoSlug := *event.PullRequest.ToRef.Repository.Slug + baseRepoFullname := fmt.Sprintf("%s/%s", *event.PullRequest.ToRef.Repository.Project.Name, baseRepoSlug) + baseRepoCloneURL := fmt.Sprintf("%s/scm/%s/%s.git", e.BitbucketServerURL, strings.ToLower(*event.PullRequest.ToRef.Repository.Project.Key), baseRepoSlug) + baseRepo, err = models.NewRepo( + models.BitbucketServer, + baseRepoFullname, + baseRepoCloneURL, + e.BitbucketUser, + e.BitbucketToken) + if err != nil { + return + } + + pull = models.PullRequest{ + Num: *event.PullRequest.ID, + HeadCommit: *event.PullRequest.FromRef.LatestCommit, + URL: fmt.Sprintf("%s/projects/%s/repos/%s/pull-requests/%d", e.BitbucketServerURL, *event.PullRequest.ToRef.Repository.Project.Key, *event.PullRequest.ToRef.Repository.Slug, *event.PullRequest.ID), + Branch: *event.PullRequest.FromRef.DisplayID, + Author: *event.Actor.Username, + State: prState, + BaseRepo: baseRepo, + } + user = models.User{ + Username: *event.Actor.Username, + } + return +} + +func (e *EventParser) ParseBitbucketServerPullEvent(body []byte) (pull models.PullRequest, baseRepo models.Repo, headRepo models.Repo, user models.User, err error) { + var event bitbucketserver.PullRequestEvent + if err = json.Unmarshal(body, &event); err != nil { + err = errors.Wrap(err, "parsing json") + return + } + if err = validator.New().Struct(event); err != nil { + err = errors.Wrapf(err, "API response %q was missing fields", string(body)) + return + } + pull, baseRepo, headRepo, user, err = e.parseCommonBitbucketServerEventData(event.CommonEventData) + return +} diff --git a/server/events/event_parser_test.go b/server/events/event_parser_test.go index fa81ffc88a..68711b4e7d 100644 --- a/server/events/event_parser_test.go +++ b/server/events/event_parser_test.go @@ -526,7 +526,7 @@ func TestParseBitbucketCloudCommentEvent_EmptyString(t *testing.T) { func TestParseBitbucketCloudCommentEvent_EmptyObject(t *testing.T) { _, _, _, _, _, err := parser.ParseBitbucketCloudCommentEvent([]byte("{}")) - ErrEquals(t, "Key: 'CommentEvent.CommonEventData.Actor' Error:Field validation for 'Actor' failed on the 'required' tag\nKey: 'CommentEvent.CommonEventData.Repository' Error:Field validation for 'Repository' failed on the 'required' tag\nKey: 'CommentEvent.CommonEventData.PullRequest' Error:Field validation for 'PullRequest' failed on the 'required' tag\nKey: 'CommentEvent.Comment' Error:Field validation for 'Comment' failed on the 'required' tag", err) + ErrContains(t, "Key: 'CommentEvent.CommonEventData.Actor' Error:Field validation for 'Actor' failed on the 'required' tag\nKey: 'CommentEvent.CommonEventData.Repository' Error:Field validation for 'Repository' failed on the 'required' tag\nKey: 'CommentEvent.CommonEventData.PullRequest' Error:Field validation for 'PullRequest' failed on the 'required' tag\nKey: 'CommentEvent.Comment' Error:Field validation for 'Comment' failed on the 'required' tag", err) } func TestParseBitbucketCloudCommentEvent_CommitHashMissing(t *testing.T) { @@ -537,7 +537,7 @@ func TestParseBitbucketCloudCommentEvent_CommitHashMissing(t *testing.T) { } emptyCommitHash := strings.Replace(string(bytes), ` "hash": "e0624da46d3a",`, "", -1) _, _, _, _, _, err = parser.ParseBitbucketCloudCommentEvent([]byte(emptyCommitHash)) - ErrEquals(t, "Key: 'CommentEvent.CommonEventData.PullRequest.Source.Commit.Hash' Error:Field validation for 'Hash' failed on the 'required' tag", err) + ErrContains(t, "Key: 'CommentEvent.CommonEventData.PullRequest.Source.Commit.Hash' Error:Field validation for 'Hash' failed on the 'required' tag", err) } func TestParseBitbucketCloudCommentEvent_ValidEvent(t *testing.T) { diff --git a/server/events/mocks/mock_event_parsing.go b/server/events/mocks/mock_event_parsing.go index 9395fb8486..b31962f7d9 100644 --- a/server/events/mocks/mock_event_parsing.go +++ b/server/events/mocks/mock_event_parsing.go @@ -256,6 +256,78 @@ func (mock *MockEventParsing) GetBitbucketCloudEventType(eventTypeHeader string) return ret0 } +func (mock *MockEventParsing) ParseBitbucketServerPullEvent(body []byte) (models.PullRequest, models.Repo, models.Repo, models.User, error) { + params := []pegomock.Param{body} + result := pegomock.GetGenericMockFrom(mock).Invoke("ParseBitbucketServerPullEvent", params, []reflect.Type{reflect.TypeOf((*models.PullRequest)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.User)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 models.PullRequest + var ret1 models.Repo + var ret2 models.Repo + var ret3 models.User + var ret4 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(models.PullRequest) + } + if result[1] != nil { + ret1 = result[1].(models.Repo) + } + if result[2] != nil { + ret2 = result[2].(models.Repo) + } + if result[3] != nil { + ret3 = result[3].(models.User) + } + if result[4] != nil { + ret4 = result[4].(error) + } + } + return ret0, ret1, ret2, ret3, ret4 +} + +func (mock *MockEventParsing) ParseBitbucketServerCommentEvent(body []byte) (models.PullRequest, models.Repo, models.Repo, models.User, string, error) { + params := []pegomock.Param{body} + result := pegomock.GetGenericMockFrom(mock).Invoke("ParseBitbucketServerCommentEvent", params, []reflect.Type{reflect.TypeOf((*models.PullRequest)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.User)(nil)).Elem(), reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 models.PullRequest + var ret1 models.Repo + var ret2 models.Repo + var ret3 models.User + var ret4 string + var ret5 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(models.PullRequest) + } + if result[1] != nil { + ret1 = result[1].(models.Repo) + } + if result[2] != nil { + ret2 = result[2].(models.Repo) + } + if result[3] != nil { + ret3 = result[3].(models.User) + } + if result[4] != nil { + ret4 = result[4].(string) + } + if result[5] != nil { + ret5 = result[5].(error) + } + } + return ret0, ret1, ret2, ret3, ret4, ret5 +} + +func (mock *MockEventParsing) GetBitbucketServerEventType(eventTypeHeader string) models.PullRequestEventType { + params := []pegomock.Param{eventTypeHeader} + result := pegomock.GetGenericMockFrom(mock).Invoke("GetBitbucketServerEventType", params, []reflect.Type{reflect.TypeOf((*models.PullRequestEventType)(nil)).Elem()}) + var ret0 models.PullRequestEventType + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(models.PullRequestEventType) + } + } + return ret0 +} + func (mock *MockEventParsing) VerifyWasCalledOnce() *VerifierEventParsing { return &VerifierEventParsing{mock, pegomock.Times(1), nil} } @@ -547,3 +619,84 @@ func (c *EventParsing_GetBitbucketCloudEventType_OngoingVerification) GetAllCapt } return } + +func (verifier *VerifierEventParsing) ParseBitbucketServerPullEvent(body []byte) *EventParsing_ParseBitbucketServerPullEvent_OngoingVerification { + params := []pegomock.Param{body} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ParseBitbucketServerPullEvent", params) + return &EventParsing_ParseBitbucketServerPullEvent_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type EventParsing_ParseBitbucketServerPullEvent_OngoingVerification struct { + mock *MockEventParsing + methodInvocations []pegomock.MethodInvocation +} + +func (c *EventParsing_ParseBitbucketServerPullEvent_OngoingVerification) GetCapturedArguments() []byte { + body := c.GetAllCapturedArguments() + return body[len(body)-1] +} + +func (c *EventParsing_ParseBitbucketServerPullEvent_OngoingVerification) GetAllCapturedArguments() (_param0 [][]byte) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([][]byte, len(params[0])) + for u, param := range params[0] { + _param0[u] = param.([]byte) + } + } + return +} + +func (verifier *VerifierEventParsing) ParseBitbucketServerCommentEvent(body []byte) *EventParsing_ParseBitbucketServerCommentEvent_OngoingVerification { + params := []pegomock.Param{body} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ParseBitbucketServerCommentEvent", params) + return &EventParsing_ParseBitbucketServerCommentEvent_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type EventParsing_ParseBitbucketServerCommentEvent_OngoingVerification struct { + mock *MockEventParsing + methodInvocations []pegomock.MethodInvocation +} + +func (c *EventParsing_ParseBitbucketServerCommentEvent_OngoingVerification) GetCapturedArguments() []byte { + body := c.GetAllCapturedArguments() + return body[len(body)-1] +} + +func (c *EventParsing_ParseBitbucketServerCommentEvent_OngoingVerification) GetAllCapturedArguments() (_param0 [][]byte) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([][]byte, len(params[0])) + for u, param := range params[0] { + _param0[u] = param.([]byte) + } + } + return +} + +func (verifier *VerifierEventParsing) GetBitbucketServerEventType(eventTypeHeader string) *EventParsing_GetBitbucketServerEventType_OngoingVerification { + params := []pegomock.Param{eventTypeHeader} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GetBitbucketServerEventType", params) + return &EventParsing_GetBitbucketServerEventType_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type EventParsing_GetBitbucketServerEventType_OngoingVerification struct { + mock *MockEventParsing + methodInvocations []pegomock.MethodInvocation +} + +func (c *EventParsing_GetBitbucketServerEventType_OngoingVerification) GetCapturedArguments() string { + eventTypeHeader := c.GetAllCapturedArguments() + return eventTypeHeader[len(eventTypeHeader)-1] +} + +func (c *EventParsing_GetBitbucketServerEventType_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]string, len(params[0])) + for u, param := range params[0] { + _param0[u] = param.(string) + } + } + return +} diff --git a/server/events/models/models.go b/server/events/models/models.go index ee2d327f81..99c090cfb0 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -63,14 +63,20 @@ func NewRepo(vcsHostType VCSHostType, repoFullName string, cloneURL string, vcsU cloneURL += ".git" } - // Ensure the Clone URL is for the same repo to avoid something malicious. cloneURLParsed, err := url.Parse(cloneURL) if err != nil { return Repo{}, errors.Wrap(err, "invalid clone url") } - expClonePath := fmt.Sprintf("/%s.git", repoFullName) - if expClonePath != cloneURLParsed.Path { - return Repo{}, fmt.Errorf("expected clone url to have path %q but had %q", expClonePath, cloneURLParsed.Path) + + // Ensure the Clone URL is for the same repo to avoid something malicious. + // We skip this check for Bitbucket Server because its format is different + // and because the caller in that case actually constructs the clone url + // from the repo name and so there's no point checking if they match. + if vcsHostType != BitbucketServer { + expClonePath := fmt.Sprintf("/%s.git", repoFullName) + if expClonePath != cloneURLParsed.Path { + return Repo{}, fmt.Errorf("expected clone url to have path %q but had %q", expClonePath, cloneURLParsed.Path) + } } // Construct clone urls with http auth. Need to do both https and http @@ -108,9 +114,9 @@ type PullRequest struct { // Num is the pull request number or ID. Num int // HeadCommit is a sha256 that points to the head of the branch that is being - // pull requested into the base. If the pull request is from BitBucket, - // the string will only be 12 characters long because BitBucket truncates - // its commit IDs. + // pull requested into the base. If the pull request is from Bitbucket Cloud + // the string will only be 12 characters long because Bitbucket Cloud + // truncates its commit IDs. HeadCommit string // URL is the url of the pull request. // ex. "https://github.com/runatlantis/atlantis/pull/1" diff --git a/server/events/testdata/bitbucket-server-get-pull-changes.json b/server/events/testdata/bitbucket-server-get-pull-changes.json new file mode 100644 index 0000000000..72681f8883 --- /dev/null +++ b/server/events/testdata/bitbucket-server-get-pull-changes.json @@ -0,0 +1,66 @@ +{ + "fromHash": "bbc7b2a29344646ec8605be9603a0aa625a627ef", + "toHash": "737bfd254f39b36733ee2d5089db65c7369c7692", + "properties": { + "changeScope": "ALL" + }, + "values": [ + { + "contentId": "fdc4b3b5b37ba11f0cbec7a2744cddb35bab785b", + "fromContentId": "0000000000000000000000000000000000000000", + "path": { + "components": [ + "folder", + "another.tf" + ], + "parent": "folder", + "name": "another.tf", + "extension": "tf", + "toString": "folder/another.tf" + }, + "executable": false, + "percentUnchanged": -1, + "type": "ADD", + "nodeType": "FILE", + "links": { + "self": [ + null + ] + }, + "properties": { + "gitChangeType": "ADD" + } + }, + { + "contentId": "a44a3e1d545c78dc67236af92395b864a7035498", + "fromContentId": "dc333454be3243d54ff155489df0bc0e94807a35", + "path": { + "components": [ + "main.tf" + ], + "parent": "", + "name": "main.tf", + "extension": "tf", + "toString": "main.tf" + }, + "executable": false, + "percentUnchanged": -1, + "type": "MODIFY", + "nodeType": "FILE", + "srcExecutable": false, + "links": { + "self": [ + null + ] + }, + "properties": { + "gitChangeType": "MODIFY" + } + } + ], + "size": 2, + "isLastPage": true, + "start": 0, + "limit": 25, + "nextPageStart": null +} \ No newline at end of file diff --git a/server/events/testdata/bitbucket-server-get-pull.json b/server/events/testdata/bitbucket-server-get-pull.json new file mode 100644 index 0000000000..c713635884 --- /dev/null +++ b/server/events/testdata/bitbucket-server-get-pull.json @@ -0,0 +1,156 @@ +{ + "id": 3, + "version": 0, + "title": "main.tf edited online with Bitbucket", + "state": "OPEN", + "open": true, + "closed": false, + "createdDate": 1532350340104, + "updatedDate": 1532350340104, + "fromRef": { + "id": "refs/heads/lkysow/maintf-1532350335286", + "displayId": "lkysow/maintf-1532350335286", + "latestCommit": "43b60c668d138b2070bb6a746e09ef513e51a891", + "repository": { + "slug": "atlantis-example", + "id": 1, + "name": "atlantis-example", + "scmId": "git", + "state": "AVAILABLE", + "statusMessage": "Available", + "forkable": true, + "project": { + "key": "AT", + "id": 1, + "name": "atlantis", + "public": false, + "type": "NORMAL", + "links": { + "self": [ + { + "href": "http://localhost:7990/projects/AT" + } + ] + } + }, + "public": false, + "links": { + "clone": [ + { + "href": "http://localhost:7990/scm/at/atlantis-example.git", + "name": "http" + }, + { + "href": "ssh://git@localhost:7999/at/atlantis-example.git", + "name": "ssh" + } + ], + "self": [ + { + "href": "http://localhost:7990/projects/AT/repos/atlantis-example/browse" + } + ] + } + } + }, + "toRef": { + "id": "refs/heads/master", + "displayId": "master", + "latestCommit": "bbc7b2a29344646ec8605be9603a0aa625a627ef", + "repository": { + "slug": "atlantis-example", + "id": 1, + "name": "atlantis-example", + "scmId": "git", + "state": "AVAILABLE", + "statusMessage": "Available", + "forkable": true, + "project": { + "key": "AT", + "id": 1, + "name": "atlantis", + "public": false, + "type": "NORMAL", + "links": { + "self": [ + { + "href": "http://localhost:7990/projects/AT" + } + ] + } + }, + "public": false, + "links": { + "clone": [ + { + "href": "http://localhost:7990/scm/at/atlantis-example.git", + "name": "http" + }, + { + "href": "ssh://git@localhost:7999/at/atlantis-example.git", + "name": "ssh" + } + ], + "self": [ + { + "href": "http://localhost:7990/projects/AT/repos/atlantis-example/browse" + } + ] + } + } + }, + "locked": false, + "author": { + "user": { + "name": "lkysow", + "emailAddress": "lkysow@gmail.com", + "id": 1, + "displayName": "Luke Kysow", + "active": true, + "slug": "lkysow", + "type": "NORMAL", + "links": { + "self": [ + { + "href": "http://localhost:7990/users/lkysow" + } + ] + } + }, + "role": "AUTHOR", + "approved": false, + "status": "UNAPPROVED" + }, + "reviewers": [ + { + "user": { + "name": "another-user", + "emailAddress": "lkysow@gmail.com", + "id": 2, + "displayName": "another-user", + "active": true, + "slug": "another-user", + "type": "NORMAL", + "links": { + "self": [ + { + "href": "http://localhost:7990/users/another-user" + } + ] + } + }, + "lastReviewedCommit": "43b60c668d138b2070bb6a746e09ef513e51a891", + "role": "REVIEWER", + "approved": true, + "status": "APPROVED" + } + ], + "participants": [], + "links": { + "self": [ + { + "href": "http://localhost:7990/projects/AT/repos/atlantis-example/pull-requests/3" + } + ] + } +} \ No newline at end of file diff --git a/server/events/testdata/bitbucket-server-pull-event-declined.json b/server/events/testdata/bitbucket-server-pull-event-declined.json new file mode 100644 index 0000000000..2ed88b9a93 --- /dev/null +++ b/server/events/testdata/bitbucket-server-pull-event-declined.json @@ -0,0 +1,85 @@ +{ + "eventKey": "pr:declined", + "date": "2018-07-23T13:59:48+0200", + "actor": { + "name": "lkysow", + "emailAddress": "lkysow@gmail.com", + "id": 1, + "displayName": "Luke Kysow", + "active": true, + "slug": "lkysow", + "type": "NORMAL" + }, + "pullRequest": { + "id": 1, + "version": 7, + "title": "Null resource2", + "state": "DECLINED", + "open": false, + "closed": true, + "createdDate": 1532207977313, + "updatedDate": 1532347188162, + "closedDate": 1532347188162, + "fromRef": { + "id": "refs/heads/branch", + "displayId": "branch", + "latestCommit": "46955afd9b6c5dfa8753727d0669925e057e69b1", + "repository": { + "slug": "atlantis-example", + "id": 1, + "name": "atlantis-example", + "scmId": "git", + "state": "AVAILABLE", + "statusMessage": "Available", + "forkable": true, + "project": { + "key": "AT", + "id": 1, + "name": "atlantis", + "public": false, + "type": "NORMAL" + }, + "public": false + } + }, + "toRef": { + "id": "refs/heads/master", + "displayId": "master", + "latestCommit": "120cff6e1452086c90689c810c15b534381ba61b", + "repository": { + "slug": "atlantis-example", + "id": 1, + "name": "atlantis-example", + "scmId": "git", + "state": "AVAILABLE", + "statusMessage": "Available", + "forkable": true, + "project": { + "key": "AT", + "id": 1, + "name": "atlantis", + "public": false, + "type": "NORMAL" + }, + "public": false + } + }, + "locked": false, + "author": { + "user": { + "name": "lkysow", + "emailAddress": "lkysow@gmail.com", + "id": 1, + "displayName": "Luke Kysow", + "active": true, + "slug": "lkysow", + "type": "NORMAL" + }, + "role": "AUTHOR", + "approved": false, + "status": "UNAPPROVED" + }, + "reviewers": [], + "participants": [] + } +} diff --git a/server/events/testdata/bitbucket-server-pull-event-merged.json b/server/events/testdata/bitbucket-server-pull-event-merged.json new file mode 100644 index 0000000000..561b847792 --- /dev/null +++ b/server/events/testdata/bitbucket-server-pull-event-merged.json @@ -0,0 +1,117 @@ +{ + "eventKey": "pr:merged", + "date": "2018-07-23T14:00:19+0200", + "actor": { + "name": "lkysow", + "emailAddress": "lkysow@gmail.com", + "id": 1, + "displayName": "Luke Kysow", + "active": true, + "slug": "lkysow", + "type": "NORMAL" + }, + "pullRequest": { + "id": 2, + "version": 2, + "title": "Branch", + "description": "* Null resource\r\n* main.tf edited online with Bitbucket\r\n* Update 2\r\n* main.tf edited online with Bitbucket\r\n* kkj\r\n* main.tf edited online with Bitbucket", + "state": "MERGED", + "open": false, + "closed": true, + "createdDate": 1532211497403, + "updatedDate": 1532347219220, + "closedDate": 1532347219220, + "fromRef": { + "id": "refs/heads/branch", + "displayId": "branch", + "latestCommit": "86a574157f5a2dadaf595b9f06c70fdfdd039912", + "repository": { + "slug": "atlantis-example-fork", + "id": 2, + "name": "atlantis-example-fork", + "scmId": "git", + "state": "AVAILABLE", + "statusMessage": "Available", + "forkable": true, + "origin": { + "slug": "atlantis-example", + "id": 1, + "name": "atlantis-example", + "scmId": "git", + "state": "AVAILABLE", + "statusMessage": "Available", + "forkable": true, + "project": { + "key": "AT", + "id": 1, + "name": "atlantis", + "public": false, + "type": "NORMAL" + }, + "public": false + }, + "project": { + "key": "~LKYSOW", + "id": 2, + "name": "Luke Kysow", + "type": "PERSONAL", + "owner": { + "name": "lkysow", + "emailAddress": "lkysow@gmail.com", + "id": 1, + "displayName": "Luke Kysow", + "active": true, + "slug": "lkysow", + "type": "NORMAL" + } + }, + "public": false + } + }, + "toRef": { + "id": "refs/heads/master", + "displayId": "master", + "latestCommit": "120cff6e1452086c90689c810c15b534381ba61b", + "repository": { + "slug": "atlantis-example", + "id": 1, + "name": "atlantis-example", + "scmId": "git", + "state": "AVAILABLE", + "statusMessage": "Available", + "forkable": true, + "project": { + "key": "AT", + "id": 1, + "name": "atlantis", + "public": false, + "type": "NORMAL" + }, + "public": false + } + }, + "locked": false, + "author": { + "user": { + "name": "lkysow", + "emailAddress": "lkysow@gmail.com", + "id": 1, + "displayName": "Luke Kysow", + "active": true, + "slug": "lkysow", + "type": "NORMAL" + }, + "role": "AUTHOR", + "approved": false, + "status": "UNAPPROVED" + }, + "reviewers": [], + "participants": [], + "properties": { + "mergeCommit": { + "displayId": "bbc7b2a2934", + "id": "bbc7b2a29344646ec8605be9603a0aa625a627ef" + } + } + } +} diff --git a/server/events/vcs/bitbucketcloud/bitbucketcloud.go b/server/events/vcs/bitbucketcloud/bitbucketcloud.go index 267a723a9c..91efa0e5cf 100644 --- a/server/events/vcs/bitbucketcloud/bitbucketcloud.go +++ b/server/events/vcs/bitbucketcloud/bitbucketcloud.go @@ -2,3 +2,5 @@ // It is separate from bitbucketserver because Bitbucket Server has different // APIs. package bitbucketcloud + +const Hostname = "bitbucket.org" diff --git a/server/events/vcs/bitbucketcloud/client.go b/server/events/vcs/bitbucketcloud/client.go index 66fcd21ea4..71b8a5f13d 100644 --- a/server/events/vcs/bitbucketcloud/client.go +++ b/server/events/vcs/bitbucketcloud/client.go @@ -7,18 +7,14 @@ import ( "io" "io/ioutil" "net/http" - "net/url" "github.com/pkg/errors" "github.com/runatlantis/atlantis/server/events/models" + "gopkg.in/go-playground/validator.v9" ) const ( - // The comments API is only available in the 1.0 API right now. - commentsAPIPathFmt = "%s/1.0/repositories/%s/pullrequests/%d/comments" - diffStatAPIPathFmt = "%s/2.0/repositories/%s/pullrequests/%d/diffstat" - pullApprovedAPIPathFmt = "%s/2.0/repositories/%s/pullrequests/%d" - buildStatusAPIPathFmt = "%s/2.0/repositories/%s/commit/%s/statuses/build" + APIBaseURL = "https://api.bitbucket.org" ) type Client struct { @@ -29,31 +25,21 @@ type Client struct { AtlantisURL string } -// NewClient builds a bitbucket cloud client. Returns an error if the baseURL is -// malformed. httpClient is the client to use to make the requests, username -// and password are used as basic auth in the requests, baseURL is the API's -// baseURL, ex. https://api.bitbucket.org. Don't include the API version, ex. -// '/1.0' since that changes based on the API call. atlantisURL is the +// NewClient builds a bitbucket 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, baseURL string, atlantisURL string) (*Client, error) { +func NewClient(httpClient *http.Client, username string, password string, atlantisURL string) *Client { if httpClient == nil { httpClient = http.DefaultClient } - // Remove the trailing '/' from the URL. - parsedURL, err := url.Parse(baseURL) - if err != nil { - return nil, errors.Wrapf(err, "parsing %s", baseURL) - } - urlWithoutPath := fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host) return &Client{ HttpClient: httpClient, Username: username, Password: password, - BaseURL: urlWithoutPath, + BaseURL: APIBaseURL, AtlantisURL: atlantisURL, - }, nil + } } // GetModifiedFiles returns the names of files that were modified in the merge request. @@ -61,9 +47,9 @@ func NewClient(httpClient *http.Client, username string, password string, baseUR func (b *Client) GetModifiedFiles(repo models.Repo, pull models.PullRequest) ([]string, error) { var files []string - nextPageURL := fmt.Sprintf(diffStatAPIPathFmt, b.BaseURL, repo.FullName, pull.Num) - // We'll only loop 100 times as a safety measure. - maxLoops := 100 + 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 { @@ -73,6 +59,9 @@ func (b *Client) GetModifiedFiles(repo models.Repo, pull models.PullRequest) ([] if err := json.Unmarshal(resp, &diffStat); err != nil { return nil, err } + 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) @@ -105,14 +94,14 @@ func (b *Client) CreateComment(repo models.Repo, pullNum int, comment string) er if err != nil { return errors.Wrap(err, "json encoding") } - path := fmt.Sprintf(commentsAPIPathFmt, b.BaseURL, repo.FullName, pullNum) + path := fmt.Sprintf("%s/1.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(pullApprovedAPIPathFmt, b.BaseURL, repo.FullName, pull.Num) + 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 @@ -121,6 +110,9 @@ func (b *Client) PullIsApproved(repo models.Repo, pull models.PullRequest) (bool if err := json.Unmarshal(resp, &pullResp); err != nil { return false, err } + 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 { if *participant.Approved { return true, nil @@ -148,7 +140,7 @@ func (b *Client) UpdateStatus(repo models.Repo, pull models.PullRequest, status "description": description, }) - path := fmt.Sprintf(buildStatusAPIPathFmt, b.BaseURL, repo.FullName, pull.HeadCommit) + 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") } diff --git a/server/events/vcs/bitbucketcloud/client_test.go b/server/events/vcs/bitbucketcloud/client_test.go index 283632069f..3baa512f45 100644 --- a/server/events/vcs/bitbucketcloud/client_test.go +++ b/server/events/vcs/bitbucketcloud/client_test.go @@ -68,8 +68,8 @@ func TestClient_GetModifiedFilesPagination(t *testing.T) { defer testServer.Close() serverURL = testServer.URL - client, err := bitbucketcloud.NewClient(http.DefaultClient, "user", "pass", serverURL, "runatlantis.io") - Ok(t, err) + client := bitbucketcloud.NewClient(http.DefaultClient, "user", "pass", "runatlantis.io") + client.BaseURL = testServer.URL files, err := client.GetModifiedFiles(models.Repo{ FullName: "owner/repo", @@ -129,8 +129,8 @@ func TestClient_GetModifiedFilesOldNil(t *testing.T) { })) defer testServer.Close() - client, err := bitbucketcloud.NewClient(http.DefaultClient, "user", "pass", testServer.URL, "runatlantis.io") - Ok(t, err) + client := bitbucketcloud.NewClient(http.DefaultClient, "user", "pass", "runatlantis.io") + client.BaseURL = testServer.URL files, err := client.GetModifiedFiles(models.Repo{ FullName: "owner/repo", diff --git a/server/events/vcs/bitbucketcloud/models.go b/server/events/vcs/bitbucketcloud/models.go index f53f589422..270cfca2b4 100644 --- a/server/events/vcs/bitbucketcloud/models.go +++ b/server/events/vcs/bitbucketcloud/models.go @@ -1,5 +1,13 @@ package bitbucketcloud +const ( + PullCreatedHeader = "pullrequest:created" + PullUpdatedHeader = "pullrequest:updated" + PullFulfilledHeader = "pullrequest:fulfilled" + PullRejectedHeader = "pullrequest:rejected" + PullCommentCreatedHeader = "pullrequest:comment_created" +) + type CommentEvent struct { CommonEventData Comment *Comment `json:"comment,omitempty" validate:"required"` diff --git a/server/events/vcs/bitbucketserver/bitbucketserver.go b/server/events/vcs/bitbucketserver/bitbucketserver.go new file mode 100644 index 0000000000..3a706d7551 --- /dev/null +++ b/server/events/vcs/bitbucketserver/bitbucketserver.go @@ -0,0 +1 @@ +package bitbucketserver diff --git a/server/events/vcs/bitbucketserver/client.go b/server/events/vcs/bitbucketserver/client.go new file mode 100644 index 0000000000..01efcf36aa --- /dev/null +++ b/server/events/vcs/bitbucketserver/client.go @@ -0,0 +1,222 @@ +package bitbucketserver + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "regexp" + "strings" + + "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 bitbucket cloud client. Returns an error if the baseURL is +// malformed. httpClient is the client to use to make the requests, username +// and password are used as basic auth in the requests, baseURL is the API's +// baseURL, ex. https://corp.com:7990. Don't include the API version, ex. +// '/1.0' since that changes based on the API call. 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, baseURL string, atlantisURL string) (*Client, error) { + if httpClient == nil { + httpClient = http.DefaultClient + } + // Remove the trailing '/' from the URL. + parsedURL, err := url.Parse(baseURL) + if err != nil { + return nil, errors.Wrapf(err, "parsing %s", baseURL) + } + if parsedURL.Scheme == "" { + return nil, fmt.Errorf("must have 'http://' or 'https://' in base url %q", baseURL) + } + urlWithoutPath := fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host) + return &Client{ + HttpClient: httpClient, + Username: username, + Password: password, + BaseURL: urlWithoutPath, + AtlantisURL: atlantisURL, + }, nil +} + +// 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 + + projectKey, err := b.GetProjectKey(repo.Name, repo.SanitizedCloneURL) + if err != nil { + return nil, err + } + nextPageStart := "0" + baseURL := fmt.Sprintf("%s/rest/api/1.0/projects/%s/repos/%s/pull-requests/%d/changes", + b.BaseURL, projectKey, repo.Name, 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", fmt.Sprintf("%s?start=%s", baseURL, nextPageStart), nil) + if err != nil { + return nil, err + } + var changes Changes + if err := json.Unmarshal(resp, &changes); err != nil { + return nil, err + } + if err := validator.New().Struct(changes); err != nil { + return nil, errors.Wrapf(err, "API response %q was missing fields", string(resp)) + } + for _, v := range changes.Values { + files = append(files, *v.Path.ToString) + } + if *changes.IsLastPage { + break + } + nextPageStart = *changes.NextPageStart + } + + // 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 +} + +func (b *Client) GetProjectKey(repoName string, cloneURL string) (string, error) { + // Get the project key out of the repo clone URL. + // Given http://bitbucket.corp:7990/scm/at/atlantis-example.git + // we want to get 'at'. + expr := fmt.Sprintf(".*/(.*?)/%s\\.git", repoName) + capture, err := regexp.Compile(expr) + if err != nil { + return "", errors.Wrapf(err, "constructing regex from %q", expr) + } + matches := capture.FindStringSubmatch(cloneURL) + if len(matches) != 2 { + return "", fmt.Errorf("could not extract project key from %q, regex returned %q", cloneURL, strings.Join(matches, ",")) + } + return matches[1], nil +} + +// CreateComment creates a comment on the merge request. +func (b *Client) CreateComment(repo models.Repo, pullNum int, comment string) error { + bodyBytes, err := json.Marshal(map[string]string{"text": comment}) + if err != nil { + return errors.Wrap(err, "json encoding") + } + projectKey, err := b.GetProjectKey(repo.Name, repo.SanitizedCloneURL) + if err != nil { + return err + } + path := fmt.Sprintf("%s/rest/api/1.0/projects/%s/repos/%s/pull-requests/%d/comments", b.BaseURL, projectKey, repo.Name, 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) { + projectKey, err := b.GetProjectKey(repo.Name, repo.SanitizedCloneURL) + if err != nil { + return false, err + } + path := fmt.Sprintf("%s/rest/api/1.0/projects/%s/repos/%s/pull-requests/%d", b.BaseURL, projectKey, repo.Name, 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, err + } + if err := validator.New().Struct(pullResp); err != nil { + return false, errors.Wrapf(err, "API response %q was missing fields", string(resp)) + } + for _, reviewer := range pullResp.Reviewers { + if *reviewer.Approved { + 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, description string) error { + bbState := "FAILED" + switch status { + case models.PendingCommitStatus: + bbState = "INPROGRESS" + case models.SuccessCommitStatus: + bbState = "SUCCESSFUL" + case models.FailedCommitStatus: + bbState = "FAILED" + } + + bodyBytes, err := json.Marshal(map[string]string{ + "key": "atlantis", + "url": b.AtlantisURL, + "state": bbState, + "description": description, + }) + + path := fmt.Sprintf("%s/rest/build-status/1.0/commits/%s", b.BaseURL, pull.HeadCommit) + if err != nil { + return errors.Wrap(err, "json encoding") + } + _, err = b.makeRequest("POST", path, bytes.NewBuffer(bodyBytes)) + return err +} + +// prepRequest adds the HTTP basic auth. +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) + 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") + } + if reqBody != nil { + req.Header.Add("Content-Type", "application/json") + } + 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 && resp.StatusCode != 204 { + 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/bitbucketserver/models.go b/server/events/vcs/bitbucketserver/models.go new file mode 100644 index 0000000000..85e1cad699 --- /dev/null +++ b/server/events/vcs/bitbucketserver/models.go @@ -0,0 +1,66 @@ +package bitbucketserver + +const ( + PullCreatedHeader = "pr:opened" + PullFulfilledHeader = "pr:merged" + PullDeclinedHeader = "pr:declined" + PullCommentCreatedHeader = "pr:comment:added" +) + +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"` + PullRequest *PullRequest `json:"pullRequest,omitempty" validate:"required"` +} + +type PullRequest struct { + ID *int `json:"id,omitempty" validate:"required"` + FromRef *Ref `json:"fromRef,omitempty" validate:"required"` + ToRef *Ref `json:"toRef,omitempty" validate:"required"` + State *string `json:"state,omitempty" validate:"required"` + Reviewers []struct { + Approved *bool `json:"approved,omitempty" validate:"required"` + } `json:"reviewers,omitempty" validate:"required"` +} + +type Ref struct { + Repository *Repository `json:"repository,omitempty" validate:"required"` + DisplayID *string `json:"displayId,omitempty" validate:"required"` + LatestCommit *string `json:"latestCommit,omitempty" validate:"required"` +} + +type Repository struct { + Slug *string `json:"slug,omitempty" validate:"required"` + Project *Project `json:"project,omitempty" validate:"required"` +} + +type Project struct { + Name *string `json:"name,omitempty" validate:"required"` + Key *string `json:"key,omitempty" validate:"required"` +} + +type Actor struct { + Username *string `json:"name,omitempty" validate:"required"` +} + +type Comment struct { + Text *string `json:"text,omitempty" validate:"required"` +} + +type Changes struct { + Values []struct { + Path struct { + ToString *string `json:"toString,omitempty" validate:"required"` + } `json:"path,omitempty" validate:"required"` + } `json:"values,omitempty" validate:"required"` + NextPageStart *string `json:"nextPageStart,omitempty"` + IsLastPage *bool `json:"isLastPage,omitempty" validate:"required"` +} diff --git a/server/events/vcs/vcs.go b/server/events/vcs/vcs.go new file mode 100644 index 0000000000..547b1da48b --- /dev/null +++ b/server/events/vcs/vcs.go @@ -0,0 +1 @@ +package vcs diff --git a/server/events_controller.go b/server/events_controller.go index 4c376842ff..9f9059b02f 100644 --- a/server/events_controller.go +++ b/server/events_controller.go @@ -23,6 +23,8 @@ import ( "github.com/runatlantis/atlantis/server/events" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/events/vcs" + "github.com/runatlantis/atlantis/server/events/vcs/bitbucketcloud" + "github.com/runatlantis/atlantis/server/events/vcs/bitbucketserver" "github.com/runatlantis/atlantis/server/logging" ) @@ -79,13 +81,25 @@ func (e *EventsController) Post(w http.ResponseWriter, r *http.Request) { e.handleGitlabPost(w, r) return } else if r.Header.Get(bitbucketEventTypeHeader) != "" { - if !e.supportsHost(models.BitbucketCloud) { - e.respond(w, logging.Debug, http.StatusBadRequest, "Ignoring request since not configured to support Bitbucket") + // Bitbucket Cloud and Server use the same event type header but they + // use different request ID headers. + if r.Header.Get(bitbucketCloudRequestIDHeader) != "" { + if !e.supportsHost(models.BitbucketCloud) { + e.respond(w, logging.Debug, http.StatusBadRequest, "Ignoring request since not configured to support Bitbucket Cloud") + return + } + e.Logger.Debug("handling Bitbucket Cloud post") + e.handleBitbucketCloudPost(w, r) + return + } else if r.Header.Get(bitbucketServerRequestIDHeader) != "" { + if !e.supportsHost(models.BitbucketServer) { + e.respond(w, logging.Debug, http.StatusBadRequest, "Ignoring request since not configured to support Bitbucket Server") + return + } + e.Logger.Debug("handling Bitbucket Server post") + e.handleBitbucketServerPost(w, r) return } - e.Logger.Debug("handling Bitbucket post") - e.handleBitbucketPost(w, r) - return } e.respond(w, logging.Debug, http.StatusBadRequest, "Ignoring request") } @@ -113,7 +127,7 @@ func (e *EventsController) handleGithubPost(w http.ResponseWriter, r *http.Reque } } -func (e *EventsController) handleBitbucketPost(w http.ResponseWriter, r *http.Request) { +func (e *EventsController) handleBitbucketCloudPost(w http.ResponseWriter, r *http.Request) { eventType := r.Header.Get(bitbucketEventTypeHeader) reqID := r.Header.Get(bitbucketCloudRequestIDHeader) defer r.Body.Close() // nolint: errcheck @@ -123,19 +137,42 @@ func (e *EventsController) handleBitbucketPost(w http.ResponseWriter, r *http.Re return } switch eventType { - case "pullrequest:created", "pullrequest:updated", "pullrequest:fulfilled", "pullrequest:rejected": + case bitbucketcloud.PullCreatedHeader, bitbucketcloud.PullUpdatedHeader, bitbucketcloud.PullFulfilledHeader, bitbucketcloud.PullRejectedHeader: e.Logger.Debug("handling as pull request state changed event") - e.HandleBitbucketPullRequestEvent(w, eventType, body, reqID) + e.HandleBitbucketCloudPullRequestEvent(w, eventType, body, reqID) return - case "pullrequest:comment_created": + case bitbucketcloud.PullCommentCreatedHeader: e.Logger.Debug("handling as comment created event") - e.HandleBitbucketCommentEvent(w, body, reqID) + e.HandleBitbucketCloudCommentEvent(w, body, reqID) return default: e.respond(w, logging.Debug, http.StatusOK, "Ignoring unsupported event type %s %s=%s", eventType, bitbucketCloudRequestIDHeader, reqID) } } +func (e *EventsController) handleBitbucketServerPost(w http.ResponseWriter, r *http.Request) { + eventType := r.Header.Get(bitbucketEventTypeHeader) + reqID := r.Header.Get(bitbucketServerRequestIDHeader) + defer r.Body.Close() // nolint: errcheck + body, err := ioutil.ReadAll(r.Body) + if err != nil { + e.respond(w, logging.Error, http.StatusBadRequest, "Unable to read body: %s %s=%s", err, bitbucketServerRequestIDHeader, reqID) + return + } + switch eventType { + case bitbucketserver.PullCreatedHeader, bitbucketserver.PullFulfilledHeader, bitbucketserver.PullDeclinedHeader: + e.Logger.Debug("handling as pull request state changed event") + e.HandleBitbucketServerPullRequestEvent(w, eventType, body, reqID) + return + case bitbucketserver.PullCommentCreatedHeader: + e.Logger.Debug("handling as comment created event") + e.HandleBitbucketServerCommentEvent(w, body, reqID) + return + default: + e.respond(w, logging.Debug, http.StatusOK, "Ignoring unsupported event type %s %s=%s", eventType, bitbucketServerRequestIDHeader, reqID) + } +} + // 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) { @@ -155,8 +192,8 @@ func (e *EventsController) HandleGithubCommentEvent(w http.ResponseWriter, event e.handleCommentEvent(w, baseRepo, nil, nil, user, pullNum, event.Comment.GetBody(), models.Github) } -// HandleBitbucketCommentEvent handles comment events from Bitbucket. -func (e *EventsController) HandleBitbucketCommentEvent(w http.ResponseWriter, body []byte, reqID string) { +// HandleBitbucketCloudCommentEvent handles comment events from Bitbucket. +func (e *EventsController) HandleBitbucketCloudCommentEvent(w http.ResponseWriter, body []byte, reqID string) { pull, baseRepo, headRepo, user, comment, err := e.Parser.ParseBitbucketCloudCommentEvent(body) if err != nil { e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing pull data: %s %s=%s", err, bitbucketCloudRequestIDHeader, reqID) @@ -165,7 +202,17 @@ func (e *EventsController) HandleBitbucketCommentEvent(w http.ResponseWriter, bo e.handleCommentEvent(w, baseRepo, &headRepo, &pull, user, pull.Num, comment, models.BitbucketCloud) } -func (e *EventsController) HandleBitbucketPullRequestEvent(w http.ResponseWriter, eventType string, body []byte, reqID string) { +// HandleBitbucketServerCommentEvent handles comment events from Bitbucket. +func (e *EventsController) HandleBitbucketServerCommentEvent(w http.ResponseWriter, body []byte, reqID string) { + pull, baseRepo, headRepo, user, comment, err := e.Parser.ParseBitbucketServerCommentEvent(body) + if err != nil { + e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing pull data: %s %s=%s", err, bitbucketCloudRequestIDHeader, reqID) + return + } + e.handleCommentEvent(w, baseRepo, &headRepo, &pull, user, pull.Num, comment, models.BitbucketCloud) +} + +func (e *EventsController) HandleBitbucketCloudPullRequestEvent(w http.ResponseWriter, eventType string, body []byte, reqID string) { pull, baseRepo, headRepo, user, err := e.Parser.ParseBitbucketCloudPullEvent(body) if err != nil { e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing pull data: %s %s=%s", err, bitbucketCloudRequestIDHeader, reqID) @@ -176,6 +223,17 @@ func (e *EventsController) HandleBitbucketPullRequestEvent(w http.ResponseWriter e.handlePullRequestEvent(w, baseRepo, headRepo, pull, user, pullEventType) } +func (e *EventsController) HandleBitbucketServerPullRequestEvent(w http.ResponseWriter, eventType string, body []byte, reqID string) { + pull, baseRepo, headRepo, user, err := e.Parser.ParseBitbucketServerPullEvent(body) + if err != nil { + e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing pull data: %s %s=%s", err, bitbucketServerRequestIDHeader, reqID) + return + } + pullEventType := e.Parser.GetBitbucketServerEventType(eventType) + e.Logger.Info("identified event as type %q", pullEventType.String()) + e.handlePullRequestEvent(w, baseRepo, headRepo, pull, user, pullEventType) +} + // HandleGithubPullRequestEvent will delete any locks associated with the pull // request if the event is a pull request closed event. It's exported to make // testing easier. diff --git a/server/server.go b/server/server.go index 3fb862f59c..63af666e5b 100644 --- a/server/server.go +++ b/server/server.go @@ -41,6 +41,7 @@ import ( "github.com/runatlantis/atlantis/server/events/terraform" "github.com/runatlantis/atlantis/server/events/vcs" "github.com/runatlantis/atlantis/server/events/vcs/bitbucketcloud" + "github.com/runatlantis/atlantis/server/events/vcs/bitbucketserver" "github.com/runatlantis/atlantis/server/events/webhooks" "github.com/runatlantis/atlantis/server/events/yaml" "github.com/runatlantis/atlantis/server/logging" @@ -137,7 +138,8 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { var supportedVCSHosts []models.VCSHostType var githubClient *vcs.GithubClient var gitlabClient *vcs.GitlabClient - var bitbucketClient *bitbucketcloud.Client + var bitbucketCloudClient *bitbucketcloud.Client + var bitbucketServerClient *bitbucketserver.Client if userConfig.GithubUser != "" { supportedVCSHosts = append(supportedVCSHosts, models.Github) var err error @@ -168,17 +170,25 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { } } if userConfig.BitbucketUser != "" { - supportedVCSHosts = append(supportedVCSHosts, models.BitbucketCloud) - var err error - bitbucketClient, err = bitbucketcloud.NewClient( - http.DefaultClient, - userConfig.BitbucketUser, - userConfig.BitbucketToken, - // todo: don't hardcode when we allow for bitbucket server - "https://api.bitbucket.org/", - userConfig.AtlantisURL) - if err != nil { - return nil, errors.Wrapf(err, "setting up Bitbucket client") + if userConfig.BitbucketHostname == bitbucketcloud.Hostname { + supportedVCSHosts = append(supportedVCSHosts, models.BitbucketCloud) + bitbucketCloudClient = bitbucketcloud.NewClient( + http.DefaultClient, + userConfig.BitbucketUser, + userConfig.BitbucketToken, + userConfig.AtlantisURL) + } else { + supportedVCSHosts = append(supportedVCSHosts, models.BitbucketServer) + var err error + bitbucketServerClient, err = bitbucketserver.NewClient( + http.DefaultClient, + userConfig.BitbucketUser, + userConfig.BitbucketToken, + userConfig.BitbucketHostname, + userConfig.AtlantisURL) + if err != nil { + return nil, errors.Wrapf(err, "setting up Bitbucket Server client") + } } } @@ -196,7 +206,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { if err != nil { return nil, errors.Wrap(err, "initializing webhooks") } - vcsClient := vcs.NewDefaultClientProxy(githubClient, gitlabClient, bitbucketClient, bitbucketClient) + vcsClient := vcs.NewDefaultClientProxy(githubClient, gitlabClient, bitbucketCloudClient, bitbucketServerClient) commitStatusUpdater := &events.DefaultCommitStatusUpdater{Client: vcsClient} terraformClient, err := terraform.NewClient(userConfig.DataDir) // The flag.Lookup call is to detect if we're running in a unit test. If we @@ -232,12 +242,13 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { } logger := logging.NewSimpleLogger("server", nil, false, logging.ToLogLevel(userConfig.LogLevel)) eventParser := &events.EventParser{ - GithubUser: userConfig.GithubUser, - GithubToken: userConfig.GithubToken, - GitlabUser: userConfig.GitlabUser, - GitlabToken: userConfig.GitlabToken, - BitbucketUser: userConfig.BitbucketUser, - BitbucketToken: userConfig.BitbucketToken, + GithubUser: userConfig.GithubUser, + GithubToken: userConfig.GithubToken, + GitlabUser: userConfig.GitlabUser, + GitlabToken: userConfig.GitlabToken, + BitbucketUser: userConfig.BitbucketUser, + BitbucketToken: userConfig.BitbucketToken, + BitbucketServerURL: userConfig.BitbucketHostname, } commentParser := &events.CommentParser{ GithubUser: userConfig.GithubUser, From 33926a01f269e59ca4c70609a2e6f0b219d9a52c Mon Sep 17 00:00:00 2001 From: Luke Kysow Date: Tue, 24 Jul 2018 15:20:17 +0200 Subject: [PATCH 05/11] WebHook -> Webhook --- CONTRIBUTING.md | 2 +- cmd/server.go | 12 +++++----- cmd/server_test.go | 36 ++++++++++++++-------------- server/events_controller.go | 12 +++++----- server/events_controller_e2e_test.go | 4 ++-- server/events_controller_test.go | 4 ++-- server/server.go | 8 +++---- 7 files changed, 39 insertions(+), 39 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8002004d11..784f7836f8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,7 +56,7 @@ atlantis server --gh-user --gh-token --repo-whiteli ``` ngrok http 4141 ``` -- Create a WebHook in your repo and use the `https` url that `ngrok` printed out after running `ngrok http 4141`. Be sure to append `/events` so your webhook url looks something like `https://efce3bcd.ngrok.io/events`. See [Add GitHub Webhook](https://github.com/runatlantis/atlantis#add-github-webhook). +- Create a Webhook in your repo and use the `https` url that `ngrok` printed out after running `ngrok http 4141`. Be sure to append `/events` so your webhook url looks something like `https://efce3bcd.ngrok.io/events`. See [Add GitHub Webhook](https://github.com/runatlantis/atlantis#add-github-webhook). - Create a pull request and type `atlantis help`. You should see the request in the `ngrok` and Atlantis logs and you should also see Atlantis comment back. ## Code Style diff --git a/cmd/server.go b/cmd/server.go index a6451c8062..75567eea15 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -44,11 +44,11 @@ const ( GHHostnameFlag = "gh-hostname" GHTokenFlag = "gh-token" GHUserFlag = "gh-user" - GHWebHookSecret = "gh-webhook-secret" // nolint: gosec + GHWebhookSecret = "gh-webhook-secret" // nolint: gosec GitlabHostnameFlag = "gitlab-hostname" GitlabTokenFlag = "gitlab-token" GitlabUserFlag = "gitlab-user" - GitlabWebHookSecret = "gitlab-webhook-secret" // nolint: gosec + GitlabWebhookSecret = "gitlab-webhook-secret" // nolint: gosec LogLevelFlag = "log-level" PortFlag = "port" RepoWhitelistFlag = "repo-whitelist" @@ -109,7 +109,7 @@ var stringFlags = []stringFlag{ description: "GitHub token of API user. Can also be specified via the ATLANTIS_GH_TOKEN environment variable.", }, { - name: GHWebHookSecret, + name: GHWebhookSecret, description: "Secret used to validate GitHub webhooks (see https://developer.github.com/webhooks/securing/)." + " SECURITY WARNING: If not specified, Atlantis won't be able to validate that the incoming webhook call came from GitHub. " + "This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. " + @@ -129,7 +129,7 @@ var stringFlags = []stringFlag{ description: "GitLab token of API user. Can also be specified via the ATLANTIS_GITLAB_TOKEN environment variable.", }, { - name: GitlabWebHookSecret, + name: GitlabWebhookSecret, description: "Optional secret used to validate GitLab webhooks." + " SECURITY WARNING: If not specified, Atlantis won't be able to validate that the incoming webhook call came from GitLab. " + "This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. " + @@ -434,10 +434,10 @@ func (s *ServerCmd) trimAtSymbolFromUsers(userConfig *server.UserConfig) { } func (s *ServerCmd) securityWarnings(userConfig *server.UserConfig) { - if userConfig.GithubUser != "" && userConfig.GithubWebHookSecret == "" && !s.SilenceOutput { + if userConfig.GithubUser != "" && userConfig.GithubWebhookSecret == "" && !s.SilenceOutput { fmt.Fprintf(os.Stderr, "%s[WARN] No GitHub webhook secret set. This could allow attackers to spoof requests from GitHub.%s\n", RedTermStart, RedTermEnd) } - if userConfig.GitlabUser != "" && userConfig.GitlabWebHookSecret == "" && !s.SilenceOutput { + if userConfig.GitlabUser != "" && userConfig.GitlabWebhookSecret == "" && !s.SilenceOutput { fmt.Fprintf(os.Stderr, "%s[WARN] No GitLab webhook secret set. This could allow attackers to spoof requests from GitLab.%s\n", RedTermStart, RedTermEnd) } if userConfig.BitbucketUser != "" && userConfig.BitbucketHostname == DefaultBitbucketHostname && !s.SilenceOutput { diff --git a/cmd/server_test.go b/cmd/server_test.go index 017c87a6a6..71d52305b8 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -334,11 +334,11 @@ func TestExecute_Defaults(t *testing.T) { Equals(t, "github.com", passedConfig.GithubHostname) Equals(t, "token", passedConfig.GithubToken) Equals(t, "user", passedConfig.GithubUser) - Equals(t, "", passedConfig.GithubWebHookSecret) + Equals(t, "", passedConfig.GithubWebhookSecret) Equals(t, "gitlab.com", passedConfig.GitlabHostname) Equals(t, "gitlab-token", passedConfig.GitlabToken) Equals(t, "gitlab-user", passedConfig.GitlabUser) - Equals(t, "", passedConfig.GitlabWebHookSecret) + Equals(t, "", passedConfig.GitlabWebhookSecret) Equals(t, "bitbucket.org", passedConfig.BitbucketHostname) Equals(t, "bitbucket-token", passedConfig.BitbucketToken) Equals(t, "bitbucket-user", passedConfig.BitbucketUser) @@ -432,11 +432,11 @@ func TestExecute_Flags(t *testing.T) { cmd.GHHostnameFlag: "ghhostname", cmd.GHTokenFlag: "token", cmd.GHUserFlag: "user", - cmd.GHWebHookSecret: "secret", + cmd.GHWebhookSecret: "secret", cmd.GitlabHostnameFlag: "gitlab-hostname", cmd.GitlabTokenFlag: "gitlab-token", cmd.GitlabUserFlag: "gitlab-user", - cmd.GitlabWebHookSecret: "gitlab-secret", + cmd.GitlabWebhookSecret: "gitlab-secret", cmd.LogLevelFlag: "debug", cmd.PortFlag: 8181, cmd.RepoWhitelistFlag: "github.com/runatlantis/atlantis", @@ -456,11 +456,11 @@ func TestExecute_Flags(t *testing.T) { Equals(t, "ghhostname", passedConfig.GithubHostname) Equals(t, "token", passedConfig.GithubToken) Equals(t, "user", passedConfig.GithubUser) - Equals(t, "secret", passedConfig.GithubWebHookSecret) + Equals(t, "secret", passedConfig.GithubWebhookSecret) Equals(t, "gitlab-hostname", passedConfig.GitlabHostname) Equals(t, "gitlab-token", passedConfig.GitlabToken) Equals(t, "gitlab-user", passedConfig.GitlabUser) - Equals(t, "gitlab-secret", passedConfig.GitlabWebHookSecret) + Equals(t, "gitlab-secret", passedConfig.GitlabWebhookSecret) Equals(t, "debug", passedConfig.LogLevel) Equals(t, 8181, passedConfig.Port) Equals(t, "github.com/runatlantis/atlantis", passedConfig.RepoWhitelist) @@ -509,11 +509,11 @@ ssl-key-file: key-file Equals(t, "ghhostname", passedConfig.GithubHostname) Equals(t, "token", passedConfig.GithubToken) Equals(t, "user", passedConfig.GithubUser) - Equals(t, "secret", passedConfig.GithubWebHookSecret) + Equals(t, "secret", passedConfig.GithubWebhookSecret) Equals(t, "gitlab-hostname", passedConfig.GitlabHostname) Equals(t, "gitlab-token", passedConfig.GitlabToken) Equals(t, "gitlab-user", passedConfig.GitlabUser) - Equals(t, "gitlab-secret", passedConfig.GitlabWebHookSecret) + Equals(t, "gitlab-secret", passedConfig.GitlabWebhookSecret) Equals(t, "debug", passedConfig.LogLevel) Equals(t, 8181, passedConfig.Port) Equals(t, "github.com/runatlantis/atlantis", passedConfig.RepoWhitelist) @@ -587,11 +587,11 @@ ssl-key-file: key-file Equals(t, "override-gh-hostname", passedConfig.GithubHostname) Equals(t, "override-gh-token", passedConfig.GithubToken) Equals(t, "override-gh-user", passedConfig.GithubUser) - Equals(t, "override-gh-webhook-secret", passedConfig.GithubWebHookSecret) + Equals(t, "override-gh-webhook-secret", passedConfig.GithubWebhookSecret) Equals(t, "override-gitlab-hostname", passedConfig.GitlabHostname) Equals(t, "override-gitlab-token", passedConfig.GitlabToken) Equals(t, "override-gitlab-user", passedConfig.GitlabUser) - Equals(t, "override-gitlab-webhook-secret", passedConfig.GitlabWebHookSecret) + Equals(t, "override-gitlab-webhook-secret", passedConfig.GitlabWebhookSecret) Equals(t, "info", passedConfig.LogLevel) Equals(t, 8282, passedConfig.Port) Equals(t, "override,override", passedConfig.RepoWhitelist) @@ -636,11 +636,11 @@ ssl-key-file: key-file cmd.GHHostnameFlag: "override-gh-hostname", cmd.GHTokenFlag: "override-gh-token", cmd.GHUserFlag: "override-gh-user", - cmd.GHWebHookSecret: "override-gh-webhook-secret", + cmd.GHWebhookSecret: "override-gh-webhook-secret", cmd.GitlabHostnameFlag: "override-gitlab-hostname", cmd.GitlabTokenFlag: "override-gitlab-token", cmd.GitlabUserFlag: "override-gitlab-user", - cmd.GitlabWebHookSecret: "override-gitlab-webhook-secret", + cmd.GitlabWebhookSecret: "override-gitlab-webhook-secret", cmd.LogLevelFlag: "info", cmd.PortFlag: 8282, cmd.RepoWhitelistFlag: "override,override", @@ -658,11 +658,11 @@ ssl-key-file: key-file Equals(t, "override-gh-hostname", passedConfig.GithubHostname) Equals(t, "override-gh-token", passedConfig.GithubToken) Equals(t, "override-gh-user", passedConfig.GithubUser) - Equals(t, "override-gh-webhook-secret", passedConfig.GithubWebHookSecret) + Equals(t, "override-gh-webhook-secret", passedConfig.GithubWebhookSecret) Equals(t, "override-gitlab-hostname", passedConfig.GitlabHostname) Equals(t, "override-gitlab-token", passedConfig.GitlabToken) Equals(t, "override-gitlab-user", passedConfig.GitlabUser) - Equals(t, "override-gitlab-webhook-secret", passedConfig.GitlabWebHookSecret) + Equals(t, "override-gitlab-webhook-secret", passedConfig.GitlabWebhookSecret) Equals(t, "info", passedConfig.LogLevel) Equals(t, 8282, passedConfig.Port) Equals(t, "override,override", passedConfig.RepoWhitelist) @@ -709,11 +709,11 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) { cmd.GHHostnameFlag: "override-gh-hostname", cmd.GHTokenFlag: "override-gh-token", cmd.GHUserFlag: "override-gh-user", - cmd.GHWebHookSecret: "override-gh-webhook-secret", + cmd.GHWebhookSecret: "override-gh-webhook-secret", cmd.GitlabHostnameFlag: "override-gitlab-hostname", cmd.GitlabTokenFlag: "override-gitlab-token", cmd.GitlabUserFlag: "override-gitlab-user", - cmd.GitlabWebHookSecret: "override-gitlab-webhook-secret", + cmd.GitlabWebhookSecret: "override-gitlab-webhook-secret", cmd.LogLevelFlag: "info", cmd.PortFlag: 8282, cmd.RepoWhitelistFlag: "override,override", @@ -733,11 +733,11 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) { Equals(t, "override-gh-hostname", passedConfig.GithubHostname) Equals(t, "override-gh-token", passedConfig.GithubToken) Equals(t, "override-gh-user", passedConfig.GithubUser) - Equals(t, "override-gh-webhook-secret", passedConfig.GithubWebHookSecret) + Equals(t, "override-gh-webhook-secret", passedConfig.GithubWebhookSecret) Equals(t, "override-gitlab-hostname", passedConfig.GitlabHostname) Equals(t, "override-gitlab-token", passedConfig.GitlabToken) Equals(t, "override-gitlab-user", passedConfig.GitlabUser) - Equals(t, "override-gitlab-webhook-secret", passedConfig.GitlabWebHookSecret) + Equals(t, "override-gitlab-webhook-secret", passedConfig.GitlabWebhookSecret) Equals(t, "info", passedConfig.LogLevel) Equals(t, 8282, passedConfig.Port) Equals(t, "override,override", passedConfig.RepoWhitelist) diff --git a/server/events_controller.go b/server/events_controller.go index 9f9059b02f..7293b9b424 100644 --- a/server/events_controller.go +++ b/server/events_controller.go @@ -44,16 +44,16 @@ type EventsController struct { Logger *logging.SimpleLogger Parser events.EventParsing CommentParser events.CommentParsing - // GithubWebHookSecret is the secret added to this webhook via the GitHub + // GithubWebhookSecret is the secret added to this webhook via the GitHub // UI that identifies this call as coming from GitHub. If empty, no // request validation is done. - GithubWebHookSecret []byte + GithubWebhookSecret []byte GithubRequestValidator GithubRequestValidator GitlabRequestParserValidator GitlabRequestParserValidator - // GitlabWebHookSecret is the secret added to this webhook via the GitLab + // GitlabWebhookSecret is the secret added to this webhook via the GitLab // UI that identifies this call as coming from GitLab. If empty, no // request validation is done. - GitlabWebHookSecret []byte + GitlabWebhookSecret []byte RepoWhitelistChecker *events.RepoWhitelistChecker // SupportedVCSHosts is which VCS hosts Atlantis was configured upon // startup to support. @@ -106,7 +106,7 @@ func (e *EventsController) Post(w http.ResponseWriter, r *http.Request) { func (e *EventsController) handleGithubPost(w http.ResponseWriter, r *http.Request) { // Validate the request against the optional webhook secret. - payload, err := e.GithubRequestValidator.Validate(r, e.GithubWebHookSecret) + payload, err := e.GithubRequestValidator.Validate(r, e.GithubWebhookSecret) if err != nil { e.respond(w, logging.Warn, http.StatusBadRequest, err.Error()) return @@ -294,7 +294,7 @@ func (e *EventsController) handlePullRequestEvent(w http.ResponseWriter, baseRep } func (e *EventsController) handleGitlabPost(w http.ResponseWriter, r *http.Request) { - event, err := e.GitlabRequestParserValidator.ParseAndValidate(r, e.GitlabWebHookSecret) + event, err := e.GitlabRequestParserValidator.ParseAndValidate(r, e.GitlabWebhookSecret) if err != nil { e.respond(w, logging.Warn, http.StatusBadRequest, err.Error()) return diff --git a/server/events_controller_e2e_test.go b/server/events_controller_e2e_test.go index fa39a7cc28..a54926506b 100644 --- a/server/events_controller_e2e_test.go +++ b/server/events_controller_e2e_test.go @@ -301,10 +301,10 @@ func setupE2E(t *testing.T) (server.EventsController, *vcsmocks.MockClientProxy, Logger: logger, Parser: eventParser, CommentParser: commentParser, - GithubWebHookSecret: nil, + GithubWebhookSecret: nil, GithubRequestValidator: &server.DefaultGithubRequestValidator{}, GitlabRequestParserValidator: &server.DefaultGitlabRequestParserValidator{}, - GitlabWebHookSecret: nil, + GitlabWebhookSecret: nil, RepoWhitelistChecker: repoWhitelistChecker, SupportedVCSHosts: []models.VCSHostType{models.Gitlab, models.Github, models.BitbucketCloud}, VCSClient: e2eVCSClient, diff --git a/server/events_controller_test.go b/server/events_controller_test.go index a3bda4b43f..163116cee4 100644 --- a/server/events_controller_test.go +++ b/server/events_controller_test.go @@ -528,9 +528,9 @@ func setup(t *testing.T) (server.EventsController, *mocks.MockGithubRequestValid CommentParser: cp, CommandRunner: cr, PullCleaner: c, - GithubWebHookSecret: secret, + GithubWebhookSecret: secret, SupportedVCSHosts: []models.VCSHostType{models.Github, models.Gitlab}, - GitlabWebHookSecret: secret, + GitlabWebhookSecret: secret, GitlabRequestParserValidator: gl, RepoWhitelistChecker: repoWhitelistChecker, VCSClient: vcsmock, diff --git a/server/server.go b/server/server.go index 63af666e5b..c4d7bb1f61 100644 --- a/server/server.go +++ b/server/server.go @@ -92,11 +92,11 @@ type UserConfig struct { GithubHostname string `mapstructure:"gh-hostname"` GithubToken string `mapstructure:"gh-token"` GithubUser string `mapstructure:"gh-user"` - GithubWebHookSecret string `mapstructure:"gh-webhook-secret"` + 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"` + GitlabWebhookSecret string `mapstructure:"gitlab-webhook-secret"` LogLevel string `mapstructure:"log-level"` Port int `mapstructure:"port"` RepoWhitelist string `mapstructure:"repo-whitelist"` @@ -319,10 +319,10 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { Parser: eventParser, CommentParser: commentParser, Logger: logger, - GithubWebHookSecret: []byte(userConfig.GithubWebHookSecret), + GithubWebhookSecret: []byte(userConfig.GithubWebhookSecret), GithubRequestValidator: &DefaultGithubRequestValidator{}, GitlabRequestParserValidator: &DefaultGitlabRequestParserValidator{}, - GitlabWebHookSecret: []byte(userConfig.GitlabWebHookSecret), + GitlabWebhookSecret: []byte(userConfig.GitlabWebhookSecret), RepoWhitelistChecker: repoWhitelist, SupportedVCSHosts: supportedVCSHosts, VCSClient: vcsClient, From e8c0f7c5bb2ba8d7e045a665a7866d7fc4d32a96 Mon Sep 17 00:00:00 2001 From: Luke Kysow Date: Tue, 24 Jul 2018 16:13:55 +0200 Subject: [PATCH 06/11] Add webhook secret checking. --- cmd/server.go | 100 +++--- cmd/server_test.go | 284 +++++++++++------- .../vcs/bitbucketcloud/bitbucketcloud.go | 2 +- .../vcs/bitbucketserver/request_validation.go | 71 +++++ server/events_controller.go | 13 + server/server.go | 44 +-- 6 files changed, 351 insertions(+), 163 deletions(-) create mode 100644 server/events/vcs/bitbucketserver/request_validation.go diff --git a/cmd/server.go b/cmd/server.go index 75567eea15..4b76fb3f01 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -15,6 +15,7 @@ package cmd import ( "fmt" + "net/url" "os" "path/filepath" "strings" @@ -33,36 +34,37 @@ import ( // 3. Add your flag's description etc. to the stringFlags, intFlags, or boolFlags slices. const ( // Flag names. - AllowForkPRsFlag = "allow-fork-prs" - AllowRepoConfigFlag = "allow-repo-config" - AtlantisURLFlag = "atlantis-url" - BitbucketHostnameFlag = "bitbucket-hostname" - BitbucketTokenFlag = "bitbucket-token" - BitbucketUserFlag = "bitbucket-user" - ConfigFlag = "config" - DataDirFlag = "data-dir" - GHHostnameFlag = "gh-hostname" - GHTokenFlag = "gh-token" - GHUserFlag = "gh-user" - GHWebhookSecret = "gh-webhook-secret" // nolint: gosec - GitlabHostnameFlag = "gitlab-hostname" - GitlabTokenFlag = "gitlab-token" - GitlabUserFlag = "gitlab-user" - GitlabWebhookSecret = "gitlab-webhook-secret" // nolint: gosec - LogLevelFlag = "log-level" - PortFlag = "port" - RepoWhitelistFlag = "repo-whitelist" - RequireApprovalFlag = "require-approval" - SSLCertFileFlag = "ssl-cert-file" - SSLKeyFileFlag = "ssl-key-file" + AllowForkPRsFlag = "allow-fork-prs" + AllowRepoConfigFlag = "allow-repo-config" + AtlantisURLFlag = "atlantis-url" + BitbucketBaseURLFlag = "bitbucket-base-url" + BitbucketTokenFlag = "bitbucket-token" + BitbucketUserFlag = "bitbucket-user" + BitbucketWebhookSecretFlag = "bitbucket-webhook-secret" // nolint: gosec + ConfigFlag = "config" + DataDirFlag = "data-dir" + GHHostnameFlag = "gh-hostname" + GHTokenFlag = "gh-token" + GHUserFlag = "gh-user" + GHWebhookSecretFlag = "gh-webhook-secret" // nolint: gosec + GitlabHostnameFlag = "gitlab-hostname" + GitlabTokenFlag = "gitlab-token" + GitlabUserFlag = "gitlab-user" + GitlabWebhookSecretFlag = "gitlab-webhook-secret" // nolint: gosec + LogLevelFlag = "log-level" + PortFlag = "port" + RepoWhitelistFlag = "repo-whitelist" + RequireApprovalFlag = "require-approval" + SSLCertFileFlag = "ssl-cert-file" + SSLKeyFileFlag = "ssl-key-file" // Flag defaults. - DefaultBitbucketHostname = bitbucketcloud.Hostname - DefaultDataDir = "~/.atlantis" - DefaultGHHostname = "github.com" - DefaultGitlabHostname = "gitlab.com" - DefaultLogLevel = "info" - DefaultPort = 4141 + DefaultBitbucketBaseURL = bitbucketcloud.BaseURL + DefaultDataDir = "~/.atlantis" + DefaultGHHostname = "github.com" + DefaultGitlabHostname = "gitlab.com" + DefaultLogLevel = "info" + DefaultPort = 4141 ) const RedTermStart = "\033[31m" @@ -82,9 +84,18 @@ var stringFlags = []stringFlag{ description: "Bitbucket app password of API user. Can also be specified via the ATLANTIS_BITBUCKET_TOKEN environment variable.", }, { - name: BitbucketHostnameFlag, - description: "Hostname and port of your Bitbucket Server (aka Stash) installation. If using Bitbucket Cloud (bitbucket.org), no need to set.", - defaultValue: DefaultBitbucketHostname, + name: BitbucketBaseURLFlag, + description: "Base URL of Bitbucket Server (aka Stash) installation." + + " Must include scheme, ex. 'http://bitbucket.corp:7990' or 'https://bitbucket.corp'." + + " If using Bitbucket Cloud (bitbucket.org), do not set.", + defaultValue: DefaultBitbucketBaseURL, + }, + { + name: BitbucketWebhookSecretFlag, + description: "Secret used to validate Bitbucket webhooks. Only Bitbucket Server supports webhook secrets." + + " SECURITY WARNING: If not specified, Atlantis won't be able to validate that the incoming webhook call came from Bitbucket. " + + "This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. " + + "Should be specified via the ATLANTIS_BITBUCKET_WEBHOOK_SECRET environment variable.", }, { name: ConfigFlag, @@ -109,7 +120,7 @@ var stringFlags = []stringFlag{ description: "GitHub token of API user. Can also be specified via the ATLANTIS_GH_TOKEN environment variable.", }, { - name: GHWebhookSecret, + name: GHWebhookSecretFlag, description: "Secret used to validate GitHub webhooks (see https://developer.github.com/webhooks/securing/)." + " SECURITY WARNING: If not specified, Atlantis won't be able to validate that the incoming webhook call came from GitHub. " + "This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. " + @@ -129,7 +140,7 @@ var stringFlags = []stringFlag{ description: "GitLab token of API user. Can also be specified via the ATLANTIS_GITLAB_TOKEN environment variable.", }, { - name: GitlabWebhookSecret, + name: GitlabWebhookSecretFlag, description: "Optional secret used to validate GitLab webhooks." + " SECURITY WARNING: If not specified, Atlantis won't be able to validate that the incoming webhook call came from GitLab. " + "This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. " + @@ -144,7 +155,8 @@ var stringFlags = []stringFlag{ name: RepoWhitelistFlag, description: "Comma separated list of repositories that Atlantis will operate on. " + "The format is {hostname}/{owner}/{repo}, ex. github.com/runatlantis/atlantis. '*' matches any characters until the next comma and can be used for example to whitelist " + - "all repos: '*' (not recommended), an entire hostname: 'internalgithub.com/*' or an organization: 'github.com/runatlantis/*'.", + "all repos: '*' (not recommended), an entire hostname: 'internalgithub.com/*' or an organization: 'github.com/runatlantis/*'." + + " For Bitbucket Server, {hostname} is the domain without scheme and port, {owner} is the name of the project (not the key), and {repo} is the repo name.", }, { name: SSLCertFileFlag, @@ -344,8 +356,8 @@ func (s *ServerCmd) setDefaults(c *server.UserConfig) { if c.GitlabHostname == "" { c.GitlabHostname = DefaultGitlabHostname } - if c.BitbucketHostname == "" { - c.BitbucketHostname = DefaultBitbucketHostname + if c.BitbucketBaseURL == "" { + c.BitbucketBaseURL = DefaultBitbucketBaseURL } if c.LogLevel == "" { c.LogLevel = DefaultLogLevel @@ -387,6 +399,17 @@ func (s *ServerCmd) validate(userConfig server.UserConfig) error { return fmt.Errorf("--%s cannot contain ://, should be hostnames only", RepoWhitelistFlag) } + if userConfig.BitbucketBaseURL == DefaultBitbucketBaseURL && userConfig.BitbucketWebhookSecret != "" { + return fmt.Errorf("--%s cannot be specified for Bitbucket Cloud because it is not supported by Bitbucket", BitbucketWebhookSecretFlag) + } + + parsed, err := url.Parse(userConfig.BitbucketBaseURL) + if err != nil { + return fmt.Errorf("error parsing --%s flag value %q: %s", BitbucketWebhookSecretFlag, userConfig.BitbucketBaseURL, err) + } + if parsed.Scheme != "http" && parsed.Scheme != "https" { + return fmt.Errorf("--%s must have http:// or https://, got %q", BitbucketBaseURLFlag, userConfig.BitbucketBaseURL) + } return nil } @@ -440,7 +463,10 @@ func (s *ServerCmd) securityWarnings(userConfig *server.UserConfig) { if userConfig.GitlabUser != "" && userConfig.GitlabWebhookSecret == "" && !s.SilenceOutput { fmt.Fprintf(os.Stderr, "%s[WARN] No GitLab webhook secret set. This could allow attackers to spoof requests from GitLab.%s\n", RedTermStart, RedTermEnd) } - if userConfig.BitbucketUser != "" && userConfig.BitbucketHostname == DefaultBitbucketHostname && !s.SilenceOutput { + if userConfig.BitbucketUser != "" && userConfig.BitbucketBaseURL != DefaultBitbucketBaseURL && userConfig.BitbucketWebhookSecret == "" && !s.SilenceOutput { + fmt.Fprintf(os.Stderr, "%s[WARN] No Bitbucket webhook secret set. This could allow attackers to spoof requests from Bitbucket.%s\n", RedTermStart, RedTermEnd) + } + if userConfig.BitbucketUser != "" && userConfig.BitbucketBaseURL == DefaultBitbucketBaseURL && !s.SilenceOutput { fmt.Fprintf(os.Stderr, "%s[WARN] Bitbucket Cloud does not support webhook secrets. This could allow attackers to spoof requests from Bitbucket. Ensure you are whitelisting Bitbucket IPs.%s\n", RedTermStart, RedTermEnd) } } diff --git a/cmd/server_test.go b/cmd/server_test.go index 71d52305b8..9397146fe5 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -339,9 +339,10 @@ func TestExecute_Defaults(t *testing.T) { Equals(t, "gitlab-token", passedConfig.GitlabToken) Equals(t, "gitlab-user", passedConfig.GitlabUser) Equals(t, "", passedConfig.GitlabWebhookSecret) - Equals(t, "bitbucket.org", passedConfig.BitbucketHostname) + Equals(t, "https://api.bitbucket.org", passedConfig.BitbucketBaseURL) Equals(t, "bitbucket-token", passedConfig.BitbucketToken) Equals(t, "bitbucket-user", passedConfig.BitbucketUser) + Equals(t, "", passedConfig.BitbucketWebhookSecret) Equals(t, "info", passedConfig.LogLevel) Equals(t, 4141, passedConfig.Port) Equals(t, false, passedConfig.RequireApproval) @@ -422,27 +423,28 @@ func TestExecute_BitbucketUser(t *testing.T) { func TestExecute_Flags(t *testing.T) { t.Log("Should use all flags that are set.") c := setup(map[string]interface{}{ - cmd.AtlantisURLFlag: "url", - cmd.AllowForkPRsFlag: true, - cmd.AllowRepoConfigFlag: true, - //cmd.BitbucketHostnameFlag: "ghhostname", - cmd.BitbucketTokenFlag: "bitbucket-token", - cmd.BitbucketUserFlag: "bitbucket-user", - cmd.DataDirFlag: "/path", - cmd.GHHostnameFlag: "ghhostname", - cmd.GHTokenFlag: "token", - cmd.GHUserFlag: "user", - cmd.GHWebhookSecret: "secret", - cmd.GitlabHostnameFlag: "gitlab-hostname", - cmd.GitlabTokenFlag: "gitlab-token", - cmd.GitlabUserFlag: "gitlab-user", - cmd.GitlabWebhookSecret: "gitlab-secret", - cmd.LogLevelFlag: "debug", - cmd.PortFlag: 8181, - cmd.RepoWhitelistFlag: "github.com/runatlantis/atlantis", - cmd.RequireApprovalFlag: true, - cmd.SSLCertFileFlag: "cert-file", - cmd.SSLKeyFileFlag: "key-file", + cmd.AtlantisURLFlag: "url", + cmd.AllowForkPRsFlag: true, + cmd.AllowRepoConfigFlag: true, + cmd.BitbucketBaseURLFlag: "https://bitbucket-base-url.com", + cmd.BitbucketTokenFlag: "bitbucket-token", + cmd.BitbucketUserFlag: "bitbucket-user", + cmd.BitbucketWebhookSecretFlag: "bitbucket-secret", + cmd.DataDirFlag: "/path", + cmd.GHHostnameFlag: "ghhostname", + cmd.GHTokenFlag: "token", + cmd.GHUserFlag: "user", + cmd.GHWebhookSecretFlag: "secret", + cmd.GitlabHostnameFlag: "gitlab-hostname", + cmd.GitlabTokenFlag: "gitlab-token", + cmd.GitlabUserFlag: "gitlab-user", + cmd.GitlabWebhookSecretFlag: "gitlab-secret", + cmd.LogLevelFlag: "debug", + cmd.PortFlag: 8181, + cmd.RepoWhitelistFlag: "github.com/runatlantis/atlantis", + cmd.RequireApprovalFlag: true, + cmd.SSLCertFileFlag: "cert-file", + cmd.SSLKeyFileFlag: "key-file", }) err := c.Execute() Ok(t, err) @@ -450,8 +452,10 @@ func TestExecute_Flags(t *testing.T) { Equals(t, "url", passedConfig.AtlantisURL) Equals(t, true, passedConfig.AllowForkPRs) Equals(t, true, passedConfig.AllowRepoConfig) + Equals(t, "https://bitbucket-base-url.com", passedConfig.BitbucketBaseURL) Equals(t, "bitbucket-token", passedConfig.BitbucketToken) Equals(t, "bitbucket-user", passedConfig.BitbucketUser) + Equals(t, "bitbucket-secret", passedConfig.BitbucketWebhookSecret) Equals(t, "/path", passedConfig.DataDir) Equals(t, "ghhostname", passedConfig.GithubHostname) Equals(t, "token", passedConfig.GithubToken) @@ -475,8 +479,10 @@ func TestExecute_ConfigFile(t *testing.T) { atlantis-url: "url" allow-fork-prs: true allow-repo-config: true +bitbucket-base-url: "https://mydomain.com" bitbucket-token: "bitbucket-token" bitbucket-user: "bitbucket-user" +bitbucket-webhook-secret: "bitbucket-secret" data-dir: "/path" gh-hostname: "ghhostname" gh-token: "token" @@ -503,8 +509,10 @@ ssl-key-file: key-file Equals(t, "url", passedConfig.AtlantisURL) Equals(t, true, passedConfig.AllowForkPRs) Equals(t, true, passedConfig.AllowRepoConfig) + Equals(t, "https://mydomain.com", passedConfig.BitbucketBaseURL) Equals(t, "bitbucket-token", passedConfig.BitbucketToken) Equals(t, "bitbucket-user", passedConfig.BitbucketUser) + Equals(t, "bitbucket-secret", passedConfig.BitbucketWebhookSecret) Equals(t, "/path", passedConfig.DataDir) Equals(t, "ghhostname", passedConfig.GithubHostname) Equals(t, "token", passedConfig.GithubToken) @@ -528,8 +536,10 @@ func TestExecute_EnvironmentOverride(t *testing.T) { atlantis-url: "url" allow-fork-prs: true allow-repo-config: true +bitbucket-base-url: "https://mydomain.com" bitbucket-token: "bitbucket-token" bitbucket-user: "bitbucket-user" +bitbucket-webhook-secret: "bitbucket-secret" data-dir: "/path" gh-hostname: "ghhostname" gh-token: "token" @@ -550,26 +560,28 @@ ssl-key-file: key-file // NOTE: We add the ATLANTIS_ prefix below. for name, value := range map[string]string{ - "ATLANTIS_URL": "override-url", - "ALLOW_FORK_PRS": "false", - "ALLOW_REPO_CONFIG": "false", - "BITBUCKET_TOKEN": "override-bitbucket-token", - "BITBUCKET_USER": "override-bitbucket-user", - "DATA_DIR": "/override-path", - "GH_HOSTNAME": "override-gh-hostname", - "GH_TOKEN": "override-gh-token", - "GH_USER": "override-gh-user", - "GH_WEBHOOK_SECRET": "override-gh-webhook-secret", - "GITLAB_HOSTNAME": "override-gitlab-hostname", - "GITLAB_TOKEN": "override-gitlab-token", - "GITLAB_USER": "override-gitlab-user", - "GITLAB_WEBHOOK_SECRET": "override-gitlab-webhook-secret", - "LOG_LEVEL": "info", - "PORT": "8282", - "REPO_WHITELIST": "override,override", - "REQUIRE_APPROVAL": "false", - "SSL_CERT_FILE": "override-cert-file", - "SSL_KEY_FILE": "override-key-file", + "ATLANTIS_URL": "override-url", + "ALLOW_FORK_PRS": "false", + "ALLOW_REPO_CONFIG": "false", + "BITBUCKET_BASE_URL": "https://override-bitbucket-base-url", + "BITBUCKET_TOKEN": "override-bitbucket-token", + "BITBUCKET_USER": "override-bitbucket-user", + "BITBUCKET_WEBHOOK_SECRET": "override-bitbucket-secret", + "DATA_DIR": "/override-path", + "GH_HOSTNAME": "override-gh-hostname", + "GH_TOKEN": "override-gh-token", + "GH_USER": "override-gh-user", + "GH_WEBHOOK_SECRET": "override-gh-webhook-secret", + "GITLAB_HOSTNAME": "override-gitlab-hostname", + "GITLAB_TOKEN": "override-gitlab-token", + "GITLAB_USER": "override-gitlab-user", + "GITLAB_WEBHOOK_SECRET": "override-gitlab-webhook-secret", + "LOG_LEVEL": "info", + "PORT": "8282", + "REPO_WHITELIST": "override,override", + "REQUIRE_APPROVAL": "false", + "SSL_CERT_FILE": "override-cert-file", + "SSL_KEY_FILE": "override-key-file", } { os.Setenv("ATLANTIS_"+name, value) // nolint: errcheck } @@ -581,8 +593,10 @@ ssl-key-file: key-file Equals(t, "override-url", passedConfig.AtlantisURL) Equals(t, false, passedConfig.AllowForkPRs) Equals(t, false, passedConfig.AllowRepoConfig) + Equals(t, "https://override-bitbucket-base-url", passedConfig.BitbucketBaseURL) Equals(t, "override-bitbucket-token", passedConfig.BitbucketToken) Equals(t, "override-bitbucket-user", passedConfig.BitbucketUser) + Equals(t, "override-bitbucket-secret", passedConfig.BitbucketWebhookSecret) Equals(t, "/override-path", passedConfig.DataDir) Equals(t, "override-gh-hostname", passedConfig.GithubHostname) Equals(t, "override-gh-token", passedConfig.GithubToken) @@ -606,8 +620,10 @@ func TestExecute_FlagConfigOverride(t *testing.T) { atlantis-url: "url" allow-fork-prs: true allow-repo-config: true +bitbucket-base-url: "https://bitbucket-base-url" bitbucket-token: "bitbucket-token" bitbucket-user: "bitbucket-user" +bitbucket-webhook-secret: "bitbucket-secret" data-dir: "/path" gh-hostname: "ghhostname" gh-token: "token" @@ -627,33 +643,37 @@ ssl-key-file: key-file defer os.Remove(tmpFile) // nolint: errcheck c := setup(map[string]interface{}{ - cmd.AtlantisURLFlag: "override-url", - cmd.AllowForkPRsFlag: false, - cmd.AllowRepoConfigFlag: false, - cmd.BitbucketTokenFlag: "override-bitbucket-token", - cmd.BitbucketUserFlag: "override-bitbucket-user", - cmd.DataDirFlag: "/override-path", - cmd.GHHostnameFlag: "override-gh-hostname", - cmd.GHTokenFlag: "override-gh-token", - cmd.GHUserFlag: "override-gh-user", - cmd.GHWebhookSecret: "override-gh-webhook-secret", - cmd.GitlabHostnameFlag: "override-gitlab-hostname", - cmd.GitlabTokenFlag: "override-gitlab-token", - cmd.GitlabUserFlag: "override-gitlab-user", - cmd.GitlabWebhookSecret: "override-gitlab-webhook-secret", - cmd.LogLevelFlag: "info", - cmd.PortFlag: 8282, - cmd.RepoWhitelistFlag: "override,override", - cmd.RequireApprovalFlag: false, - cmd.SSLCertFileFlag: "override-cert-file", - cmd.SSLKeyFileFlag: "override-key-file", + cmd.AtlantisURLFlag: "override-url", + cmd.AllowForkPRsFlag: false, + cmd.AllowRepoConfigFlag: false, + cmd.BitbucketBaseURLFlag: "https://override-bitbucket-base-url", + cmd.BitbucketTokenFlag: "override-bitbucket-token", + cmd.BitbucketUserFlag: "override-bitbucket-user", + cmd.BitbucketWebhookSecretFlag: "override-bitbucket-secret", + cmd.DataDirFlag: "/override-path", + cmd.GHHostnameFlag: "override-gh-hostname", + cmd.GHTokenFlag: "override-gh-token", + cmd.GHUserFlag: "override-gh-user", + cmd.GHWebhookSecretFlag: "override-gh-webhook-secret", + cmd.GitlabHostnameFlag: "override-gitlab-hostname", + cmd.GitlabTokenFlag: "override-gitlab-token", + cmd.GitlabUserFlag: "override-gitlab-user", + cmd.GitlabWebhookSecretFlag: "override-gitlab-webhook-secret", + cmd.LogLevelFlag: "info", + cmd.PortFlag: 8282, + cmd.RepoWhitelistFlag: "override,override", + cmd.RequireApprovalFlag: false, + cmd.SSLCertFileFlag: "override-cert-file", + cmd.SSLKeyFileFlag: "override-key-file", }) err := c.Execute() Ok(t, err) Equals(t, "override-url", passedConfig.AtlantisURL) Equals(t, false, passedConfig.AllowForkPRs) + Equals(t, "https://override-bitbucket-base-url", passedConfig.BitbucketBaseURL) Equals(t, "override-bitbucket-token", passedConfig.BitbucketToken) Equals(t, "override-bitbucket-user", passedConfig.BitbucketUser) + Equals(t, "override-bitbucket-secret", passedConfig.BitbucketWebhookSecret) Equals(t, "/override-path", passedConfig.DataDir) Equals(t, "override-gh-hostname", passedConfig.GithubHostname) Equals(t, "override-gh-token", passedConfig.GithubToken) @@ -674,52 +694,63 @@ ssl-key-file: key-file func TestExecute_FlagEnvVarOverride(t *testing.T) { t.Log("Flags should override environment variables.") - for name, value := range map[string]string{ - "ATLANTIS_URL": "url", - "ALLOW_FORK_PRS": "true", - "ALLOW_REPO_CONFIG": "true", - "BITBUCKET_TOKEN": "bitbucket-token", - "BITBUCKET_USER": "bitbucket-user", - "DATA_DIR": "/path", - "GH_HOSTNAME": "gh-hostname", - "GH_TOKEN": "gh-token", - "GH_USER": "gh-user", - "GH_WEBHOOK_SECRET": "gh-webhook-secret", - "GITLAB_HOSTNAME": "gitlab-hostname", - "GITLAB_TOKEN": "gitlab-token", - "GITLAB_USER": "gitlab-user", - "GITLAB_WEBHOOK_SECRET": "gitlab-webhook-secret", - "LOG_LEVEL": "debug", - "PORT": "8181", - "REPO_WHITELIST": "*", - "REQUIRE_APPROVAL": "true", - "SSL_CERT_FILE": "cert-file", - "SSL_KEY_FILE": "key-file", - } { + envVars := map[string]string{ + "ATLANTIS_URL": "url", + "ALLOW_FORK_PRS": "true", + "ALLOW_REPO_CONFIG": "true", + "BITBUCKET_BASE_URL": "https://bitbucket-base-url", + "BITBUCKET_TOKEN": "bitbucket-token", + "BITBUCKET_USER": "bitbucket-user", + "BITBUCKET_WEBHOOK_SECRET": "bitbucket-secret", + "DATA_DIR": "/path", + "GH_HOSTNAME": "gh-hostname", + "GH_TOKEN": "gh-token", + "GH_USER": "gh-user", + "GH_WEBHOOK_SECRET": "gh-webhook-secret", + "GITLAB_HOSTNAME": "gitlab-hostname", + "GITLAB_TOKEN": "gitlab-token", + "GITLAB_USER": "gitlab-user", + "GITLAB_WEBHOOK_SECRET": "gitlab-webhook-secret", + "LOG_LEVEL": "debug", + "PORT": "8181", + "REPO_WHITELIST": "*", + "REQUIRE_APPROVAL": "true", + "SSL_CERT_FILE": "cert-file", + "SSL_KEY_FILE": "key-file", + } + for name, value := range envVars { os.Setenv("ATLANTIS_"+name, value) // nolint: errcheck } + defer func() { + // Unset after this test finishes. + for name := range envVars { + os.Unsetenv("ATLANTIS_" + name) // nolint: errcheck + } + }() c := setup(map[string]interface{}{ - cmd.AtlantisURLFlag: "override-url", - cmd.AllowForkPRsFlag: false, - cmd.AllowRepoConfigFlag: false, - cmd.BitbucketTokenFlag: "override-bitbucket-token", - cmd.BitbucketUserFlag: "override-bitbucket-user", - cmd.DataDirFlag: "/override-path", - cmd.GHHostnameFlag: "override-gh-hostname", - cmd.GHTokenFlag: "override-gh-token", - cmd.GHUserFlag: "override-gh-user", - cmd.GHWebhookSecret: "override-gh-webhook-secret", - cmd.GitlabHostnameFlag: "override-gitlab-hostname", - cmd.GitlabTokenFlag: "override-gitlab-token", - cmd.GitlabUserFlag: "override-gitlab-user", - cmd.GitlabWebhookSecret: "override-gitlab-webhook-secret", - cmd.LogLevelFlag: "info", - cmd.PortFlag: 8282, - cmd.RepoWhitelistFlag: "override,override", - cmd.RequireApprovalFlag: false, - cmd.SSLCertFileFlag: "override-cert-file", - cmd.SSLKeyFileFlag: "override-key-file", + cmd.AtlantisURLFlag: "override-url", + cmd.AllowForkPRsFlag: false, + cmd.AllowRepoConfigFlag: false, + cmd.BitbucketBaseURLFlag: "https://override-bitbucket-base-url", + cmd.BitbucketTokenFlag: "override-bitbucket-token", + cmd.BitbucketUserFlag: "override-bitbucket-user", + cmd.BitbucketWebhookSecretFlag: "override-bitbucket-secret", + cmd.DataDirFlag: "/override-path", + cmd.GHHostnameFlag: "override-gh-hostname", + cmd.GHTokenFlag: "override-gh-token", + cmd.GHUserFlag: "override-gh-user", + cmd.GHWebhookSecretFlag: "override-gh-webhook-secret", + cmd.GitlabHostnameFlag: "override-gitlab-hostname", + cmd.GitlabTokenFlag: "override-gitlab-token", + cmd.GitlabUserFlag: "override-gitlab-user", + cmd.GitlabWebhookSecretFlag: "override-gitlab-webhook-secret", + cmd.LogLevelFlag: "info", + cmd.PortFlag: 8282, + cmd.RepoWhitelistFlag: "override,override", + cmd.RequireApprovalFlag: false, + cmd.SSLCertFileFlag: "override-cert-file", + cmd.SSLKeyFileFlag: "override-key-file", }) err := c.Execute() Ok(t, err) @@ -727,8 +758,10 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) { Equals(t, "override-url", passedConfig.AtlantisURL) Equals(t, false, passedConfig.AllowForkPRs) Equals(t, false, passedConfig.AllowRepoConfig) + Equals(t, "https://override-bitbucket-base-url", passedConfig.BitbucketBaseURL) Equals(t, "override-bitbucket-token", passedConfig.BitbucketToken) Equals(t, "override-bitbucket-user", passedConfig.BitbucketUser) + Equals(t, "override-bitbucket-secret", passedConfig.BitbucketWebhookSecret) Equals(t, "/override-path", passedConfig.DataDir) Equals(t, "override-gh-hostname", passedConfig.GithubHostname) Equals(t, "override-gh-token", passedConfig.GithubToken) @@ -746,6 +779,49 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) { Equals(t, "override-key-file", passedConfig.SSLKeyFile) } +// If using bitbucket cloud, webhook secrets are not supported. +func TestExecute_BitbucketCloudWithWebhookSecret(t *testing.T) { + c := setup(map[string]interface{}{ + cmd.BitbucketUserFlag: "user", + cmd.BitbucketTokenFlag: "token", + cmd.RepoWhitelistFlag: "*", + cmd.BitbucketWebhookSecretFlag: "my secret", + }) + err := c.Execute() + ErrEquals(t, "--bitbucket-webhook-secret cannot be specified for Bitbucket Cloud because it is not supported by Bitbucket", err) +} + +// Base URL must have a scheme. +func TestExecute_BitbucketServerBaseURLScheme(t *testing.T) { + c := setup(map[string]interface{}{ + cmd.BitbucketUserFlag: "user", + cmd.BitbucketTokenFlag: "token", + cmd.RepoWhitelistFlag: "*", + cmd.BitbucketBaseURLFlag: "mydomain.com", + }) + ErrEquals(t, "--bitbucket-base-url must have http:// or https://, got \"mydomain.com\"", c.Execute()) + + c = setup(map[string]interface{}{ + cmd.BitbucketUserFlag: "user", + cmd.BitbucketTokenFlag: "token", + cmd.RepoWhitelistFlag: "*", + cmd.BitbucketBaseURLFlag: "://mydomain.com", + }) + ErrEquals(t, "error parsing --bitbucket-webhook-secret flag value \"://mydomain.com\": parse ://mydomain.com: missing protocol scheme", c.Execute()) +} + +// Port should be retained on base url. +func TestExecute_BitbucketServerBaseURLPort(t *testing.T) { + c := setup(map[string]interface{}{ + cmd.BitbucketUserFlag: "user", + cmd.BitbucketTokenFlag: "token", + cmd.RepoWhitelistFlag: "*", + cmd.BitbucketBaseURLFlag: "http://mydomain.com:7990", + }) + Ok(t, c.Execute()) + Equals(t, "http://mydomain.com:7990", passedConfig.BitbucketBaseURL) +} + func setup(flags map[string]interface{}) *cobra.Command { vipr := viper.New() for k, v := range flags { diff --git a/server/events/vcs/bitbucketcloud/bitbucketcloud.go b/server/events/vcs/bitbucketcloud/bitbucketcloud.go index 91efa0e5cf..89dcd3ce51 100644 --- a/server/events/vcs/bitbucketcloud/bitbucketcloud.go +++ b/server/events/vcs/bitbucketcloud/bitbucketcloud.go @@ -3,4 +3,4 @@ // APIs. package bitbucketcloud -const Hostname = "bitbucket.org" +const BaseURL = "https://api.bitbucket.org" diff --git a/server/events/vcs/bitbucketserver/request_validation.go b/server/events/vcs/bitbucketserver/request_validation.go new file mode 100644 index 0000000000..3c269a8cf8 --- /dev/null +++ b/server/events/vcs/bitbucketserver/request_validation.go @@ -0,0 +1,71 @@ +package bitbucketserver + +import ( + "crypto/hmac" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "encoding/hex" + "fmt" + "hash" + "strings" + + "github.com/pkg/errors" +) + +// Attribution: This code is taken from https://github.com/google/go-github. + +func ValidateSignature(payload []byte, signature string, secretKey []byte) error { + messageMAC, hashFunc, err := messageMAC(signature) + if err != nil { + return err + } + if !checkMAC(payload, messageMAC, secretKey, hashFunc) { + return errors.New("payload signature check failed") + } + return nil +} + +// genMAC generates the HMAC signature for a message provided the secret key +// and hashFunc. +func genMAC(message, key []byte, hashFunc func() hash.Hash) []byte { + mac := hmac.New(hashFunc, key) + mac.Write(message) + return mac.Sum(nil) +} + +// checkMAC reports whether messageMAC is a valid HMAC tag for message. +func checkMAC(message, messageMAC, key []byte, hashFunc func() hash.Hash) bool { + expectedMAC := genMAC(message, key, hashFunc) + return hmac.Equal(messageMAC, expectedMAC) +} + +// messageMAC returns the hex-decoded HMAC tag from the signature and its +// corresponding hash function. +func messageMAC(signature string) ([]byte, func() hash.Hash, error) { + if signature == "" { + return nil, nil, errors.New("missing signature") + } + sigParts := strings.SplitN(signature, "=", 2) + if len(sigParts) != 2 { + return nil, nil, fmt.Errorf("error parsing signature %q", signature) + } + + var hashFunc func() hash.Hash + switch sigParts[0] { + case "sha1": + hashFunc = sha1.New + case "sha256": + hashFunc = sha256.New + case "sha512": + hashFunc = sha512.New + default: + return nil, nil, fmt.Errorf("unknown hash type prefix: %q", sigParts[0]) + } + + buf, err := hex.DecodeString(sigParts[1]) + if err != nil { + return nil, nil, fmt.Errorf("error decoding signature %q: %v", signature, err) + } + return buf, hashFunc, nil +} diff --git a/server/events_controller.go b/server/events_controller.go index 7293b9b424..0b5ffc9601 100644 --- a/server/events_controller.go +++ b/server/events_controller.go @@ -20,6 +20,7 @@ import ( "github.com/google/go-github/github" "github.com/lkysow/go-gitlab" + "github.com/pkg/errors" "github.com/runatlantis/atlantis/server/events" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/events/vcs" @@ -35,6 +36,7 @@ const gitlabHeader = "X-Gitlab-Event" const bitbucketEventTypeHeader = "X-Event-Key" const bitbucketCloudRequestIDHeader = "X-Request-UUID" const bitbucketServerRequestIDHeader = "X-Request-ID" +const bitbucketServerSignatureHeader = "X-Hub-Signature" // EventsController handles all webhook requests which signify 'events' in the // VCS host, ex. GitHub. @@ -60,6 +62,10 @@ type EventsController struct { SupportedVCSHosts []models.VCSHostType VCSClient vcs.ClientProxy TestingMode bool + // BitbucketWebhookSecret is the secret added to this webhook via the Bitbucket + // UI that identifies this call as coming from Bitbucket. If empty, no + // request validation is done. + BitbucketWebhookSecret []byte } // Post handles POST webhook requests. @@ -153,12 +159,19 @@ func (e *EventsController) handleBitbucketCloudPost(w http.ResponseWriter, r *ht func (e *EventsController) handleBitbucketServerPost(w http.ResponseWriter, r *http.Request) { eventType := r.Header.Get(bitbucketEventTypeHeader) reqID := r.Header.Get(bitbucketServerRequestIDHeader) + sig := r.Header.Get(bitbucketServerSignatureHeader) defer r.Body.Close() // nolint: errcheck body, err := ioutil.ReadAll(r.Body) if err != nil { e.respond(w, logging.Error, http.StatusBadRequest, "Unable to read body: %s %s=%s", err, bitbucketServerRequestIDHeader, reqID) return } + if len(e.BitbucketWebhookSecret) > 0 { + if err := bitbucketserver.ValidateSignature(body, sig, e.BitbucketWebhookSecret); err != nil { + e.respond(w, logging.Warn, http.StatusBadRequest, errors.Wrap(err, "request did not pass validation").Error()) + return + } + } switch eventType { case bitbucketserver.PullCreatedHeader, bitbucketserver.PullFulfilledHeader, bitbucketserver.PullDeclinedHeader: e.Logger.Debug("handling as pull request state changed event") diff --git a/server/server.go b/server/server.go index c4d7bb1f61..ae48f0bc55 100644 --- a/server/server.go +++ b/server/server.go @@ -82,24 +82,25 @@ type Server struct { // 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"` - BitbucketHostname string `mapstructure:"bitbucket-hostname"` - BitbucketToken string `mapstructure:"bitbucket-token"` - BitbucketUser string `mapstructure:"bitbucket-user"` - 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"` - RepoWhitelist string `mapstructure:"repo-whitelist"` + AllowForkPRs bool `mapstructure:"allow-fork-prs"` + AllowRepoConfig bool `mapstructure:"allow-repo-config"` + AtlantisURL string `mapstructure:"atlantis-url"` + BitbucketBaseURL string `mapstructure:"bitbucket-base-url"` + BitbucketToken string `mapstructure:"bitbucket-token"` + BitbucketUser string `mapstructure:"bitbucket-user"` + BitbucketWebhookSecret string `mapstructure:"bitbucket-webhook-secret"` + 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"` + 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"` @@ -170,7 +171,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { } } if userConfig.BitbucketUser != "" { - if userConfig.BitbucketHostname == bitbucketcloud.Hostname { + if userConfig.BitbucketBaseURL == bitbucketcloud.BaseURL { supportedVCSHosts = append(supportedVCSHosts, models.BitbucketCloud) bitbucketCloudClient = bitbucketcloud.NewClient( http.DefaultClient, @@ -184,7 +185,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { http.DefaultClient, userConfig.BitbucketUser, userConfig.BitbucketToken, - userConfig.BitbucketHostname, + userConfig.BitbucketBaseURL, userConfig.AtlantisURL) if err != nil { return nil, errors.Wrapf(err, "setting up Bitbucket Server client") @@ -248,7 +249,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { GitlabToken: userConfig.GitlabToken, BitbucketUser: userConfig.BitbucketUser, BitbucketToken: userConfig.BitbucketToken, - BitbucketServerURL: userConfig.BitbucketHostname, + BitbucketServerURL: userConfig.BitbucketBaseURL, } commentParser := &events.CommentParser{ GithubUser: userConfig.GithubUser, @@ -326,6 +327,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { RepoWhitelistChecker: repoWhitelist, SupportedVCSHosts: supportedVCSHosts, VCSClient: vcsClient, + BitbucketWebhookSecret: []byte(userConfig.BitbucketWebhookSecret), } return &Server{ AtlantisVersion: config.AtlantisVersion, From da236e953b773796e8b2137eed635728388f8d15 Mon Sep 17 00:00:00 2001 From: Luke Kysow Date: Tue, 24 Jul 2018 20:22:38 +0200 Subject: [PATCH 07/11] Test Bitbucket Server --- cmd/server.go | 2 +- server/events/command_runner.go | 8 +- server/events/event_parser.go | 2 +- server/events/event_parser_test.go | 190 +++++++++++++++++- server/events/models/models_test.go | 18 ++ .../bitbucket-server-comment-event.json | 4 +- .../bitbucket-server-pull-event-merged.json | 23 +-- server/events/vcs/bitbucketcloud/client.go | 10 +- server/events/vcs/bitbucketserver/client.go | 4 +- .../events/vcs/bitbucketserver/client_test.go | 141 +++++++++++++ server/events/vcs/bitbucketserver/models.go | 2 +- .../request_validation_test.go | 24 +++ server/events_controller.go | 2 +- server/locks_controller.go | 6 +- 14 files changed, 395 insertions(+), 41 deletions(-) create mode 100644 server/events/vcs/bitbucketserver/client_test.go create mode 100644 server/events/vcs/bitbucketserver/request_validation_test.go diff --git a/cmd/server.go b/cmd/server.go index 4b76fb3f01..4495910fb9 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -40,7 +40,7 @@ const ( BitbucketBaseURLFlag = "bitbucket-base-url" BitbucketTokenFlag = "bitbucket-token" BitbucketUserFlag = "bitbucket-user" - BitbucketWebhookSecretFlag = "bitbucket-webhook-secret" // nolint: gosec + BitbucketWebhookSecretFlag = "bitbucket-webhook-secret" ConfigFlag = "config" DataDirFlag = "data-dir" GHHostnameFlag = "gh-hostname" diff --git a/server/events/command_runner.go b/server/events/command_runner.go index 54c260378d..42db89f739 100644 --- a/server/events/command_runner.go +++ b/server/events/command_runner.go @@ -228,13 +228,17 @@ func (c *DefaultCommandRunner) buildLogger(repoFullName string, pullNum int) *lo func (c *DefaultCommandRunner) validateCtxAndComment(ctx *CommandContext) bool { if !c.AllowForkPRs && ctx.HeadRepo.Owner != ctx.BaseRepo.Owner { ctx.Log.Info("command was run on a fork pull request which is disallowed") - c.VCSClient.CreateComment(ctx.BaseRepo, ctx.Pull.Num, fmt.Sprintf("Atlantis commands can't be run on fork pull requests. To enable, set --%s", c.AllowForkPRsFlag)) // nolint: errcheck + if err := c.VCSClient.CreateComment(ctx.BaseRepo, ctx.Pull.Num, fmt.Sprintf("Atlantis commands can't be run on fork pull requests. To enable, set --%s", c.AllowForkPRsFlag)); err != nil { + ctx.Log.Err("unable to comment: %s", err) + } return false } if ctx.Pull.State != models.OpenPullState { ctx.Log.Info("command was run on closed pull request") - c.VCSClient.CreateComment(ctx.BaseRepo, ctx.Pull.Num, "Atlantis commands can't be run on closed pull requests") // nolint: errcheck + if err := c.VCSClient.CreateComment(ctx.BaseRepo, ctx.Pull.Num, "Atlantis commands can't be run on closed pull requests"); err != nil { + ctx.Log.Err("unable to comment: %s", err) + } return false } return true diff --git a/server/events/event_parser.go b/server/events/event_parser.go index 2e6b9ba69d..e790ebdd89 100644 --- a/server/events/event_parser.go +++ b/server/events/event_parser.go @@ -435,7 +435,7 @@ func (e *EventParser) GetBitbucketServerEventType(eventTypeHeader string) models switch eventTypeHeader { case bitbucketserver.PullCreatedHeader: return models.OpenedPullEvent - case bitbucketserver.PullFulfilledHeader, bitbucketserver.PullDeclinedHeader: + case bitbucketserver.PullMergedHeader, bitbucketserver.PullDeclinedHeader: return models.ClosedPullEvent } return models.OtherPullEvent diff --git a/server/events/event_parser_test.go b/server/events/event_parser_test.go index 68711b4e7d..2829ff6794 100644 --- a/server/events/event_parser_test.go +++ b/server/events/event_parser_test.go @@ -31,12 +31,13 @@ import ( ) var parser = events.EventParser{ - GithubUser: "github-user", - GithubToken: "github-token", - GitlabUser: "gitlab-user", - GitlabToken: "gitlab-token", - BitbucketUser: "bitbucket-user", - BitbucketToken: "bitbucket-token", + GithubUser: "github-user", + GithubToken: "github-token", + GitlabUser: "gitlab-user", + GitlabToken: "gitlab-token", + BitbucketUser: "bitbucket-user", + BitbucketToken: "bitbucket-token", + BitbucketServerURL: "http://mycorp.com:7490", } func TestParseGithubRepo(t *testing.T) { @@ -704,6 +705,183 @@ func TestGetBitbucketCloudEventType(t *testing.T) { } } +func TestParseBitbucketServerCommentEvent_EmptyString(t *testing.T) { + _, _, _, _, _, err := parser.ParseBitbucketServerCommentEvent([]byte("")) + ErrEquals(t, "parsing json: unexpected end of JSON input", err) +} + +func TestParseBitbucketServerCommentEvent_EmptyObject(t *testing.T) { + _, _, _, _, _, err := parser.ParseBitbucketServerCommentEvent([]byte("{}")) + ErrContains(t, `API response "{}" was missing fields: Key: 'CommentEvent.CommonEventData.Actor' Error:Field validation for 'Actor' failed on the 'required' tag`, err) +} + +func TestParseBitbucketServerCommentEvent_CommitHashMissing(t *testing.T) { + path := filepath.Join("testdata", "bitbucket-server-comment-event.json") + bytes, err := ioutil.ReadFile(path) + if err != nil { + Ok(t, err) + } + emptyCommitHash := strings.Replace(string(bytes), `"latestCommit": "bfb1af1ba9c2a2fa84cd61af67e6e1b60a22e060",`, "", -1) + _, _, _, _, _, err = parser.ParseBitbucketServerCommentEvent([]byte(emptyCommitHash)) + ErrContains(t, "Key: 'CommentEvent.CommonEventData.PullRequest.FromRef.LatestCommit' Error:Field validation for 'LatestCommit' failed on the 'required' tag", err) +} + +func TestParseBitbucketServerCommentEvent_ValidEvent(t *testing.T) { + path := filepath.Join("testdata", "bitbucket-server-comment-event.json") + bytes, err := ioutil.ReadFile(path) + if err != nil { + Ok(t, err) + } + pull, baseRepo, headRepo, user, comment, err := parser.ParseBitbucketServerCommentEvent(bytes) + Ok(t, err) + expBaseRepo := models.Repo{ + FullName: "atlantis/atlantis-example", + Owner: "atlantis", + Name: "atlantis-example", + CloneURL: "http://bitbucket-user:bitbucket-token@mycorp.com:7490/scm/at/atlantis-example.git", + SanitizedCloneURL: "http://mycorp.com:7490/scm/at/atlantis-example.git", + VCSHost: models.VCSHost{ + Hostname: "mycorp.com", + Type: models.BitbucketServer, + }, + } + Equals(t, expBaseRepo, baseRepo) + Equals(t, models.PullRequest{ + Num: 1, + HeadCommit: "bfb1af1ba9c2a2fa84cd61af67e6e1b60a22e060", + URL: "http://mycorp.com:7490/projects/AT/repos/atlantis-example/pull-requests/1", + Branch: "branch", + Author: "lkysow", + State: models.OpenPullState, + BaseRepo: expBaseRepo, + }, pull) + Equals(t, models.Repo{ + FullName: "atlantis-fork/atlantis-example", + Owner: "atlantis-fork", + Name: "atlantis-example", + CloneURL: "http://bitbucket-user:bitbucket-token@mycorp.com:7490/scm/fk/atlantis-example.git", + SanitizedCloneURL: "http://mycorp.com:7490/scm/fk/atlantis-example.git", + VCSHost: models.VCSHost{ + Hostname: "mycorp.com", + Type: models.BitbucketServer, + }, + }, headRepo) + Equals(t, models.User{ + Username: "lkysow", + }, user) + Equals(t, "atlantis plan", comment) +} + +func TestParseBitbucketServerCommentEvent_MultipleStates(t *testing.T) { + path := filepath.Join("testdata", "bitbucket-server-comment-event.json") + bytes, err := ioutil.ReadFile(path) + if err != nil { + Ok(t, err) + } + + cases := []struct { + pullState string + exp models.PullRequestState + }{ + { + "OPEN", + models.OpenPullState, + }, + { + "MERGED", + models.ClosedPullState, + }, + { + "DECLINED", + models.ClosedPullState, + }, + } + + for _, c := range cases { + t.Run(c.pullState, func(t *testing.T) { + withState := strings.Replace(string(bytes), `"state": "OPEN"`, fmt.Sprintf(`"state": "%s"`, c.pullState), -1) + pull, _, _, _, _, err := parser.ParseBitbucketServerCommentEvent([]byte(withState)) + Ok(t, err) + Equals(t, c.exp, pull.State) + }) + } +} + +func TestParseBitbucketServerPullEvent_ValidEvent(t *testing.T) { + path := filepath.Join("testdata", "bitbucket-server-pull-event-merged.json") + bytes, err := ioutil.ReadFile(path) + if err != nil { + Ok(t, err) + } + pull, baseRepo, headRepo, user, err := parser.ParseBitbucketServerPullEvent(bytes) + Ok(t, err) + expBaseRepo := models.Repo{ + FullName: "atlantis/atlantis-example", + Owner: "atlantis", + Name: "atlantis-example", + CloneURL: "http://bitbucket-user:bitbucket-token@mycorp.com:7490/scm/at/atlantis-example.git", + SanitizedCloneURL: "http://mycorp.com:7490/scm/at/atlantis-example.git", + VCSHost: models.VCSHost{ + Hostname: "mycorp.com", + Type: models.BitbucketServer, + }, + } + Equals(t, expBaseRepo, baseRepo) + Equals(t, models.PullRequest{ + Num: 2, + HeadCommit: "86a574157f5a2dadaf595b9f06c70fdfdd039912", + URL: "http://mycorp.com:7490/projects/AT/repos/atlantis-example/pull-requests/2", + Branch: "branch", + Author: "lkysow", + State: models.ClosedPullState, + BaseRepo: expBaseRepo, + }, pull) + Equals(t, models.Repo{ + FullName: "atlantis-fork/atlantis-example", + Owner: "atlantis-fork", + Name: "atlantis-example", + CloneURL: "http://bitbucket-user:bitbucket-token@mycorp.com:7490/scm/fk/atlantis-example.git", + SanitizedCloneURL: "http://mycorp.com:7490/scm/fk/atlantis-example.git", + VCSHost: models.VCSHost{ + Hostname: "mycorp.com", + Type: models.BitbucketServer, + }, + }, headRepo) + Equals(t, models.User{ + Username: "lkysow", + }, user) +} + +func TestGetBitbucketServerEventType(t *testing.T) { + cases := []struct { + header string + exp models.PullRequestEventType + }{ + { + header: "pr:opened", + exp: models.OpenedPullEvent, + }, + { + header: "pr:merged", + exp: models.ClosedPullEvent, + }, + { + header: "pr:declined", + exp: models.ClosedPullEvent, + }, + { + header: "random", + exp: models.OtherPullEvent, + }, + } + for _, c := range cases { + t.Run(c.header, func(t *testing.T) { + act := parser.GetBitbucketServerEventType(c.header) + Equals(t, c.exp, act) + }) + } +} + var mergeEventJSON = `{ "object_kind": "merge_request", "user": { diff --git a/server/events/models/models_test.go b/server/events/models/models_test.go index 22faa3a384..4e2ed097a6 100644 --- a/server/events/models/models_test.go +++ b/server/events/models/models_test.go @@ -41,6 +41,24 @@ func TestNewRepo_CloneURLWrongRepo(t *testing.T) { ErrEquals(t, `expected clone url to have path "/owner/repo.git" but had "/notowner/repo.git"`, err) } +// For bitbucket server we don't validate the clone URL because the callers +// are actually constructing it. +func TestNewRepo_CloneURLBitbucketServer(t *testing.T) { + repo, err := models.NewRepo(models.BitbucketServer, "owner/repo", "http://mycorp.com:7990/scm/at/atlantis-example.git", "u", "p") + Ok(t, err) + Equals(t, models.Repo{ + FullName: "owner/repo", + Owner: "owner", + Name: "repo", + CloneURL: "http://u:p@mycorp.com:7990/scm/at/atlantis-example.git", + SanitizedCloneURL: "http://mycorp.com:7990/scm/at/atlantis-example.git", + VCSHost: models.VCSHost{ + Hostname: "mycorp.com", + Type: models.BitbucketServer, + }, + }, repo) +} + func TestNewRepo_FullNameWrongFormat(t *testing.T) { cases := []string{ "owner/repo/extra", diff --git a/server/events/testdata/bitbucket-server-comment-event.json b/server/events/testdata/bitbucket-server-comment-event.json index c188a79bb4..dc5fae6d91 100644 --- a/server/events/testdata/bitbucket-server-comment-event.json +++ b/server/events/testdata/bitbucket-server-comment-event.json @@ -32,9 +32,9 @@ "statusMessage": "Available", "forkable": true, "project": { - "key": "AT", + "key": "FK", "id": 1, - "name": "atlantis", + "name": "atlantis-fork", "public": false, "type": "NORMAL" }, diff --git a/server/events/testdata/bitbucket-server-pull-event-merged.json b/server/events/testdata/bitbucket-server-pull-event-merged.json index 561b847792..ed22810b1a 100644 --- a/server/events/testdata/bitbucket-server-pull-event-merged.json +++ b/server/events/testdata/bitbucket-server-pull-event-merged.json @@ -26,9 +26,9 @@ "displayId": "branch", "latestCommit": "86a574157f5a2dadaf595b9f06c70fdfdd039912", "repository": { - "slug": "atlantis-example-fork", + "slug": "atlantis-example", "id": 2, - "name": "atlantis-example-fork", + "name": "atlantis-example", "scmId": "git", "state": "AVAILABLE", "statusMessage": "Available", @@ -42,28 +42,19 @@ "statusMessage": "Available", "forkable": true, "project": { - "key": "AT", + "key": "FK", "id": 1, - "name": "atlantis", + "name": "atlantis-fork", "public": false, "type": "NORMAL" }, "public": false }, "project": { - "key": "~LKYSOW", + "key": "FK", "id": 2, - "name": "Luke Kysow", - "type": "PERSONAL", - "owner": { - "name": "lkysow", - "emailAddress": "lkysow@gmail.com", - "id": 1, - "displayName": "Luke Kysow", - "active": true, - "slug": "lkysow", - "type": "NORMAL" - } + "name": "atlantis-fork", + "type": "NORMAL" }, "public": false } diff --git a/server/events/vcs/bitbucketcloud/client.go b/server/events/vcs/bitbucketcloud/client.go index 71b8a5f13d..0c7b23772a 100644 --- a/server/events/vcs/bitbucketcloud/client.go +++ b/server/events/vcs/bitbucketcloud/client.go @@ -13,10 +13,6 @@ import ( "gopkg.in/go-playground/validator.v9" ) -const ( - APIBaseURL = "https://api.bitbucket.org" -) - type Client struct { HttpClient *http.Client Username string @@ -37,7 +33,7 @@ func NewClient(httpClient *http.Client, username string, password string, atlant HttpClient: httpClient, Username: username, Password: password, - BaseURL: APIBaseURL, + BaseURL: BaseURL, AtlantisURL: atlantisURL, } } @@ -57,7 +53,7 @@ func (b *Client) GetModifiedFiles(repo models.Repo, pull models.PullRequest) ([] } var diffStat DiffStat if err := json.Unmarshal(resp, &diffStat); err != nil { - return nil, err + 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)) @@ -108,7 +104,7 @@ func (b *Client) PullIsApproved(repo models.Repo, pull models.PullRequest) (bool } var pullResp PullRequest if err := json.Unmarshal(resp, &pullResp); err != nil { - return false, err + 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)) diff --git a/server/events/vcs/bitbucketserver/client.go b/server/events/vcs/bitbucketserver/client.go index 01efcf36aa..d9a3668f01 100644 --- a/server/events/vcs/bitbucketserver/client.go +++ b/server/events/vcs/bitbucketserver/client.go @@ -75,7 +75,7 @@ func (b *Client) GetModifiedFiles(repo models.Repo, pull models.PullRequest) ([] } var changes Changes if err := json.Unmarshal(resp, &changes); err != nil { - return nil, err + return nil, errors.Wrapf(err, "Could not parse response %q", string(resp)) } if err := validator.New().Struct(changes); err != nil { return nil, errors.Wrapf(err, "API response %q was missing fields", string(resp)) @@ -145,7 +145,7 @@ func (b *Client) PullIsApproved(repo models.Repo, pull models.PullRequest) (bool } var pullResp PullRequest if err := json.Unmarshal(resp, &pullResp); err != nil { - return false, err + 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)) diff --git a/server/events/vcs/bitbucketserver/client_test.go b/server/events/vcs/bitbucketserver/client_test.go new file mode 100644 index 0000000000..810017854a --- /dev/null +++ b/server/events/vcs/bitbucketserver/client_test.go @@ -0,0 +1,141 @@ +package bitbucketserver_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/runatlantis/atlantis/server/events/models" + "github.com/runatlantis/atlantis/server/events/vcs/bitbucketcloud" + "github.com/runatlantis/atlantis/server/events/vcs/bitbucketserver" + . "github.com/runatlantis/atlantis/testing" +) + +// Should follow pagination properly. +func TestClient_GetModifiedFilesPagination(t *testing.T) { + respTemplate := ` +{ + "values": [ + { + "path": { + "toString": "%s" + } + }, + { + "path": { + "toString": "%s" + } + } + ], + "size": 2, + "isLastPage": true, + "start": 0, + "limit": 2, + "nextPageStart": null +} +` + 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 "/rest/api/1.0/projects/ow/repos/repo/pull-requests/1/changes?start=0": + resp := strings.Replace(firstResp, `"isLastPage": true`, `"isLastPage": false`, -1) + resp = strings.Replace(resp, `"nextPageStart": null`, `"nextPageStart": "3"`, -1) + w.Write([]byte(resp)) // nolint: errcheck + return + // The second should hit this URL. + case "/rest/api/1.0/projects/ow/repos/repo/pull-requests/1/changes?start=3": + 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, err := bitbucketserver.NewClient(http.DefaultClient, "user", "pass", serverURL, "runatlantis.io") + Ok(t, err) + + files, err := client.GetModifiedFiles(models.Repo{ + FullName: "owner/repo", + Owner: "owner", + Name: "repo", + SanitizedCloneURL: fmt.Sprintf("%s/scm/ow/repo.git", serverURL), + VCSHost: models.VCSHost{ + Type: models.BitbucketCloud, + Hostname: "bitbucket.org", + }, + }, 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://api.bitbucket.org/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 := bitbucketcloud.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.BitbucketCloud, + Hostname: "bitbucket.org", + }, + }, models.PullRequest{ + Num: 1, + }) + Ok(t, err) + Equals(t, []string{"parent/child/file1.txt"}, files) +} diff --git a/server/events/vcs/bitbucketserver/models.go b/server/events/vcs/bitbucketserver/models.go index 85e1cad699..d1d8333d62 100644 --- a/server/events/vcs/bitbucketserver/models.go +++ b/server/events/vcs/bitbucketserver/models.go @@ -2,7 +2,7 @@ package bitbucketserver const ( PullCreatedHeader = "pr:opened" - PullFulfilledHeader = "pr:merged" + PullMergedHeader = "pr:merged" PullDeclinedHeader = "pr:declined" PullCommentCreatedHeader = "pr:comment:added" ) diff --git a/server/events/vcs/bitbucketserver/request_validation_test.go b/server/events/vcs/bitbucketserver/request_validation_test.go new file mode 100644 index 0000000000..9b9d5228a4 --- /dev/null +++ b/server/events/vcs/bitbucketserver/request_validation_test.go @@ -0,0 +1,24 @@ +package bitbucketserver_test + +import ( + "testing" + + "github.com/runatlantis/atlantis/server/events/vcs/bitbucketserver" + . "github.com/runatlantis/atlantis/testing" +) + +func TestValidateSignature(t *testing.T) { + body := `{"eventKey":"pr:comment:added","date":"2018-07-24T15:10:05+0200","actor":{"name":"lkysow","emailAddress":"lkysow@gmail.com","id":1,"displayName":"Luke Kysow","active":true,"slug":"lkysow","type":"NORMAL"},"pullRequest":{"id":5,"version":0,"title":"another.tf edited online with Bitbucket","state":"OPEN","open":true,"closed":false,"createdDate":1532365427513,"updatedDate":1532365427513,"fromRef":{"id":"refs/heads/lkysow/anothertf-1532365422773","displayId":"lkysow/anothertf-1532365422773","latestCommit":"b52b8e254e956654dcdb394d0ccba9199f420427","repository":{"slug":"atlantis-example","id":1,"name":"atlantis-example","scmId":"git","state":"AVAILABLE","statusMessage":"Available","forkable":true,"project":{"key":"AT","id":1,"name":"atlantis","public":false,"type":"NORMAL"},"public":false}},"toRef":{"id":"refs/heads/master","displayId":"master","latestCommit":"0a338874369017deba7c22e99e6000932724282f","repository":{"slug":"atlantis-example","id":1,"name":"atlantis-example","scmId":"git","state":"AVAILABLE","statusMessage":"Available","forkable":true,"project":{"key":"AT","id":1,"name":"atlantis","public":false,"type":"NORMAL"},"public":false}},"locked":false,"author":{"user":{"name":"lkysow","emailAddress":"lkysow@gmail.com","id":1,"displayName":"Luke Kysow","active":true,"slug":"lkysow","type":"NORMAL"},"role":"AUTHOR","approved":false,"status":"UNAPPROVED"},"reviewers":[],"participants":[]},"comment":{"properties":{"repositoryId":1},"id":65,"version":0,"text":"comment","author":{"name":"lkysow","emailAddress":"lkysow@gmail.com","id":1,"displayName":"Luke Kysow","active":true,"slug":"lkysow","type":"NORMAL"},"createdDate":1532437805137,"updatedDate":1532437805137,"comments":[],"tasks":[]}}` + secret := "mysecret" + sig := `sha256=ed11f92d1565a4de586727fe8260558277d58009e8957a79eb4749a7009ce083` + err := bitbucketserver.ValidateSignature([]byte(body), sig, []byte(secret)) + Ok(t, err) +} + +func TestValidateSignature_Invalid(t *testing.T) { + body := `"eventKey":"pr:comment:added","date":"2018-07-24T15:10:05+0200","actor":{"name":"lkysow","emailAddress":"lkysow@gmail.com","id":1,"displayName":"Luke Kysow","active":true,"slug":"lkysow","type":"NORMAL"},"pullRequest":{"id":5,"version":0,"title":"another.tf edited online with Bitbucket","state":"OPEN","open":true,"closed":false,"createdDate":1532365427513,"updatedDate":1532365427513,"fromRef":{"id":"refs/heads/lkysow/anothertf-1532365422773","displayId":"lkysow/anothertf-1532365422773","latestCommit":"b52b8e254e956654dcdb394d0ccba9199f420427","repository":{"slug":"atlantis-example","id":1,"name":"atlantis-example","scmId":"git","state":"AVAILABLE","statusMessage":"Available","forkable":true,"project":{"key":"AT","id":1,"name":"atlantis","public":false,"type":"NORMAL"},"public":false}},"toRef":{"id":"refs/heads/master","displayId":"master","latestCommit":"0a338874369017deba7c22e99e6000932724282f","repository":{"slug":"atlantis-example","id":1,"name":"atlantis-example","scmId":"git","state":"AVAILABLE","statusMessage":"Available","forkable":true,"project":{"key":"AT","id":1,"name":"atlantis","public":false,"type":"NORMAL"},"public":false}},"locked":false,"author":{"user":{"name":"lkysow","emailAddress":"lkysow@gmail.com","id":1,"displayName":"Luke Kysow","active":true,"slug":"lkysow","type":"NORMAL"},"role":"AUTHOR","approved":false,"status":"UNAPPROVED"},"reviewers":[],"participants":[]},"comment":{"properties":{"repositoryId":1},"id":65,"version":0,"text":"comment","author":{"name":"lkysow","emailAddress":"lkysow@gmail.com","id":1,"displayName":"Luke Kysow","active":true,"slug":"lkysow","type":"NORMAL"},"createdDate":1532437805137,"updatedDate":1532437805137,"comments":[],"tasks":[]}}` + secret := "mysecret" + sig := `sha256=ed11f92d1565a4de586727fe8260558277d58009e8957a79eb4749a7009ce083` + err := bitbucketserver.ValidateSignature([]byte(body), sig, []byte(secret)) + ErrEquals(t, "payload signature check failed", err) +} diff --git a/server/events_controller.go b/server/events_controller.go index 0b5ffc9601..5daa53e3b6 100644 --- a/server/events_controller.go +++ b/server/events_controller.go @@ -173,7 +173,7 @@ func (e *EventsController) handleBitbucketServerPost(w http.ResponseWriter, r *h } } switch eventType { - case bitbucketserver.PullCreatedHeader, bitbucketserver.PullFulfilledHeader, bitbucketserver.PullDeclinedHeader: + case bitbucketserver.PullCreatedHeader, bitbucketserver.PullMergedHeader, bitbucketserver.PullDeclinedHeader: e.Logger.Debug("handling as pull request state changed event") e.HandleBitbucketServerPullRequestEvent(w, eventType, body, reqID) return diff --git a/server/locks_controller.go b/server/locks_controller.go index 140cb30460..751cee31af 100644 --- a/server/locks_controller.go +++ b/server/locks_controller.go @@ -96,8 +96,10 @@ func (l *LocksController) DeleteLock(w http.ResponseWriter, r *http.Request) { l.Logger.Err("unable to obtain working dir lock when trying to delete old plans: %s", err) } else { defer unlock() - err = l.WorkingDir.DeleteForWorkspace(lock.Pull.BaseRepo, lock.Pull, lock.Workspace) - l.Logger.Err("unable to delete workspace: %s", err) + // nolint: vetshadow + if err := l.WorkingDir.DeleteForWorkspace(lock.Pull.BaseRepo, lock.Pull, lock.Workspace); err != nil { + l.Logger.Err("unable to delete workspace: %s", err) + } } // Once the lock has been deleted, comment back on the pull request. From ddbf9677f2b217eda60681f1d0d25f7ceacd1fda Mon Sep 17 00:00:00 2001 From: Luke Kysow Date: Tue, 24 Jul 2018 21:10:57 +0200 Subject: [PATCH 08/11] Update Bitbucket server docs --- runatlantis.io/docs/deployment.md | 35 +++- runatlantis.io/guide/getting-started.md | 161 ++++++++++++------ .../guide/images/bitbucket-server-webhook.png | Bin 0 -> 221133 bytes runatlantis.io/guide/requirements.md | 2 +- 4 files changed, 148 insertions(+), 50 deletions(-) create mode 100644 runatlantis.io/guide/images/bitbucket-server-webhook.png diff --git a/runatlantis.io/docs/deployment.md b/runatlantis.io/docs/deployment.md index 8dda719eff..b6baf8b943 100644 --- a/runatlantis.io/docs/deployment.md +++ b/runatlantis.io/docs/deployment.md @@ -24,7 +24,7 @@ Once you've decided where to host Atlantis you need to add that URL as a webhook to your Git host so that Atlantis gets notified about pull request events. See the instructions for your specific provider below: -### GitHub Webhook +### GitHub/GitHub Enterprise Webhook If you already have a GitHub organization we recommend installing the webhook at the **organization level** rather than on each repository, however both methods will work. ::: tip @@ -78,6 +78,19 @@ If you're using GitLab, navigate to your project's home page in GitLab - Click **Save** Bitbucket Webhook +### Bitbucket Server (aka Stash) Webhook +- Go to your repo's home page +- Click **Settings** in the sidebar +- Click **Webhooks** under the **WORKFLOW** section +- Click **Create webhook** +- Enter "Atlantis" for **Name** +- set **URL** to `http://$URL/events` (or `https://$URL/events` if you're using SSL) where `$URL` is where Atlantis is hosted. **Be sure to add `/events`** +- Double-check you added `/events` to the end of your URL. +- Set **Secret** to a random key (https://www.random.org/strings/). You'll need to pass this value to the `--bitbucket-webhook-secret` flag when you start Atlantis +- Under **Repository** select **Push** +- Under **Pull Request**, select: Opened, Modified, Merged, Declined, Deleted and Comment added +- Click **Save**Bitbucket Webhook + ## Create an access token for Atlantis We recommend using a dedicated CI user or creating a new user named **@atlantis** that performs all API actions, however for testing, you can use your own user. Here we'll create the access token that Atlantis uses to comment on the pull request and @@ -101,6 +114,14 @@ set commit statuses. - Select **Pull requests**: **Read** and **Write** so that Atlantis can read your pull requests and write comments to them - copy the access token +### Create a Bitbucket Server (aka Stash) Personal Access Token +- Click on your avatar in the top right and select **Manage account** +- Click **Personal access tokens** in the sidebar +- Click **Create a token** +- Name the token **atlantis** +- Give the token **Read** Project permissions and **Write** Pull request permissions +- Click **Create** and copy the access token + ## Start Atlantis Now you're ready to start Atlantis! The exact command depends on your Git host: @@ -157,6 +178,18 @@ atlantis server \ --repo-whitelist="$REPO_WHITELIST" ``` +### Bitbucket Server (aka Stash) +```bash +BASE_URL=YOUR_BITBUCKET_SERVER_URL # ex. http://bitbucket.mycorp:7990 +atlantis server \ +--atlantis-url="$URL" \ +--bitbucket-user="$USERNAME" \ +--bitbucket-token="$TOKEN" \ +--bitbucket-webhook-secret="$SECRET" \ +--bitbucket-base-url="$BASE_URL" \ +--repo-whitelist="$REPO_WHITELIST" +``` + Where - `$URL` is the URL that Atlantis can be reached at - `$USERNAME` is the GitHub/GitLab/Bitbucket username you generated the token for diff --git a/runatlantis.io/guide/getting-started.md b/runatlantis.io/guide/getting-started.md index 059e12196a..9de2cfe78f 100644 --- a/runatlantis.io/guide/getting-started.md +++ b/runatlantis.io/guide/getting-started.md @@ -1,5 +1,5 @@ # Getting Started -These instructions are for running Atlantis locally so you can test it out against +These instructions are for running Atlantis **locally on your own computer** so you can test it out against your own repositories before deciding whether to install it more permanently. ::: tip @@ -43,7 +43,7 @@ URL="https://{YOUR_HOSTNAME}.ngrok.io" GitHub and GitLab use webhook secrets so clients can verify that the webhooks came from them. ::: warning -Bitbucket Cloud (bitbucket.org) doesn't use webhook secrets so if you're using Bitbucket you can skip this +Bitbucket Cloud (bitbucket.org) doesn't use webhook secrets so if you're using Bitbucket Cloud you can skip this however you should whitelist Bitbucket IPs as a precaution. ::: Create a random string of any length (you can use [http://www.unit-conversion.info/texttools/random-string-generator/](http://www.unit-conversion.info/texttools/random-string-generator/)) @@ -55,59 +55,98 @@ SECRET="{YOUR_RANDOM_STRING}" ## Add Webhook Take the URL that ngrok output and create a webhook in your GitHub, GitLab or Bitbucket repo: -### GitHub -- Go to your repo's settings -- Select **Webhooks** or **Hooks** in the sidebar -- Click **Add webhook** -- set **Payload URL** to your ngrok url with `/events` at the end. Ex. `https://c5004d84.ngrok.io/events` -- double-check you added `/events` to the end of your URL. -- set **Content type** to `application/json` -- set **Secret** to your random string -- select **Let me select individual events** -- check the boxes - - **Pull request reviews** - - **Pushes** - - **Issue comments** - - **Pull requests** -- leave **Active** checked -- click **Add webhook** - -### GitLab -- Go to your repo's home page -- Click **Settings > Integrations** in the sidebar -- set **URL** to your ngrok url with `/events` at the end. Ex. `https://c5004d84.ngrok.io/events` -- double-check you added `/events` to the end of your URL. -- set **Secret Token** to your random string -- check the boxes - - **Push events** - - **Comments** - - **Merge Request events** -- leave **Enable SSL verification** checked -- click **Add webhook** +### GitHub or GitHub Enterprise +
+ Expand +
    +
  • Go to your repo's settings
  • +
  • Select Webhooks or Hooks in the sidebar
  • +
  • Click Add webhook
  • +
  • set Payload URL to your ngrok url with /events at the end. Ex. https://c5004d84.ngrok.io/events
  • +
  • double-check you added /events to the end of your URL.
  • +
  • set Content type to application/json
  • +
  • set Secret to your random string
  • +
  • select Let me select individual events
  • +
  • check the boxes +
      +
    • Pull request reviews
    • +
    • Pushes
    • +
    • Issue comments
    • +
    • Pull requests
    • +
    +
  • +
  • leave Active checked
  • +
  • click Add webhook
  • +
+
+ +### GitLab or GitLab Enterprise +
+ Expand +
    +
  • Go to your repo's home page
  • +
  • Click Settings > Integrations in the sidebar
  • +
  • set URL to your ngrok url with /events at the end. Ex. https://c5004d84.ngrok.io/events
  • +
  • double-check you added /events to the end of your URL.
  • +
  • set Secret Token to your random string
  • +
  • check the boxes +
      +
    • Push events
    • +
    • Comments
    • +
    • Merge Request events
    • +
    +
  • +
  • leave Enable SSL verification checked
  • +
  • click Add webhook
  • +
+
### Bitbucket Cloud (bitbucket.org) -- Go to your repo's home page -- Click **Settings** in the sidebar -- Click **Webhooks** under the **WORKFLOW** section -- Click **Add webhook** -- Enter "Atlantis" for **Title** -- set **URL** to your ngrok url with `/events` at the end. Ex. `https://c5004d84.ngrok.io/events` -- double-check you added `/events` to the end of your URL. -- Keep **Status** as Active -- Don't check **Skip certificate validation** because NGROK has a valid cert. -- Select **Choose from a full list of triggers** -- Under **Repository** **un**check everything -- Under **Issues** leave everything **un**checked -- Under **Pull Request**, select: Created, Updated, Merged, Declined and Comment created -- Click **Save** -Bitbucket Webhook +
+ Expand +
    +
  • Go to your repo's home page
  • +
  • Click Settings in the sidebar
  • +
  • Click Webhooks under the WORKFLOW section
  • +
  • Click Add webhook
  • +
  • Enter "Atlantis" for Title
  • +
  • Set URL to your ngrok url with /events at the end. Ex. https://c5004d84.ngrok.io/events
  • +
  • Double-check you added /events to the end of your URL.
  • +
  • Keep Status as Active
  • +
  • Don't check Skip certificate validation because NGROK has a valid cert.
  • +
  • Select Choose from a full list of triggers
  • +
  • Under Repositoryuncheck everything
  • +
  • Under Issues leave everything unchecked
  • +
  • Under Pull Request, select: Created, Updated, Merged, Declined and Comment created
  • +
  • Click SaveBitbucket Webhook
  • +
+
+ +### Bitbucket Server (aka Stash) +
+ Expand +
    +
  • Go to your repo's home page
  • +
  • Click Settings in the sidebar
  • +
  • Click Webhooks under the WORKFLOW section
  • +
  • Click Create webhook
  • +
  • Enter "Atlantis" for Name
  • +
  • Set URL to your ngrok url with /events at the end. Ex. https://c5004d84.ngrok.io/events
  • +
  • Double-check you added /events to the end of your URL.
  • +
  • Set Secret to your random string
  • +
  • Under Repository select Push
  • +
  • Under Pull Request, select: Opened, Modified, Merged, Declined, Deleted and Comment added
  • +
  • Click SaveBitbucket Webhook
  • +
+
+ ## Create an access token for Atlantis We recommend using a dedicated CI user or creating a new user named **@atlantis** that performs all API actions, however for testing, you can use your own user. Here we'll create the access token that Atlantis uses to comment on the pull request and set commit statuses. -### GitHub +### GitHub or GitHub Enterprise - follow [https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/#creating-a-token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/#creating-a-token) - create a token with **repo** scope - set the token as an environment variable @@ -115,7 +154,7 @@ set commit statuses. TOKEN="{YOUR_TOKEN}" ``` -### GitLab +### GitLab or GitLab Enterprise - follow [https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html#creating-a-personal-access-token](https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html#creating-a-personal-access-token) - create a token with **api** scope - set the token as an environment variable @@ -132,6 +171,17 @@ TOKEN="{YOUR_TOKEN}" TOKEN="{YOUR_TOKEN}" ``` +### Bitbucket Server (aka Stash) +- Click on your avatar in the top right and select **Manage account** +- Click **Personal access tokens** in the sidebar +- Click **Create a token** +- Name the token **atlantis** +- Give the token **Read** Project permissions and **Write** Pull request permissions +- Click **create** and set the token as an environment variable +``` +TOKEN="{YOUR_TOKEN}" +``` + ## Start Atlantis You're almost ready to start Atlantis, just set two more variables: @@ -140,6 +190,9 @@ You're almost ready to start Atlantis, just set two more variables: USERNAME="{the username of your GitHub, GitLab or Bitbucket user}" REPO_WHITELIST="$YOUR_GIT_HOST/$YOUR_USERNAME/$YOUR_REPO" # ex. REPO_WHITELIST="github.com/runatlantis/atlantis" +# If you're using Bitbucket Server, $YOUR_GIT_HOST will be the domain name of your +# server without scheme or port and $YOUR_USERNAME will be the name of the **project** the repo +# is under, **not the key** of the project. ``` Now you can start Atlantis. The exact command differs depending on your Git Host: @@ -196,6 +249,18 @@ atlantis server \ --repo-whitelist="$REPO_WHITELIST" ``` +### Bitbucket Server (aka Stash) +```bash +BASE_URL=YOUR_BITBUCKET_SERVER_URL # ex. http://bitbucket.mycorp:7990 +atlantis server \ +--atlantis-url="$URL" \ +--bitbucket-user="$USERNAME" \ +--bitbucket-token="$TOKEN" \ +--bitbucket-webhook-secret="$SECRET" \ +--bitbucket-base-url="$BASE_URL" \ +--repo-whitelist="$REPO_WHITELIST" +``` + ## Create a pull request Create a pull request so you can test Atlantis. ::: tip diff --git a/runatlantis.io/guide/images/bitbucket-server-webhook.png b/runatlantis.io/guide/images/bitbucket-server-webhook.png new file mode 100644 index 0000000000000000000000000000000000000000..d9167c0bbace2773e4bce1513ecf62b60e1f1db7 GIT binary patch literal 221133 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 z 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

0bit zv3ppQCfXA_br;5Q@ubh9cV^sH8@%!ODZ^d^Yzn?HMlZ9tCdkRawg~d}luZpKA5q3S; z;qn6P5dXT&3w1Od|CndwH{s8(+~;RWx-EQ!C-hd{H_?e5Jl8tgnam2jKdT@J;#KLY zdv=KU$y>(7&Dy}lEf_YDiLFSCuO%NFOj}}LV?3E^-5ti06L+$=LFa!<4!DLCI#{|qlu){`jU4avae6tdJ=(k<}os%GlT@I2Bx5=ly&~1om z2y0c#Qe=9t#oJgV{Wk5PDwQ%xrfX}OsmSkjdA?A%2KcD%$D^pQd@?(MZ zGyTC{D2cFHoi1sxxcO%s+>mE6X*Gh~KX7B{`;#v!xbH52wt3NX*7y2sYq#j_BZ72Y z_E#(Ax+1LVx+y^xh~Gowy(bV2Z`rqYPTSFHv$XRk*#VWSVwf|FdD9|Af3J_y zzPFR6rx?YFt<@gwIS5<57PwrNt~wRN6~|loJ9d)2ZEu%_@TKUHmT+U?XqY75(ye9q zCd$gs2C(;~Sc5*+=i$>bD&kG{ zKkSP^4m@*5&6GVOOCjMt`67YljP#C;FAT#EoQ>3m)VLn1=UR8P2i>*PpcjZT8LQFS z=Uf>~13WeUEcW&aLH=#tYhtp1Yd)9}8p3=4wehp+C^UqpaO$S#(fW1s$5%1_7}|{&5NpM%DrzmV z7@^YDvV~U~eD@rG*U=dmHy8pr(*jPP9!+%;+%M;2>Az#<^%rIm;Rc$$Hx`7QHDr|`7Pba>9 z-w-QuGP#6TZ0C+&qvUSYGP(H>`>YK{3X-DHAaiqrECux2(TcpsJ|+f5_0zDP*RMk? zNBv2Yj<@RyeJ#H4?-GT`P~vr5<*Px|f9SZJPDu0?sVi;BE#z|?Q8z9%fgPKFk-#X_tboI{~^b;_loJ*;y%

F!zPvw2k zI&srFjx}t-hq_P&bj3o)^h)&+Nwg-l%@VcqG@B7OiB4C3Pu$5UdBjWz%_WRJ zOy)Q0xXgL!cg>Vz$Y+l+phv&C9}Eo)bpH6bw73C}!<2RViJk=QsW1X&7dSuL7k=aQ zHVTQ4ZNOG&)$U}#HB+%3`eRCrB+_pAG*V;Fn^=8T=Onf=N13iLj`$r>YoxUg}`z^Ad%F^<=W@liV3 zh!S%nQSzBaf^yRD8#G@RY{VK1%2HH8>2k_FMa!K?X^ze2WuM9dUs&v$-4K8MpuN=f zjis=}5C+=reG!vV|K!;4E+~)B@Y5mJ{JwZgNb_W{QLATDwGxg*|7`NB`3u&?`vM?;w zv>8TyvJ$W_=5ur{MH)?S`H?Z8y!1MTp&SC7?Uyv1@7{RBmNSy2j6sYQZhzY=F*B8*!x6p>Gm$rzh5# zlO$;5#QPS9y!qEJd<&K)0z8RZ1`(i6r{>d4$-b4xHs|ZLX6+{;c%EA;9 z-%P{|Nt#5J(NG(|V}CNt(dGpB#Z01bg;B(@yK}fP+pVGV!)cvwYV6=ze*i5a2R{8s z?aFDL#EJ@jma*zYcmy(W%9o%1TuD?7Ls(6tD(AXsCQq02sG|{`s%`%h04Y1fE}AJ@ zsDphT;i|54Y=HI&(u?Azr72u7OEE;4};6oyZ05D~!b|K=JCwnG+vi-H&9HAgrFjL1RyW&Yz~bviVI6_I*pabzr6tYkc9J zjIVbdCBCCC@CLcXaG^HW*b={a;fwh4hrUQG05SwE#xv?b`HkGxtN>n=t`a2M2$+8Z z|5^k!3yOm0yW3y=CcIv;go5+z=>gN}idzToy-s|z8a!!;)Zi)5_G0LBM;I=oRy(!L za#Vx0kFP!t)Tq7vR^?>8J0f;01+w&c<$(P8AjnNSgX1|=>0~C*NjkrvEYoQ`gt&67 zSN^V{D|WJAdM}b&=iwJtx+oAH;FR0M=ggMObsYbg&%bOULIa{yuMr9K{wc8byV-sh zT$8D*Z=^k9e=uJ^@hakWj;fZEej%+Bs@=iQe*=LMPUI(GJQby0J*L}yGgv_-IpXH@ z)vyGtY3=-0a~(e7^p5E1mnw=62D-xdquzsm@R>4`&96i8j$Y#OH|)A99Yi0UIfxc} znMnI&-}DP-<0*U*(4$SjC_|R{r-vYXs$_{2xT1K`!zDvVt!w>`A!CJ*LnjE++gWS- zHONhGAG*(7yshl4NWhph{^i$8)3o=>Q;)QG?0k8Wgohh*i3$%)wpg{IKo9y`1Wc&Y z2?2N*$i#GWtjWp*Giavu^J^ci5nV+8E>Wlp7q0?^Kw)cdntV-Ma?SiKPU|+Uy z6BqR^v()})@=0mO9*N9!N_zEn*-6g%l>`98Zh7`i^IuTEBj60y;CGCL1w zvG4Q*7Z-LfF|Adp6L(t9{dqT%$noEyx*19$OKpLMYLwz;Sq5V%Zto6ZfW9V^AG>PX?wlqNq^&#Ns>K}uG zh~ZbkJt&9Qy^9ULg&novNhy-&JT)-p;-?^~XTUq~1MVn3k;RCW5mo1-KGh3V85 zMN{2X&!Rp+20vGoSx*iO8`%k~?M1|%G}jv(*O#Y!+~u33cofqnoe2I%)+Oq%@D~BQ z>)ttEsN^6%g!h|BeE{umbXSZ(Fa_9|E$9j>pr~olx=LN+UAcPH!ss1AGoRCJ&`JpVLNCZe5 zbCDR;f)^A)o7_4RBB4BX!=7F1vvIe!Zw@gZg(W33RrS$Vjkd><(2vgQBCuKfmT89rT2~*SR{LsYk$0L*KQ@Xb}0lo;yxS8QlKzjX|mC-09Y$Ni1c9oe}4FnU=JP za{V(&x2o}(mYVzr5f`E8`P_Bz99h+3N9gYJ*hRBmb{{&*2r6LX4W|?*?@~)KUDLvYj%1 z0smLO|8IyWnxEP5(xppgYKrm(|BUbdw76z)SKNPSUX!h{rNMvalA4mXV##C6(EkB0 CCQ!Zr literal 0 HcmV?d00001 diff --git a/runatlantis.io/guide/requirements.md b/runatlantis.io/guide/requirements.md index b77a754cd5..c7db091686 100644 --- a/runatlantis.io/guide/requirements.md +++ b/runatlantis.io/guide/requirements.md @@ -6,7 +6,7 @@ * GitHub (public, private or enterprise) * GitLab (public, private or enterprise) * Bitbucket Cloud aka bitbucket.org (public or private) -* Bitbucket Server aka Stash (Coming soon: [https://github.com/runatlantis/atlantis/issues/190](https://github.com/runatlantis/atlantis/issues/190)) +* Bitbucket Server aka Stash ## Remote State Atlantis supports all remote state backends. It **does not** support local state From 5d43445505d0856f0bb3257f53be6cba58d11811 Mon Sep 17 00:00:00 2001 From: Luke Kysow Date: Wed, 25 Jul 2018 13:14:02 +0200 Subject: [PATCH 09/11] Test models --- server/events/models/models_test.go | 57 +++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/server/events/models/models_test.go b/server/events/models/models_test.go index 4e2ed097a6..dee8b1f3f5 100644 --- a/server/events/models/models_test.go +++ b/server/events/models/models_test.go @@ -125,3 +125,60 @@ func TestProject_String(t *testing.T) { Path: "my/path", }).String()) } + +func TestNewProject(t *testing.T) { + cases := []struct { + path string + expPath string + }{ + { + "/", + ".", + }, + { + "./another/path", + "another/path", + }, + { + ".", + ".", + }, + } + + for _, c := range cases { + t.Run(c.path, func(t *testing.T) { + p := models.NewProject("repo/owner", c.path) + Equals(t, c.expPath, p.Path) + }) + } +} + +func TestVCSHostType_ToString(t *testing.T) { + cases := []struct { + vcsType models.VCSHostType + exp string + }{ + { + models.Github, + "Github", + }, + { + models.Gitlab, + "Gitlab", + }, + { + models.BitbucketCloud, + "BitbucketCloud", + }, + { + models.BitbucketServer, + "BitbucketServer", + }, + } + + for _, c := range cases { + t.Run(c.exp, func(t *testing.T) { + Equals(t, c.exp, c.vcsType.String()) + }) + } +} From 6e14c200f3ace8ccff1894be3d34d6573d5a01f2 Mon Sep 17 00:00:00 2001 From: Luke Kysow Date: Wed, 25 Jul 2018 14:49:47 +0200 Subject: [PATCH 10/11] URL encode auth to avoid url issues. --- server/events/models/models.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/server/events/models/models.go b/server/events/models/models.go index 99c090cfb0..668f26da3f 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -79,9 +79,14 @@ func NewRepo(vcsHostType VCSHostType, repoFullName string, cloneURL string, vcsU } } - // Construct clone urls with http auth. Need to do both https and http - // because in GitLab's docs they have some http urls. - auth := fmt.Sprintf("%s:%s@", vcsUser, vcsToken) + // We url encode because we're using them in a URL and weird characters can + // mess up git. + escapedVCSUser := url.QueryEscape(vcsUser) + escapedVCSToken := url.QueryEscape(vcsToken) + auth := fmt.Sprintf("%s:%s@", escapedVCSUser, escapedVCSToken) + + // Construct clone urls with http and https auth. Need to do both + // because Bitbucket supports http. authedCloneURL := strings.Replace(cloneURL, "https://", "https://"+auth, -1) authedCloneURL = strings.Replace(authedCloneURL, "http://", "http://"+auth, -1) From 08c19ed7216d11444ee13ed25304426a8fa9a92a Mon Sep 17 00:00:00 2001 From: Luke Kysow Date: Wed, 25 Jul 2018 14:50:01 +0200 Subject: [PATCH 11/11] Make headings different so TOC works. --- runatlantis.io/docs/deployment.md | 12 +++++------ runatlantis.io/guide/getting-started.md | 28 ++++++++++++------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/runatlantis.io/docs/deployment.md b/runatlantis.io/docs/deployment.md index b6baf8b943..7d846b40c3 100644 --- a/runatlantis.io/docs/deployment.md +++ b/runatlantis.io/docs/deployment.md @@ -125,7 +125,7 @@ set commit statuses. ## Start Atlantis Now you're ready to start Atlantis! The exact command depends on your Git host: -### GitHub +### GitHub Command ```bash atlantis server \ --atlantis-url="$URL" \ @@ -135,7 +135,7 @@ atlantis server \ --repo-whitelist="$REPO_WHITELIST" ``` -### GitHub Enterprise +### GitHub Enterprise Command ```bash HOSTNAME=YOUR_GITHUB_ENTERPRISE_HOSTNAME # ex. github.runatlantis.io atlantis server \ @@ -147,7 +147,7 @@ atlantis server \ --repo-whitelist="$REPO_WHITELIST" ``` -### GitLab +### GitLab Command ```bash atlantis server \ --atlantis-url="$URL" \ @@ -157,7 +157,7 @@ atlantis server \ --repo-whitelist="$REPO_WHITELIST" ``` -### GitLab Enterprise +### GitLab Enterprise Command ```bash HOSTNAME=YOUR_GITLAB_ENTERPRISE_HOSTNAME # ex. gitlab.runatlantis.io atlantis server \ @@ -169,7 +169,7 @@ atlantis server \ --repo-whitelist="$REPO_WHITELIST" ``` -### Bitbucket Cloud (bitbucket.org) +### Bitbucket Cloud (bitbucket.org) Command ```bash atlantis server \ --atlantis-url="$URL" \ @@ -178,7 +178,7 @@ atlantis server \ --repo-whitelist="$REPO_WHITELIST" ``` -### Bitbucket Server (aka Stash) +### Bitbucket Server (aka Stash) Command ```bash BASE_URL=YOUR_BITBUCKET_SERVER_URL # ex. http://bitbucket.mycorp:7990 atlantis server \ diff --git a/runatlantis.io/guide/getting-started.md b/runatlantis.io/guide/getting-started.md index 9de2cfe78f..355552ef28 100644 --- a/runatlantis.io/guide/getting-started.md +++ b/runatlantis.io/guide/getting-started.md @@ -55,7 +55,7 @@ SECRET="{YOUR_RANDOM_STRING}" ## Add Webhook Take the URL that ngrok output and create a webhook in your GitHub, GitLab or Bitbucket repo: -### GitHub or GitHub Enterprise +### GitHub or GitHub Enterprise Webhook

Expand
    @@ -80,7 +80,7 @@ Take the URL that ngrok output and create a webhook in your GitHub, GitLab or Bi
-### GitLab or GitLab Enterprise +### GitLab or GitLab Enterprise Webhook
Expand
    @@ -101,7 +101,7 @@ Take the URL that ngrok output and create a webhook in your GitHub, GitLab or Bi
-### Bitbucket Cloud (bitbucket.org) +### Bitbucket Cloud (bitbucket.org) Webhook
Expand
    @@ -122,7 +122,7 @@ Take the URL that ngrok output and create a webhook in your GitHub, GitLab or Bi
-### Bitbucket Server (aka Stash) +### Bitbucket Server (aka Stash) Webhook
Expand
    @@ -146,7 +146,7 @@ We recommend using a dedicated CI user or creating a new user named **@atlantis* you can use your own user. Here we'll create the access token that Atlantis uses to comment on the pull request and set commit statuses. -### GitHub or GitHub Enterprise +### GitHub or GitHub Enterprise Access Token - follow [https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/#creating-a-token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/#creating-a-token) - create a token with **repo** scope - set the token as an environment variable @@ -154,7 +154,7 @@ set commit statuses. TOKEN="{YOUR_TOKEN}" ``` -### GitLab or GitLab Enterprise +### GitLab or GitLab Enterprise Access Token - follow [https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html#creating-a-personal-access-token](https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html#creating-a-personal-access-token) - create a token with **api** scope - set the token as an environment variable @@ -162,7 +162,7 @@ TOKEN="{YOUR_TOKEN}" TOKEN="{YOUR_TOKEN}" ``` -### Bitbucket Cloud (bitbucket.org) +### Bitbucket Cloud (bitbucket.org) Access Token - follow [https://confluence.atlassian.com/bitbucket/app-passwords-828781300.html#Apppasswords-Createanapppassword](https://confluence.atlassian.com/bitbucket/app-passwords-828781300.html#Apppasswords-Createanapppassword) - Label the password "atlantis" - Select **Pull requests**: **Read** and **Write** so that Atlantis can read your pull requests and write comments to them @@ -171,7 +171,7 @@ TOKEN="{YOUR_TOKEN}" TOKEN="{YOUR_TOKEN}" ``` -### Bitbucket Server (aka Stash) +### Bitbucket Server (aka Stash) Access Token - Click on your avatar in the top right and select **Manage account** - Click **Personal access tokens** in the sidebar - Click **Create a token** @@ -196,7 +196,7 @@ REPO_WHITELIST="$YOUR_GIT_HOST/$YOUR_USERNAME/$YOUR_REPO" ``` Now you can start Atlantis. The exact command differs depending on your Git Host: -### GitHub +### GitHub Command ```bash atlantis server \ --atlantis-url="$URL" \ @@ -206,7 +206,7 @@ atlantis server \ --repo-whitelist="$REPO_WHITELIST" ``` -### GitHub Enterprise +### GitHub Enterprise Command ```bash HOSTNAME=YOUR_GITHUB_ENTERPRISE_HOSTNAME # ex. github.runatlantis.io atlantis server \ @@ -218,7 +218,7 @@ atlantis server \ --repo-whitelist="$REPO_WHITELIST" ``` -### GitLab +### GitLab Command ```bash atlantis server \ --atlantis-url="$URL" \ @@ -228,7 +228,7 @@ atlantis server \ --repo-whitelist="$REPO_WHITELIST" ``` -### GitLab Enterprise +### GitLab Enterprise Command ```bash HOSTNAME=YOUR_GITLAB_ENTERPRISE_HOSTNAME # ex. gitlab.runatlantis.io atlantis server \ @@ -240,7 +240,7 @@ atlantis server \ --repo-whitelist="$REPO_WHITELIST" ``` -### Bitbucket Cloud (bitbucket.org) +### Bitbucket Cloud (bitbucket.org) Command ```bash atlantis server \ --atlantis-url="$URL" \ @@ -249,7 +249,7 @@ atlantis server \ --repo-whitelist="$REPO_WHITELIST" ``` -### Bitbucket Server (aka Stash) +### Bitbucket Server (aka Stash) Command ```bash BASE_URL=YOUR_BITBUCKET_SERVER_URL # ex. http://bitbucket.mycorp:7990 atlantis server \