Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Bitbucket Context path #747

Merged
merged 1 commit into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/spec/v1beta2/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -1539,6 +1539,8 @@ kubectl create secret generic bb-server-token --from-literal=token=<token>
The HTTP access token must have `Repositories (Read/Write)` permission for
the repository specified in `.spec.address`.

**NOTE:** Please provide HTTPS clone URL in the `address` field of this provider. SSH URLs are not supported by this provider type.

#### Azure DevOps

When `.spec.type` is set to `azuredevops`, the referenced secret must contain a key called `token` with the value set to a
Expand Down
2 changes: 2 additions & 0 deletions docs/spec/v1beta3/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -1567,6 +1567,8 @@ kubectl create secret generic bb-server-token --from-literal=token=<token>
The HTTP access token must have `Repositories (Read/Write)` permission for
the repository specified in `.spec.address`.

**NOTE:** Please provide HTTPS clone URL in the `address` field of this provider. SSH URLs are not supported by this provider type.

#### Azure DevOps

When `.spec.type` is set to `azuredevops`, the referenced secret must contain a key called `token` with the value set to a
Expand Down
78 changes: 47 additions & 31 deletions internal/notifier/bitbucketserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,20 @@ import (

// BitbucketServer is a notifier for BitBucket Server and Data Center.
type BitbucketServer struct {
ProjectKey string
RepositorySlug string
ProviderUID string
Url *url.URL
ProviderAddress string
Host string
Username string
Password string
Token string
Client *retryablehttp.Client
}

const (
bbServerEndPointTmpl = "/rest/api/latest/projects/%[1]s/repos/%[2]s/commits/%[3]s/builds"
bbServerEndPointCommitsTmpl = "%[1]s/rest/api/latest/projects/%[2]s/repos/%[3]s/commits"
bbServerEndPointBuildsTmpl = "%[1]s/builds"
bbServerGetBuildStatusQueryString = "key"
bbServerSourceCodeMgmtString = "/scm/"
)

type bbServerBuildStatus struct {
Expand Down Expand Up @@ -82,18 +82,11 @@ type bbServerBuildStatusSetRequest struct {

// NewBitbucketServer creates and returns a new BitbucketServer notifier.
func NewBitbucketServer(providerUID string, addr string, token string, certPool *x509.CertPool, username string, password string) (*BitbucketServer, error) {
hst, id, err := parseBitbucketServerGitAddress(addr)
url, err := parseBitbucketServerGitAddress(addr)
if err != nil {
return nil, err
}

comp := strings.Split(id, "/")
if len(comp) != 2 {
return nil, fmt.Errorf("invalid repository id %q", id)
}
projectkey := comp[0]
reposlug := comp[1]

httpClient := retryablehttp.NewClient()
if certPool != nil {
httpClient.HTTPClient.Transport = &http.Transport{
Expand All @@ -114,10 +107,8 @@ func NewBitbucketServer(providerUID string, addr string, token string, certPool
}

return &BitbucketServer{
ProjectKey: projectkey,
RepositorySlug: reposlug,
ProviderUID: providerUID,
Host: hst,
Url: url,
ProviderAddress: addr,
Token: token,
Username: username,
Expand Down Expand Up @@ -151,14 +142,14 @@ func (b BitbucketServer) Post(ctx context.Context, event eventv1.Event) error {
// key has a limitation of 40 characters in bitbucket api
key := sha1String(id)

u := b.Host + b.createApiPath(rev)
dupe, err := b.duplicateBitbucketServerStatus(ctx, rev, state, name, desc, id, key, u)
u := b.Url.JoinPath(b.createBuildPath(rev)).String()
dupe, err := b.duplicateBitbucketServerStatus(ctx, state, name, desc, key, u)
if err != nil {
return fmt.Errorf("could not get existing commit status: %w", err)
}

if !dupe {
_, err = b.postBuildStatus(ctx, rev, state, name, desc, id, key, u)
_, err = b.postBuildStatus(ctx, state, name, desc, key, u)
if err != nil {
return fmt.Errorf("could not post build status: %w", err)
}
Expand All @@ -178,9 +169,9 @@ func (b BitbucketServer) state(severity string) (string, error) {
}
}

func (b BitbucketServer) duplicateBitbucketServerStatus(ctx context.Context, rev, state, name, desc, id, key, u string) (bool, error) {
func (b BitbucketServer) duplicateBitbucketServerStatus(ctx context.Context, state, name, desc, key, u string) (bool, error) {
// Prepare request object
req, err := b.prepareCommonRequest(ctx, u, nil, http.MethodGet, key, rev)
req, err := b.prepareCommonRequest(ctx, u, nil, http.MethodGet)
if err != nil {
return false, fmt.Errorf("could not check duplicate commit status: %w", err)
}
Expand Down Expand Up @@ -219,7 +210,7 @@ func (b BitbucketServer) duplicateBitbucketServerStatus(ctx context.Context, rev
return false, nil
}

func (b BitbucketServer) postBuildStatus(ctx context.Context, rev, state, name, desc, id, key, url string) (*http.Response, error) {
func (b BitbucketServer) postBuildStatus(ctx context.Context, state, name, desc, key, url string) (*http.Response, error) {
//Prepare json body
j := &bbServerBuildStatusSetRequest{
Key: key,
Expand All @@ -235,7 +226,7 @@ func (b BitbucketServer) postBuildStatus(ctx context.Context, rev, state, name,
}

//Prepare request
req, err := b.prepareCommonRequest(ctx, url, p, http.MethodPost, key, rev)
req, err := b.prepareCommonRequest(ctx, url, p, http.MethodPost)
if err != nil {
return nil, fmt.Errorf("failed preparing request for post build commit status: %w", err)
}
Expand All @@ -257,21 +248,46 @@ func (b BitbucketServer) postBuildStatus(ctx context.Context, rev, state, name,
return resp, nil
}

func (b BitbucketServer) createApiPath(rev string) string {
return fmt.Sprintf(bbServerEndPointTmpl, b.ProjectKey, b.RepositorySlug, rev)
func (b BitbucketServer) createBuildPath(rev string) string {
return fmt.Sprintf(bbServerEndPointBuildsTmpl, rev)
}

func parseBitbucketServerGitAddress(s string) (string, string, error) {
host, id, err := parseGitAddress(s)
func parseBitbucketServerGitAddress(s string) (*url.URL, error) {
u, err := url.Parse(s)
if err != nil {
return "", "", fmt.Errorf("could not parse git address: %w", err)
return nil, fmt.Errorf("could not parse git address: %w", err)
}
if u.Scheme != "http" && u.Scheme != "https" {
return nil, fmt.Errorf("could not parse git address: unsupported scheme type in address: %s. Must be http or https", u.Scheme)
}

idWithContext := strings.TrimSuffix(u.Path, ".git")

// /scm/ is always part of http/https clone urls : https://community.atlassian.com/t5/Bitbucket-questions/remote-url-in-Bitbucket-server-what-does-scm-represent-is-it/qaq-p/2060987
lastIndex := strings.LastIndex(idWithContext, bbServerSourceCodeMgmtString)
if lastIndex < 0 {
return nil, fmt.Errorf("could not parse git address: supplied provider address is not http(s) git clone url")
}
//Remove "scm/" --> https://community.atlassian.com/t5/Bitbucket-questions/remote-url-in-Bitbucket-server-what-does-scm-represent-is-it/qaq-p/2060987
id = strings.TrimPrefix(id, "scm/")
return host, id, nil

// Handle context scenarios --> https://confluence.atlassian.com/bitbucketserver/change-bitbucket-s-context-path-776640153.html
cntxtPath := idWithContext[:lastIndex] // Context path is anything that comes before last /scm/

id := idWithContext[lastIndex+len(bbServerSourceCodeMgmtString):] // Remove last `/scm/` from id as it is not used in API calls

comp := strings.Split(id, "/")
if len(comp) != 2 {
return nil, fmt.Errorf("could not parse git address: invalid repository id %q", id)
}
projectkey := comp[0]
reposlug := comp[1]

// Update the path till commits endpoint. The final builds endpoint would be added in Post function.
u.Path = fmt.Sprintf(bbServerEndPointCommitsTmpl, cntxtPath, projectkey, reposlug)

return u, nil
}

func (b BitbucketServer) prepareCommonRequest(ctx context.Context, path string, body io.Reader, method string, key, rev string) (*retryablehttp.Request, error) {
func (b BitbucketServer) prepareCommonRequest(ctx context.Context, path string, body io.Reader, method string) (*retryablehttp.Request, error) {
req, err := retryablehttp.NewRequestWithContext(ctx, method, path, body)
if err != nil {
return nil, fmt.Errorf("could not prepare request: %w", err)
Expand Down
62 changes: 60 additions & 2 deletions internal/notifier/bitbucketserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,69 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestNewBitbucketServerBasic(t *testing.T) {
func TestNewBitbucketServerBasicNoContext(t *testing.T) {
b, err := NewBitbucketServer("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://example.com:7990/scm/projectfoo/repobar.git", "", nil, "dummyuser", "testpassword")
assert.Nil(t, err)
assert.Equal(t, b.Username, "dummyuser")
assert.Equal(t, b.Password, "testpassword")
assert.Equal(t, b.Url.Scheme, "https")
assert.Equal(t, b.Url.Host, "example.com:7990")
}

func TestNewBitbucketServerBasicWithContext(t *testing.T) {
b, err := NewBitbucketServer("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://example.com:7990/context/scm/projectfoo/repobar.git", "", nil, "dummyuser", "testpassword")
assert.Nil(t, err)
assert.Equal(t, b.Username, "dummyuser")
assert.Equal(t, b.Password, "testpassword")
assert.Equal(t, b.Url.Scheme, "https")
assert.Equal(t, b.Url.Host, "example.com:7990")
}

func TestBitbucketServerApiPathNoContext(t *testing.T) {
b, err := NewBitbucketServer("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://example.com:7990/scm/projectfoo/repobar.git", "", nil, "dummyuser", "testpassword")
assert.Nil(t, err)
u := b.Url.JoinPath(b.createBuildPath("00151b98e303e19610378e6f1c49e31e5e80cd3b")).String()
assert.Equal(t, u, "https://example.com:7990/rest/api/latest/projects/projectfoo/repos/repobar/commits/00151b98e303e19610378e6f1c49e31e5e80cd3b/builds")
}

func TestBitbucketServerApiPathOneWordContext(t *testing.T) {
b, err := NewBitbucketServer("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://example.com:7990/context1/scm/projectfoo/repobar.git", "", nil, "dummyuser", "testpassword")
assert.Nil(t, err)
u := b.Url.JoinPath(b.createBuildPath("00151b98e303e19610378e6f1c49e31e5e80cd3b")).String()
assert.Equal(t, u, "https://example.com:7990/context1/rest/api/latest/projects/projectfoo/repos/repobar/commits/00151b98e303e19610378e6f1c49e31e5e80cd3b/builds")
}

func TestBitbucketServerApiPathMultipleWordContext(t *testing.T) {
b, err := NewBitbucketServer("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://example.com:7990/context1/context2/context3/scm/projectfoo/repobar.git", "", nil, "dummyuser", "testpassword")
assert.Nil(t, err)
u := b.Url.JoinPath(b.createBuildPath("00151b98e303e19610378e6f1c49e31e5e80cd3b")).String()
assert.Equal(t, u, "https://example.com:7990/context1/context2/context3/rest/api/latest/projects/projectfoo/repos/repobar/commits/00151b98e303e19610378e6f1c49e31e5e80cd3b/builds")
}

func TestBitbucketServerApiPathOneWordScmInContext(t *testing.T) {
b, err := NewBitbucketServer("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://example.com:7990/scm/scm/projectfoo/repobar.git", "", nil, "dummyuser", "testpassword")
assert.Nil(t, err)
u := b.Url.JoinPath(b.createBuildPath("00151b98e303e19610378e6f1c49e31e5e80cd3b")).String()
assert.Equal(t, u, "https://example.com:7990/scm/rest/api/latest/projects/projectfoo/repos/repobar/commits/00151b98e303e19610378e6f1c49e31e5e80cd3b/builds")
}

func TestBitbucketServerApiPathMultipleWordScmInContext(t *testing.T) {
b, err := NewBitbucketServer("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://example.com:7990/scm/context2/scm/scm/projectfoo/repobar.git", "", nil, "dummyuser", "testpassword")
assert.Nil(t, err)
u := b.Url.JoinPath(b.createBuildPath("00151b98e303e19610378e6f1c49e31e5e80cd3b")).String()
assert.Equal(t, u, "https://example.com:7990/scm/context2/scm/rest/api/latest/projects/projectfoo/repos/repobar/commits/00151b98e303e19610378e6f1c49e31e5e80cd3b/builds")
}

func TestBitbucketServerApiPathScmAlreadyRemovedInInput(t *testing.T) {
_, err := NewBitbucketServer("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://example.com:7990/context1/context2/context3/projectfoo/repobar.git", "", nil, "dummyuser", "testpassword")
assert.NotNil(t, err)
assert.Equal(t, err.Error(), "could not parse git address: supplied provider address is not http(s) git clone url")
}

func TestBitbucketServerSshAddress(t *testing.T) {
_, err := NewBitbucketServer("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "ssh://git@mybitbucket:2222/ap/fluxcd-sandbox.git", "", nil, "", "")
assert.NotNil(t, err)
assert.Equal(t, err.Error(), "could not parse git address: unsupported scheme type in address: ssh. Must be http or https")
}

func TestNewBitbucketServerToken(t *testing.T) {
Expand All @@ -55,7 +113,7 @@ func TestNewBitbucketServerInvalidCreds(t *testing.T) {
func TestNewBitbucketServerInvalidRepo(t *testing.T) {
_, err := NewBitbucketServer("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://example.com:7990/scm/projectfoo/repobar/invalid.git", "BBDC-ODIxODYxMzIyNzUyOttorMjO059P2rYTb6EH7mP", nil, "", "")
assert.NotNil(t, err)
assert.Equal(t, err.Error(), "invalid repository id \"projectfoo/repobar/invalid\"")
assert.Equal(t, err.Error(), "could not parse git address: invalid repository id \"projectfoo/repobar/invalid\"")
}

func TestPostBitbucketServerMissingRevision(t *testing.T) {
Expand Down
Loading