mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-08-03 16:58:47 +00:00
Merge pull request #194 from runatlantis/bitbucket-server
Bitbucket server aka stash
This commit is contained in:
@@ -56,7 +56,7 @@ atlantis server --gh-user <your username> --gh-token <your 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
|
||||
|
||||
109
cmd/server.go
109
cmd/server.go
@@ -15,6 +15,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -22,6 +23,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"
|
||||
)
|
||||
@@ -32,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"
|
||||
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 = "bitbucket.org"
|
||||
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"
|
||||
@@ -81,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: "Currently not supported! We only support bitbucket cloud (aka bitbucket.org) at this time.",
|
||||
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,
|
||||
@@ -108,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. " +
|
||||
@@ -128,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. " +
|
||||
@@ -143,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,
|
||||
@@ -343,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
|
||||
@@ -364,10 +377,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
|
||||
@@ -390,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
|
||||
}
|
||||
|
||||
@@ -437,13 +457,16 @@ 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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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{}{
|
||||
@@ -347,14 +334,15 @@ 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, "bitbucket.org", passedConfig.BitbucketHostname)
|
||||
Equals(t, "", passedConfig.GitlabWebhookSecret)
|
||||
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)
|
||||
@@ -435,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)
|
||||
@@ -463,17 +452,19 @@ 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)
|
||||
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)
|
||||
@@ -488,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"
|
||||
@@ -516,17 +509,19 @@ 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)
|
||||
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)
|
||||
@@ -541,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"
|
||||
@@ -563,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
|
||||
}
|
||||
@@ -594,17 +593,19 @@ 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)
|
||||
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)
|
||||
@@ -619,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"
|
||||
@@ -640,42 +643,46 @@ 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)
|
||||
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)
|
||||
@@ -687,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)
|
||||
@@ -740,17 +758,19 @@ 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)
|
||||
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)
|
||||
@@ -759,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 {
|
||||
|
||||
@@ -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**
|
||||
<img src="../guide/images/bitbucket-webhook.png" alt="Bitbucket Webhook" style="max-height: 500px">
|
||||
|
||||
### 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**<img src="../guide/images/bitbucket-server-webhook.png" alt="Bitbucket Webhook" style="max-height: 500px;">
|
||||
|
||||
## 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,10 +114,18 @@ 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:
|
||||
|
||||
### GitHub
|
||||
### GitHub Command
|
||||
```bash
|
||||
atlantis server \
|
||||
--atlantis-url="$URL" \
|
||||
@@ -114,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 \
|
||||
@@ -126,7 +147,7 @@ atlantis server \
|
||||
--repo-whitelist="$REPO_WHITELIST"
|
||||
```
|
||||
|
||||
### GitLab
|
||||
### GitLab Command
|
||||
```bash
|
||||
atlantis server \
|
||||
--atlantis-url="$URL" \
|
||||
@@ -136,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 \
|
||||
@@ -148,7 +169,7 @@ atlantis server \
|
||||
--repo-whitelist="$REPO_WHITELIST"
|
||||
```
|
||||
|
||||
### Bitbucket Cloud (bitbucket.org)
|
||||
### Bitbucket Cloud (bitbucket.org) Command
|
||||
```bash
|
||||
atlantis server \
|
||||
--atlantis-url="$URL" \
|
||||
@@ -157,6 +178,18 @@ atlantis server \
|
||||
--repo-whitelist="$REPO_WHITELIST"
|
||||
```
|
||||
|
||||
### Bitbucket Server (aka Stash) Command
|
||||
```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
|
||||
|
||||
@@ -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**
|
||||
### GitHub or GitHub Enterprise Webhook
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
<ul>
|
||||
<li>Go to your repo's settings</li>
|
||||
<li>Select <strong>Webhooks</strong> or <strong>Hooks</strong> in the sidebar</li>
|
||||
<li>Click <strong>Add webhook</strong></li>
|
||||
<li>set <strong>Payload URL</strong> to your ngrok url with <code>/events</code> at the end. Ex. <code>https://c5004d84.ngrok.io/events</code></li>
|
||||
<li>double-check you added <code>/events</code> to the end of your URL.</li>
|
||||
<li>set <strong>Content type</strong> to <code>application/json</code></li>
|
||||
<li>set <strong>Secret</strong> to your random string</li>
|
||||
<li>select <strong>Let me select individual events</strong></li>
|
||||
<li>check the boxes
|
||||
<ul>
|
||||
<li><strong>Pull request reviews</strong></li>
|
||||
<li><strong>Pushes</strong></li>
|
||||
<li><strong>Issue comments</strong></li>
|
||||
<li><strong>Pull requests</strong></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>leave <strong>Active</strong> checked</li>
|
||||
<li>click <strong>Add webhook</strong></li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
### 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**
|
||||
### GitLab or GitLab Enterprise Webhook
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
<ul>
|
||||
<li>Go to your repo's home page</li>
|
||||
<li>Click <strong>Settings > Integrations</strong> in the sidebar</li>
|
||||
<li>set <strong>URL</strong> to your ngrok url with <code>/events</code> at the end. Ex. <code>https://c5004d84.ngrok.io/events</code></li>
|
||||
<li>double-check you added <code>/events</code> to the end of your URL.</li>
|
||||
<li>set <strong>Secret Token</strong> to your random string</li>
|
||||
<li>check the boxes
|
||||
<ul>
|
||||
<li><strong>Push events</strong></li>
|
||||
<li><strong>Comments</strong></li>
|
||||
<li><strong>Merge Request events</strong></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>leave <strong>Enable SSL verification</strong> checked</li>
|
||||
<li>click <strong>Add webhook</strong></li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
### Bitbucket Cloud (bitbucket.org) Webhook
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
<ul>
|
||||
<li>Go to your repo's home page</li>
|
||||
<li>Click <strong>Settings</strong> in the sidebar</li>
|
||||
<li>Click <strong>Webhooks</strong> under the <strong>WORKFLOW</strong> section</li>
|
||||
<li>Click <strong>Add webhook</strong></li>
|
||||
<li>Enter "Atlantis" for <strong>Title</strong></li>
|
||||
<li>Set <strong>URL</strong> to your ngrok url with <code>/events</code> at the end. Ex. <code>https://c5004d84.ngrok.io/events</code></li>
|
||||
<li>Double-check you added <code>/events</code> to the end of your URL.</li>
|
||||
<li>Keep <strong>Status</strong> as Active</li>
|
||||
<li>Don't check <strong>Skip certificate validation</strong> because NGROK has a valid cert.</li>
|
||||
<li>Select <strong>Choose from a full list of triggers</strong></li>
|
||||
<li>Under <strong>Repository</strong><strong>un</strong>check everything</li>
|
||||
<li>Under <strong>Issues</strong> leave everything <strong>un</strong>checked</li>
|
||||
<li>Under <strong>Pull Request</strong>, select: Created, Updated, Merged, Declined and Comment created</li>
|
||||
<li>Click <strong>Save</strong><img src="./images/bitbucket-webhook.png" alt="Bitbucket Webhook" style="max-height: 500px;"></li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
### Bitbucket Server (aka Stash) Webhook
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
<ul>
|
||||
<li>Go to your repo's home page</li>
|
||||
<li>Click <strong>Settings</strong> in the sidebar</li>
|
||||
<li>Click <strong>Webhooks</strong> under the <strong>WORKFLOW</strong> section</li>
|
||||
<li>Click <strong>Create webhook</strong></li>
|
||||
<li>Enter "Atlantis" for <strong>Name</strong></li>
|
||||
<li>Set <strong>URL</strong> to your ngrok url with <code>/events</code> at the end. Ex. <code>https://c5004d84.ngrok.io/events</code></li>
|
||||
<li>Double-check you added <code>/events</code> to the end of your URL.</li>
|
||||
<li>Set <strong>Secret</strong> to your random string</li>
|
||||
<li>Under <strong>Repository</strong> select <strong>Push</strong></li>
|
||||
<li>Under <strong>Pull Request</strong>, select: Opened, Modified, Merged, Declined, Deleted and Comment added</li>
|
||||
<li>Click <strong>Save</strong><img src="./images/bitbucket-server-webhook.png" alt="Bitbucket Webhook" style="max-height: 500px;"></li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
### 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**
|
||||
<img src="./images/bitbucket-webhook.png" alt="Bitbucket Webhook" style="max-height: 500px">
|
||||
|
||||
## 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 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
|
||||
@@ -115,7 +154,7 @@ set commit statuses.
|
||||
TOKEN="{YOUR_TOKEN}"
|
||||
```
|
||||
|
||||
### GitLab
|
||||
### 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
|
||||
@@ -123,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
|
||||
@@ -132,6 +171,17 @@ TOKEN="{YOUR_TOKEN}"
|
||||
TOKEN="{YOUR_TOKEN}"
|
||||
```
|
||||
|
||||
### 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**
|
||||
- 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,10 +190,13 @@ 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:
|
||||
|
||||
### GitHub
|
||||
### GitHub Command
|
||||
```bash
|
||||
atlantis server \
|
||||
--atlantis-url="$URL" \
|
||||
@@ -153,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 \
|
||||
@@ -165,7 +218,7 @@ atlantis server \
|
||||
--repo-whitelist="$REPO_WHITELIST"
|
||||
```
|
||||
|
||||
### GitLab
|
||||
### GitLab Command
|
||||
```bash
|
||||
atlantis server \
|
||||
--atlantis-url="$URL" \
|
||||
@@ -175,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 \
|
||||
@@ -187,7 +240,7 @@ atlantis server \
|
||||
--repo-whitelist="$REPO_WHITELIST"
|
||||
```
|
||||
|
||||
### Bitbucket Cloud (bitbucket.org)
|
||||
### Bitbucket Cloud (bitbucket.org) Command
|
||||
```bash
|
||||
atlantis server \
|
||||
--atlantis-url="$URL" \
|
||||
@@ -196,6 +249,18 @@ atlantis server \
|
||||
--repo-whitelist="$REPO_WHITELIST"
|
||||
```
|
||||
|
||||
### Bitbucket Server (aka Stash) Command
|
||||
```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
|
||||
|
||||
BIN
runatlantis.io/guide/images/bitbucket-server-webhook.png
Normal file
BIN
runatlantis.io/guide/images/bitbucket-server-webhook.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 216 KiB |
@@ -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
|
||||
|
||||
@@ -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, models.BitbucketServer:
|
||||
if maybePull == nil {
|
||||
err = errors.New("pull request should not be nil, this is a bug!")
|
||||
}
|
||||
@@ -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
|
||||
@@ -253,7 +257,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.
|
||||
|
||||
@@ -24,7 +24,8 @@ 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"
|
||||
"github.com/runatlantis/atlantis/server/events/vcs/bitbucketserver"
|
||||
"gopkg.in/go-playground/validator.v9"
|
||||
)
|
||||
|
||||
@@ -123,47 +124,52 @@ 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
|
||||
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
|
||||
}
|
||||
|
||||
// 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":
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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 bitbucket.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":
|
||||
@@ -180,7 +186,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 +195,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,15 +220,16 @@ 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
|
||||
}
|
||||
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.PullMergedHeader, 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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
@@ -526,7 +527,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 +538,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) {
|
||||
@@ -556,7 +557,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 +578,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 +642,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 +663,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 +671,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 +699,184 @@ 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -244,9 +244,81 @@ 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 {
|
||||
ret0 = result[0].(models.PullRequestEventType)
|
||||
}
|
||||
}
|
||||
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 {
|
||||
@@ -521,23 +593,104 @@ 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]))
|
||||
for u, param := range params[0] {
|
||||
_param0[u] = param.(string)
|
||||
}
|
||||
}
|
||||
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]))
|
||||
|
||||
@@ -63,19 +63,30 @@ 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
|
||||
// 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)
|
||||
|
||||
@@ -108,9 +119,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"
|
||||
@@ -237,7 +248,8 @@ type VCSHostType int
|
||||
const (
|
||||
Github VCSHostType = iota
|
||||
Gitlab
|
||||
Bitbucket
|
||||
BitbucketCloud
|
||||
BitbucketServer
|
||||
)
|
||||
|
||||
func (h VCSHostType) String() string {
|
||||
@@ -246,8 +258,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 "<missing String() implementation>"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -61,7 +79,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")
|
||||
@@ -107,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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
233
server/events/testdata/bitbucket-cloud-pull-event-rejected.json
vendored
Normal file
233
server/events/testdata/bitbucket-cloud-pull-event-rejected.json
vendored
Normal file
@@ -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": "<p>main.tf edited online with Bitbucket</p>",
|
||||
"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}"
|
||||
}
|
||||
}
|
||||
105
server/events/testdata/bitbucket-server-comment-event.json
vendored
Normal file
105
server/events/testdata/bitbucket-server-comment-event.json
vendored
Normal file
@@ -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": "FK",
|
||||
"id": 1,
|
||||
"name": "atlantis-fork",
|
||||
"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": []
|
||||
}
|
||||
}
|
||||
66
server/events/testdata/bitbucket-server-get-pull-changes.json
vendored
Normal file
66
server/events/testdata/bitbucket-server-get-pull-changes.json
vendored
Normal file
@@ -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
|
||||
}
|
||||
156
server/events/testdata/bitbucket-server-get-pull.json
vendored
Normal file
156
server/events/testdata/bitbucket-server-get-pull.json
vendored
Normal file
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
84
server/events/testdata/bitbucket-server-pull-event-created.json
vendored
Normal file
84
server/events/testdata/bitbucket-server-pull-event-created.json
vendored
Normal file
@@ -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": []
|
||||
}
|
||||
}
|
||||
85
server/events/testdata/bitbucket-server-pull-event-declined.json
vendored
Normal file
85
server/events/testdata/bitbucket-server-pull-event-declined.json
vendored
Normal file
@@ -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": []
|
||||
}
|
||||
}
|
||||
108
server/events/testdata/bitbucket-server-pull-event-merged.json
vendored
Normal file
108
server/events/testdata/bitbucket-server-pull-event-merged.json
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"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",
|
||||
"id": 2,
|
||||
"name": "atlantis-example",
|
||||
"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": "FK",
|
||||
"id": 1,
|
||||
"name": "atlantis-fork",
|
||||
"public": false,
|
||||
"type": "NORMAL"
|
||||
},
|
||||
"public": false
|
||||
},
|
||||
"project": {
|
||||
"key": "FK",
|
||||
"id": 2,
|
||||
"name": "atlantis-fork",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
6
server/events/vcs/bitbucketcloud/bitbucketcloud.go
Normal file
6
server/events/vcs/bitbucketcloud/bitbucketcloud.go
Normal file
@@ -0,0 +1,6 @@
|
||||
// Package bitbucketcloud holds code for Bitbucket Cloud aka (bitbucket.org).
|
||||
// It is separate from bitbucketserver because Bitbucket Server has different
|
||||
// APIs.
|
||||
package bitbucketcloud
|
||||
|
||||
const BaseURL = "https://api.bitbucket.org"
|
||||
@@ -1,4 +1,4 @@
|
||||
package bitbucket
|
||||
package bitbucketcloud
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -7,18 +7,10 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
)
|
||||
|
||||
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"
|
||||
"gopkg.in/go-playground/validator.v9"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
@@ -29,31 +21,21 @@ type Client struct {
|
||||
AtlantisURL string
|
||||
}
|
||||
|
||||
// NewClient builds a bitbucket 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: BaseURL,
|
||||
AtlantisURL: atlantisURL,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetModifiedFiles returns the names of files that were modified in the merge request.
|
||||
@@ -61,9 +43,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 {
|
||||
@@ -71,7 +53,10 @@ 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))
|
||||
}
|
||||
for _, v := range diffStat.Values {
|
||||
if v.Old != nil {
|
||||
@@ -105,21 +90,24 @@ 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
|
||||
}
|
||||
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))
|
||||
}
|
||||
for _, participant := range pullResp.Participants {
|
||||
if *participant.Approved {
|
||||
@@ -148,7 +136,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")
|
||||
}
|
||||
@@ -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,8 +68,8 @@ func TestClient_GetModifiedFilesPagination(t *testing.T) {
|
||||
defer testServer.Close()
|
||||
|
||||
serverURL = testServer.URL
|
||||
client, err := bitbucket.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",
|
||||
@@ -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,8 +129,8 @@ func TestClient_GetModifiedFilesOldNil(t *testing.T) {
|
||||
}))
|
||||
defer testServer.Close()
|
||||
|
||||
client, err := bitbucket.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",
|
||||
@@ -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{
|
||||
@@ -1,4 +1,12 @@
|
||||
package bitbucket
|
||||
package bitbucketcloud
|
||||
|
||||
const (
|
||||
PullCreatedHeader = "pullrequest:created"
|
||||
PullUpdatedHeader = "pullrequest:updated"
|
||||
PullFulfilledHeader = "pullrequest:fulfilled"
|
||||
PullRejectedHeader = "pullrequest:rejected"
|
||||
PullCommentCreatedHeader = "pullrequest:comment_created"
|
||||
)
|
||||
|
||||
type CommentEvent struct {
|
||||
CommonEventData
|
||||
1
server/events/vcs/bitbucketserver/bitbucketserver.go
Normal file
1
server/events/vcs/bitbucketserver/bitbucketserver.go
Normal file
@@ -0,0 +1 @@
|
||||
package bitbucketserver
|
||||
222
server/events/vcs/bitbucketserver/client.go
Normal file
222
server/events/vcs/bitbucketserver/client.go
Normal file
@@ -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, 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))
|
||||
}
|
||||
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, errors.Wrapf(err, "Could not parse response %q", string(resp))
|
||||
}
|
||||
if err := validator.New().Struct(pullResp); err != nil {
|
||||
return false, errors.Wrapf(err, "API response %q was missing fields", string(resp))
|
||||
}
|
||||
for _, 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
|
||||
}
|
||||
141
server/events/vcs/bitbucketserver/client_test.go
Normal file
141
server/events/vcs/bitbucketserver/client_test.go
Normal file
@@ -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)
|
||||
}
|
||||
66
server/events/vcs/bitbucketserver/models.go
Normal file
66
server/events/vcs/bitbucketserver/models.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package bitbucketserver
|
||||
|
||||
const (
|
||||
PullCreatedHeader = "pr:opened"
|
||||
PullMergedHeader = "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"`
|
||||
}
|
||||
71
server/events/vcs/bitbucketserver/request_validation.go
Normal file
71
server/events/vcs/bitbucketserver/request_validation.go
Normal file
@@ -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
|
||||
}
|
||||
24
server/events/vcs/bitbucketserver/request_validation_test.go
Normal file
24
server/events/vcs/bitbucketserver/request_validation_test.go
Normal file
@@ -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)
|
||||
}
|
||||
@@ -14,7 +14,6 @@
|
||||
package vcs
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
)
|
||||
|
||||
@@ -32,74 +31,46 @@ 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 {
|
||||
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{
|
||||
GitlabClient: gitlabClient,
|
||||
GithubClient: githubClient,
|
||||
BitbucketClient: bitbucketClient,
|
||||
clients: map[models.VCSHostType]Client{
|
||||
models.Github: githubClient,
|
||||
models.Gitlab: gitlabClient,
|
||||
models.BitbucketCloud: bitbucketCloudClient,
|
||||
models.BitbucketServer: bitbucketServerClient,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
1
server/events/vcs/vcs.go
Normal file
1
server/events/vcs/vcs.go
Normal file
@@ -0,0 +1 @@
|
||||
package vcs
|
||||
@@ -20,16 +20,23 @@ 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"
|
||||
"github.com/runatlantis/atlantis/server/events/vcs/bitbucketcloud"
|
||||
"github.com/runatlantis/atlantis/server/events/vcs/bitbucketserver"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
)
|
||||
|
||||
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"
|
||||
const bitbucketServerSignatureHeader = "X-Hub-Signature"
|
||||
|
||||
// EventsController handles all webhook requests which signify 'events' in the
|
||||
// VCS host, ex. GitHub.
|
||||
@@ -39,22 +46,26 @@ 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.
|
||||
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.
|
||||
@@ -76,20 +87,32 @@ 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) {
|
||||
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")
|
||||
}
|
||||
|
||||
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
|
||||
@@ -110,26 +133,56 @@ 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(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 {
|
||||
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, bitbucketRequestIDHeader, reqID)
|
||||
e.respond(w, logging.Debug, http.StatusOK, "Ignoring unsupported event type %s %s=%s", eventType, bitbucketCloudRequestIDHeader, reqID)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EventsController) handleBitbucketServerPost(w http.ResponseWriter, r *http.Request) {
|
||||
eventType := r.Header.Get(bitbucketEventTypeHeader)
|
||||
reqID := r.Header.Get(bitbucketServerRequestIDHeader)
|
||||
sig := r.Header.Get(bitbucketServerSignatureHeader)
|
||||
defer r.Body.Close() // nolint: errcheck
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
e.respond(w, logging.Error, http.StatusBadRequest, "Unable to read body: %s %s=%s", err, bitbucketServerRequestIDHeader, reqID)
|
||||
return
|
||||
}
|
||||
if len(e.BitbucketWebhookSecret) > 0 {
|
||||
if err := bitbucketserver.ValidateSignature(body, sig, e.BitbucketWebhookSecret); err != nil {
|
||||
e.respond(w, logging.Warn, http.StatusBadRequest, errors.Wrap(err, "request did not pass validation").Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
switch eventType {
|
||||
case bitbucketserver.PullCreatedHeader, bitbucketserver.PullMergedHeader, bitbucketserver.PullDeclinedHeader:
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,23 +205,44 @@ 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, 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)
|
||||
// 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, 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)
|
||||
e.handleCommentEvent(w, baseRepo, &headRepo, &pull, user, pull.Num, comment, models.BitbucketCloud)
|
||||
}
|
||||
|
||||
func (e *EventsController) HandleBitbucketCloudPullRequestEvent(w http.ResponseWriter, eventType string, body []byte, reqID string) {
|
||||
pull, baseRepo, headRepo, user, err := e.Parser.ParseBitbucketCloudPullEvent(body)
|
||||
if err != nil {
|
||||
e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing pull data: %s %s=%s", err, bitbucketCloudRequestIDHeader, reqID)
|
||||
return
|
||||
}
|
||||
pullEventType := e.Parser.GetBitbucketCloudEventType(eventType)
|
||||
e.Logger.Info("identified event as type %q", pullEventType.String())
|
||||
e.handlePullRequestEvent(w, baseRepo, headRepo, pull, user, pullEventType)
|
||||
}
|
||||
|
||||
func (e *EventsController) HandleBitbucketServerPullRequestEvent(w http.ResponseWriter, eventType string, body []byte, reqID string) {
|
||||
pull, baseRepo, headRepo, user, err := e.Parser.ParseBitbucketServerPullEvent(body)
|
||||
if err != nil {
|
||||
e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing pull data: %s %s=%s", err, bitbucketServerRequestIDHeader, reqID)
|
||||
return
|
||||
}
|
||||
pullEventType := e.Parser.GetBitbucketServerEventType(eventType)
|
||||
e.Logger.Info("identified event as type %q", pullEventType.String())
|
||||
e.handlePullRequestEvent(w, baseRepo, headRepo, pull, user, pullEventType)
|
||||
}
|
||||
@@ -233,7 +307,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
|
||||
|
||||
@@ -301,12 +301,12 @@ 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.Bitbucket},
|
||||
SupportedVCSHosts: []models.VCSHostType{models.Gitlab, models.Github, models.BitbucketCloud},
|
||||
VCSClient: e2eVCSClient,
|
||||
}
|
||||
return ctrl, e2eVCSClient, e2eGithubGetter, workingDir
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -40,7 +40,8 @@ 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/vcs/bitbucketserver"
|
||||
"github.com/runatlantis/atlantis/server/events/webhooks"
|
||||
"github.com/runatlantis/atlantis/server/events/yaml"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
@@ -81,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"`
|
||||
@@ -137,7 +139,8 @@ 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 bitbucketCloudClient *bitbucketcloud.Client
|
||||
var bitbucketServerClient *bitbucketserver.Client
|
||||
if userConfig.GithubUser != "" {
|
||||
supportedVCSHosts = append(supportedVCSHosts, models.Github)
|
||||
var err error
|
||||
@@ -168,17 +171,25 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
}
|
||||
}
|
||||
if userConfig.BitbucketUser != "" {
|
||||
supportedVCSHosts = append(supportedVCSHosts, models.Bitbucket)
|
||||
var err error
|
||||
bitbucketClient, err = bitbucket.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.BitbucketBaseURL == bitbucketcloud.BaseURL {
|
||||
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.BitbucketBaseURL,
|
||||
userConfig.AtlantisURL)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "setting up Bitbucket Server client")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +207,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, 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 +243,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.BitbucketBaseURL,
|
||||
}
|
||||
commentParser := &events.CommentParser{
|
||||
GithubUser: userConfig.GithubUser,
|
||||
@@ -308,13 +320,14 @@ 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,
|
||||
BitbucketWebhookSecret: []byte(userConfig.BitbucketWebhookSecret),
|
||||
}
|
||||
return &Server{
|
||||
AtlantisVersion: config.AtlantisVersion,
|
||||
|
||||
Reference in New Issue
Block a user