diff --git a/docs/spec/v1beta2/providers.md b/docs/spec/v1beta2/providers.md index 97be15c..5fdab81 100644 --- a/docs/spec/v1beta2/providers.md +++ b/docs/spec/v1beta2/providers.md @@ -1539,6 +1539,8 @@ kubectl create secret generic bb-server-token --from-literal=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 diff --git a/docs/spec/v1beta3/providers.md b/docs/spec/v1beta3/providers.md index b3ebf35..080b82e 100644 --- a/docs/spec/v1beta3/providers.md +++ b/docs/spec/v1beta3/providers.md @@ -1567,6 +1567,8 @@ kubectl create secret generic bb-server-token --from-literal=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 diff --git a/internal/notifier/bitbucketserver.go b/internal/notifier/bitbucketserver.go index ca183be..98c299f 100644 --- a/internal/notifier/bitbucketserver.go +++ b/internal/notifier/bitbucketserver.go @@ -37,11 +37,9 @@ 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 @@ -49,8 +47,10 @@ type BitbucketServer struct { } 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 { @@ -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{ @@ -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, @@ -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) } @@ -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) } @@ -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, @@ -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) } @@ -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) } - //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 + 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") + } + + // 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) diff --git a/internal/notifier/bitbucketserver_test.go b/internal/notifier/bitbucketserver_test.go index fa8207e..5ce707d 100644 --- a/internal/notifier/bitbucketserver_test.go +++ b/internal/notifier/bitbucketserver_test.go @@ -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) { @@ -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) {