Merge pull request #194 from runatlantis/bitbucket-server

Bitbucket server aka stash
This commit is contained in:
Luke Kysow
2018-07-25 14:58:35 +02:00
committed by GitHub
37 changed files with 2594 additions and 458 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 &gt; 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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}"
}
}

View 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": []
}
}

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

View 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"
}
]
}
}

View 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": []
}
}

View 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": []
}
}

View 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"
}
}
}
}

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
package bitbucketserver

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

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

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

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

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

View File

@@ -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
View File

@@ -0,0 +1 @@
package vcs

View File

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

View File

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

View File

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

View File

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

View File

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