Adding bitbucketserver provider for git commit status

Signed-off-by: gdasson <gaurav.dasson@gmail.com>
This commit is contained in:
gdasson 2023-10-30 23:20:03 -05:00 committed by Gaurav Dasson
parent e425a6e307
commit 504dc991cc
8 changed files with 738 additions and 35 deletions

View File

@ -24,38 +24,39 @@ import (
)
const (
ProviderKind string = "Provider"
GenericProvider string = "generic"
GenericHMACProvider string = "generic-hmac"
SlackProvider string = "slack"
GrafanaProvider string = "grafana"
DiscordProvider string = "discord"
MSTeamsProvider string = "msteams"
RocketProvider string = "rocket"
GitHubDispatchProvider string = "githubdispatch"
GitHubProvider string = "github"
GitLabProvider string = "gitlab"
GiteaProvider string = "gitea"
BitbucketProvider string = "bitbucket"
AzureDevOpsProvider string = "azuredevops"
GoogleChatProvider string = "googlechat"
GooglePubSubProvider string = "googlepubsub"
WebexProvider string = "webex"
SentryProvider string = "sentry"
AzureEventHubProvider string = "azureeventhub"
TelegramProvider string = "telegram"
LarkProvider string = "lark"
Matrix string = "matrix"
OpsgenieProvider string = "opsgenie"
AlertManagerProvider string = "alertmanager"
PagerDutyProvider string = "pagerduty"
DataDogProvider string = "datadog"
ProviderKind string = "Provider"
GenericProvider string = "generic"
GenericHMACProvider string = "generic-hmac"
SlackProvider string = "slack"
GrafanaProvider string = "grafana"
DiscordProvider string = "discord"
MSTeamsProvider string = "msteams"
RocketProvider string = "rocket"
GitHubDispatchProvider string = "githubdispatch"
GitHubProvider string = "github"
GitLabProvider string = "gitlab"
GiteaProvider string = "gitea"
BitbucketServerProvider string = "bitbucketserver"
BitbucketProvider string = "bitbucket"
AzureDevOpsProvider string = "azuredevops"
GoogleChatProvider string = "googlechat"
GooglePubSubProvider string = "googlepubsub"
WebexProvider string = "webex"
SentryProvider string = "sentry"
AzureEventHubProvider string = "azureeventhub"
TelegramProvider string = "telegram"
LarkProvider string = "lark"
Matrix string = "matrix"
OpsgenieProvider string = "opsgenie"
AlertManagerProvider string = "alertmanager"
PagerDutyProvider string = "pagerduty"
DataDogProvider string = "datadog"
)
// ProviderSpec defines the desired state of the Provider.
type ProviderSpec struct {
// Type specifies which Provider implementation to use.
// +kubebuilder:validation:Enum=slack;discord;msteams;rocket;generic;generic-hmac;github;gitlab;gitea;bitbucket;azuredevops;googlechat;googlepubsub;webex;sentry;azureeventhub;telegram;lark;matrix;opsgenie;alertmanager;grafana;githubdispatch;pagerduty;datadog
// +kubebuilder:validation:Enum=slack;discord;msteams;rocket;generic;generic-hmac;github;gitlab;gitea;bitbucketserver;bitbucket;azuredevops;googlechat;googlepubsub;webex;sentry;azureeventhub;telegram;lark;matrix;opsgenie;alertmanager;grafana;githubdispatch;pagerduty;datadog
// +required
Type string `json:"type"`

View File

@ -291,6 +291,7 @@ spec:
- github
- gitlab
- gitea
- bitbucketserver
- bitbucket
- azuredevops
- googlechat

View File

@ -129,13 +129,14 @@ The supported alerting providers are:
The supported providers for [Git commit status updates](#git-commit-status-updates) are:
| Provider | Type |
|-------------------------------|---------------|
| [Azure DevOps](#azure-devops) | `azuredevops` |
| [Bitbucket](#bitbucket) | `bitbucket` |
| [GitHub](#github) | `github` |
| [GitLab](#gitlab) | `gitlab` |
| [Gitea](#gitea) | `gitea` |
| Provider | Type |
| ------------------------------------------------| ----------------- |
| [Azure DevOps](#azure-devops) | `azuredevops` |
| [Bitbucket](#bitbucket) | `bitbucket` |
| [BitbucketServer](#bitbucket-serverdata-center) | `bitbucketserver` |
| [GitHub](#github) | `github` |
| [GitLab](#gitlab) | `gitlab` |
| [Gitea](#gitea) | `gitea` |
#### Alerting
@ -1514,6 +1515,30 @@ You can create the secret with `kubectl` like this:
kubectl create secret generic bitbucket-token --from-literal=token=<username>:<app-password>
```
#### BitBucket Server/Data Center
When `.spec.type` is set to `bitbucketserver`, the following auth methods are available:
- Basic Authentication (username/password)
- [HTTP access tokens](https://confluence.atlassian.com/bitbucketserver/http-access-tokens-939515499.html)
For Basic Authentication, the referenced secret must contain a `password` field. The `username` field can either come from the [`.spec.username` field of the Provider](https://fluxcd.io/flux/components/notification/providers/#username) or can be defined in the referenced secret.
You can create the secret with `kubectl` like this:
```shell
kubectl create secret generic bb-server-username-password --from-literal=username=<username> --from-literal=password=<password>
```
For HTTP access tokens, the secret can be created with `kubectl` like this:
```shell
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`.
#### Azure DevOps
When `.spec.type` is set to `azuredevops`, the referenced secret must contain a key called `token` with the value set to a

1
go.mod
View File

@ -142,6 +142,7 @@ require (
golang.org/x/sys v0.13.0 // indirect
golang.org/x/term v0.13.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.13.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect

3
go.sum
View File

@ -1514,7 +1514,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -0,0 +1,294 @@
/*
Copyright 2023 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package notifier
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
"github.com/fluxcd/pkg/apis/meta"
"github.com/hashicorp/go-retryablehttp"
)
// BitbucketServer is a notifier for BitBucket Server and Data Center.
type BitbucketServer struct {
ProjectKey string
RepositorySlug string
ProviderUID string
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"
bbServerGetBuildStatusQueryString = "key"
)
type bbServerBuildStatus struct {
Name string `json:"name,omitempty"`
Key string `json:"key,omitempty"`
Parent string `json:"parent,omitempty"`
State string `json:"state,omitempty"`
Ref string `json:"ref,omitempty"`
BuildNumber string `json:"buildNumber,omitempty"`
Description string `json:"description,omitempty"`
Duration int64 `json:"duration,omitempty"`
UpdatedDate int64 `json:"updatedDate,omitempty"`
CreatedDate int64 `json:"createdDate,omitempty"`
Url string `json:"url,omitempty"`
}
type bbServerBuildStatusSetRequest struct {
BuildNumber string `json:"buildNumber,omitempty"`
Description string `json:"description,omitempty"`
Duration int64 `json:"duration,omitempty"`
Key string `json:"key"`
LastUpdated int64 `json:"lastUpdated,omitempty"`
Name string `json:"name,omitempty"`
Parent string `json:"parent,omitempty"`
Ref string `json:"ref,omitempty"`
State string `json:"state"`
Url string `json:"url"`
}
// 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)
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{
TLSClientConfig: &tls.Config{
RootCAs: certPool,
},
}
}
httpClient.HTTPClient.Timeout = 15 * time.Second
httpClient.RetryWaitMin = 2 * time.Second
httpClient.RetryWaitMax = 30 * time.Second
httpClient.RetryMax = 4
httpClient.Logger = nil
if len(token) == 0 && (len(username) == 0 || len(password) == 0) {
return nil, errors.New("invalid credentials, expected to be one of username/password or API Token")
}
return &BitbucketServer{
ProjectKey: projectkey,
RepositorySlug: reposlug,
ProviderUID: providerUID,
Host: hst,
ProviderAddress: addr,
Token: token,
Username: username,
Password: password,
Client: httpClient,
}, nil
}
// Post Bitbucket Server build status
func (b BitbucketServer) Post(ctx context.Context, event eventv1.Event) error {
// Skip progressing events
if event.HasReason(meta.ProgressingReason) {
return nil
}
revString, ok := event.Metadata[eventv1.MetaRevisionKey]
if !ok {
return errors.New("missing revision metadata")
}
rev, err := parseRevision(revString)
if err != nil {
return fmt.Errorf("could not parse revision: %w", err)
}
state, err := b.state(event.Severity)
if err != nil {
return fmt.Errorf("couldn't convert to bitbucket server state: %w", err)
}
name, desc := formatNameAndDescription(event)
name = name + " [" + desc + "]" //Bitbucket server displays this data on browser. Thus adding description here.
id := generateCommitStatusID(b.ProviderUID, event)
// 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)
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)
if err != nil {
return fmt.Errorf("could not post build status: %w", err)
}
}
return nil
}
func (b BitbucketServer) state(severity string) (string, error) {
switch severity {
case eventv1.EventSeverityInfo:
return "SUCCESSFUL", nil
case eventv1.EventSeverityError:
return "FAILED", nil
default:
return "", errors.New("bitbucket server state generated on info or error events only")
}
}
func (b BitbucketServer) duplicateBitbucketServerStatus(ctx context.Context, rev, state, name, desc, id, key, u string) (bool, error) {
// Prepare request object
req, err := b.prepareCommonRequest(ctx, u, nil, http.MethodGet, key, rev)
if err != nil {
return false, fmt.Errorf("could not check duplicate commit status: %w", err)
}
// Set query string
q := url.Values{}
q.Add(bbServerGetBuildStatusQueryString, key)
req.URL.RawQuery = q.Encode()
// Make a GET call
d, err := b.Client.Do(req)
if err != nil && d.StatusCode != http.StatusNotFound {
return false, fmt.Errorf("failed api call to check duplicate commit status: %w", err)
}
if isError(d) && d.StatusCode != http.StatusNotFound {
defer d.Body.Close()
return false, fmt.Errorf("failed api call to check duplicate commit status: %d - %s", d.StatusCode, http.StatusText(d.StatusCode))
}
defer d.Body.Close()
if d.StatusCode == http.StatusOK {
bd, err := io.ReadAll(d.Body)
if err != nil {
return false, fmt.Errorf("could not read response body for duplicate commit status: %w", err)
}
var existingCommitStatus bbServerBuildStatus
err = json.Unmarshal(bd, &existingCommitStatus)
if err != nil {
return false, fmt.Errorf("could not unmarshal json response body for duplicate commit status: %w", err)
}
// Do not post duplicate build status
if existingCommitStatus.Key == key && existingCommitStatus.State == state && existingCommitStatus.Description == desc && existingCommitStatus.Name == name {
return true, nil
}
}
return false, nil
}
func (b BitbucketServer) postBuildStatus(ctx context.Context, rev, state, name, desc, id, key, url string) (*http.Response, error) {
//Prepare json body
j := &bbServerBuildStatusSetRequest{
Key: key,
State: state,
Url: b.ProviderAddress,
Description: desc,
Name: name,
}
p := new(bytes.Buffer)
err := json.NewEncoder(p).Encode(j)
if err != nil {
return nil, fmt.Errorf("failed preparing request for post build commit status, could not encode request body to json: %w", err)
}
//Prepare request
req, err := b.prepareCommonRequest(ctx, url, p, http.MethodPost, key, rev)
if err != nil {
return nil, fmt.Errorf("failed preparing request for post build commit status: %w", err)
}
// Add Content type header
req.Header.Add("Content-Type", "application/json")
// Make a POST call
resp, err := b.Client.Do(req)
if err != nil {
return nil, fmt.Errorf("could not post build commit status: %w", err)
}
// Note: A non-2xx status code doesn't cause an error: https://pkg.go.dev/net/http#Client.Do
if isError(resp) {
defer resp.Body.Close()
return nil, fmt.Errorf("could not post build commit status: %d - %s", resp.StatusCode, http.StatusText(resp.StatusCode))
}
defer resp.Body.Close()
return resp, nil
}
func (b BitbucketServer) createApiPath(rev string) string {
return fmt.Sprintf(bbServerEndPointTmpl, b.ProjectKey, b.RepositorySlug, rev)
}
func parseBitbucketServerGitAddress(s string) (string, string, error) {
host, id, err := parseGitAddress(s)
if err != nil {
return "", "", 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
}
func (b BitbucketServer) prepareCommonRequest(ctx context.Context, path string, body io.Reader, method string, key, rev 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)
}
if b.Token != "" {
req.Header.Set("Authorization", "Bearer "+b.Token)
} else {
req.Header.Add("Authorization", "Basic "+basicAuth(b.Username, b.Password))
}
req.Header.Add("x-atlassian-token", "no-check")
req.Header.Add("x-requested-with", "XMLHttpRequest")
return req, nil
}
// isError method returns true if HTTP status `code >= 400` otherwise false.
func isError(r *http.Response) bool {
return r.StatusCode > 399
}

View File

@ -0,0 +1,378 @@
/*
Copyright 2023 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package notifier
import (
"context"
"encoding/base64"
"encoding/json"
"io"
"testing"
"net/http"
"net/http/httptest"
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestNewBitbucketServerBasic(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")
}
func TestNewBitbucketServerToken(t *testing.T) {
b, err := NewBitbucketServer("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://example.com:7990/scm/projectfoo/repobar.git", "BBDC-ODIxODYxMzIyNzUyOttorMjO059P2rYTb6EH7mP", nil, "", "")
assert.Nil(t, err)
assert.Equal(t, b.Token, "BBDC-ODIxODYxMzIyNzUyOttorMjO059P2rYTb6EH7mP")
}
func TestNewBitbucketServerInvalidCreds(t *testing.T) {
_, err := NewBitbucketServer("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://example.com:7990/scm/projectfoo/repobar.git", "", nil, "", "")
assert.NotNil(t, err)
assert.Equal(t, err.Error(), "invalid credentials, expected to be one of username/password or API Token")
}
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\"")
}
func TestPostBitbucketServerMissingRevision(t *testing.T) {
b, err := NewBitbucketServer("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://example.com:7990/scm/projectfoo/repobar.git", "BBDC-ODIxODYxMzIyNzUyOttorMjO059P2rYTb6EH7mP", nil, "", "")
assert.Nil(t, err)
//Validate missing revision
err = b.Post(context.TODO(), generateTestEventKustomization("info", map[string]string{
"dummybadrevision": "bad",
}))
assert.NotNil(t, err)
assert.Equal(t, err.Error(), "missing revision metadata")
}
func TestPostBitbucketServerBadCommitHash(t *testing.T) {
b, err := NewBitbucketServer("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://example.com:7990/scm/projectfoo/repobar.git", "BBDC-ODIxODYxMzIyNzUyOttorMjO059P2rYTb6EH7mP", nil, "", "")
assert.Nil(t, err)
//Validate extract commit hash
err = b.Post(context.TODO(), generateTestEventKustomization("info", map[string]string{
eventv1.MetaRevisionKey: "badhash",
}))
assert.NotNil(t, err)
assert.Equal(t, err.Error(), "could not parse revision: failed to extract commit hash from 'badhash' revision")
}
func TestPostBitbucketServerBadBitbucketState(t *testing.T) {
b, err := NewBitbucketServer("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://example.com:7990/scm/projectfoo/repobar.git", "BBDC-ODIxODYxMzIyNzUyOttorMjO059P2rYTb6EH7mP", nil, "", "")
assert.Nil(t, err)
//Validate conversion to bitbucket state
err = b.Post(context.TODO(), generateTestEventKustomization("badserveritystate", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}))
assert.NotNil(t, err)
assert.Equal(t, err.Error(), "couldn't convert to bitbucket server state: bitbucket server state generated on info or error events only")
}
func generateTestEventKustomization(severity string, metadata map[string]string) eventv1.Event {
return eventv1.Event{
InvolvedObject: corev1.ObjectReference{
Kind: "Kustomization",
Namespace: "flux-system",
Name: "hello-world",
},
Severity: severity,
Timestamp: metav1.Now(),
Message: "message",
Reason: "reason",
Metadata: metadata,
ReportingController: "kustomize-controller",
ReportingInstance: "kustomize-controller-xyz",
}
}
func TestBitBucketServerPostValidateRequest(t *testing.T) {
tests := []struct {
name string
errorString string
testFailReason string
headers map[string]string
username string
password string
token string
event eventv1.Event
provideruid string
key string
}{
{
name: "Validate Token Auth ",
token: "goodtoken",
provideruid: "0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a",
headers: map[string]string{
"Authorization": "Bearer goodtoken",
"x-atlassian-token": "no-check",
"x-requested-with": "XMLHttpRequest",
},
event: generateTestEventKustomization("info", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}),
key: sha1String(generateCommitStatusID("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", generateTestEventKustomization("info", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}))),
},
{
name: "Validate Basic Auth and Post State=Successful",
username: "hello",
password: "password",
provideruid: "0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a",
headers: map[string]string{
"Authorization": "Basic " + base64.StdEncoding.EncodeToString([]byte("hello"+":"+"password")),
"x-atlassian-token": "no-check",
"x-requested-with": "XMLHttpRequest",
},
event: generateTestEventKustomization("info", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}),
key: sha1String(generateCommitStatusID("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", generateTestEventKustomization("info", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}))),
},
{
name: "Validate Post State=Failed",
username: "hello",
password: "password",
provideruid: "0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a",
headers: map[string]string{
"Authorization": "Basic " + base64.StdEncoding.EncodeToString([]byte("hello"+":"+"password")),
"x-atlassian-token": "no-check",
"x-requested-with": "XMLHttpRequest",
},
event: generateTestEventKustomization("error", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}),
key: sha1String(generateCommitStatusID("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", generateTestEventKustomization("error", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}))),
},
{
name: "Fail if bad json response in existing commit status",
testFailReason: "badjson",
errorString: "could not get existing commit status: could not unmarshal json response body for duplicate commit status: unexpected end of JSON input",
username: "hello",
password: "password",
provideruid: "0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a",
headers: map[string]string{
"Authorization": "Basic " + base64.StdEncoding.EncodeToString([]byte("hello"+":"+"password")),
"x-atlassian-token": "no-check",
"x-requested-with": "XMLHttpRequest",
},
event: generateTestEventKustomization("error", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}),
key: sha1String(generateCommitStatusID("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", generateTestEventKustomization("error", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}))),
},
{
name: "Fail if status code is non-200 in existing commit status",
testFailReason: "badstatuscode",
errorString: "could not get existing commit status: failed api call to check duplicate commit status: 400 - Bad Request",
username: "hello",
password: "password",
provideruid: "0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a",
headers: map[string]string{
"Authorization": "Basic " + base64.StdEncoding.EncodeToString([]byte("hello"+":"+"password")),
"x-atlassian-token": "no-check",
"x-requested-with": "XMLHttpRequest",
},
event: generateTestEventKustomization("error", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}),
key: sha1String(generateCommitStatusID("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", generateTestEventKustomization("error", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}))),
},
{
name: "Bad post- Unauthorized",
testFailReason: "badpost",
errorString: "could not post build status: could not post build commit status: 401 - Unauthorized",
username: "hello",
password: "password",
provideruid: "0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a",
headers: map[string]string{
"Authorization": "Basic " + base64.StdEncoding.EncodeToString([]byte("hello"+":"+"password")),
"x-atlassian-token": "no-check",
"x-requested-with": "XMLHttpRequest",
},
event: generateTestEventKustomization("error", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}),
key: sha1String(generateCommitStatusID("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", generateTestEventKustomization("error", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}))),
},
{
name: "Validate duplicate commit status successful match",
username: "hello",
password: "password",
provideruid: "0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a",
headers: map[string]string{
"Authorization": "Basic " + base64.StdEncoding.EncodeToString([]byte("hello"+":"+"password")),
"x-atlassian-token": "no-check",
"x-requested-with": "XMLHttpRequest",
},
event: generateTestEventKustomization("info", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}),
key: sha1String(generateCommitStatusID("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", generateTestEventKustomization("info", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}))),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Validate Headers
for key, value := range tt.headers {
require.Equal(t, value, r.Header.Get(key))
}
// Validate URI
require.Equal(t, r.URL.Path, "/rest/api/latest/projects/projectfoo/repos/repobar/commits/5394cb7f48332b2de7c17dd8b8384bbc84b7e738/builds")
// Validate Get Build Status call
if r.Method == http.MethodGet {
//Validate that this GET request has a query string with "key" as the query paraneter
require.Equal(t, r.URL.Query().Get(bbServerGetBuildStatusQueryString), tt.key)
// Validate that this GET request has no body
require.Equal(t, http.NoBody, r.Body)
if tt.name == "Validate duplicate commit status successful match" {
w.WriteHeader(http.StatusOK)
w.Header().Add("Content-Type", "application/json")
name, desc := formatNameAndDescription(tt.event)
name = name + " [" + desc + "]"
jsondata, _ := json.Marshal(&bbServerBuildStatus{
Name: name,
Description: desc,
Key: sha1String(generateCommitStatusID(tt.provideruid, tt.event)),
State: "SUCCESSFUL",
Url: "https://example.com:7990/scm/projectfoo/repobar.git",
})
w.Write(jsondata)
}
if tt.testFailReason == "badstatuscode" {
w.WriteHeader(http.StatusBadRequest)
} else if tt.testFailReason == "badjson" {
w.WriteHeader(http.StatusOK)
w.Header().Add("Content-Type", "application/json")
//Do nothing here and an empty/null body will be returned
} else {
if tt.name != "Validate duplicate commit status successful match" {
w.WriteHeader(http.StatusOK)
w.Header().Add("Content-Type", "application/json")
w.Write([]byte(`{
"description": "reconciliation succeeded",
"key": "TEST2",
"state": "SUCCESSFUL",
"name": "kustomization/helloworld-yaml-2-bitbucket-server [reconciliation succeeded]",
"url": "https://example.com:7990/scm/projectfoo/repobar.git"
}`))
}
}
}
// Validate Post BuildStatus call
if r.Method == http.MethodPost {
// Validate that this POST request has no query string
require.Equal(t, len(r.URL.Query()), 0)
// Validate that this POST request has Content-Type: application/json header
require.Equal(t, "application/json", r.Header.Get("Content-Type"))
// Read json body of the request
b, err := io.ReadAll(r.Body)
require.NoError(t, err)
// Parse json request into Payload Request body struct
var payload bbServerBuildStatusSetRequest
err = json.Unmarshal(b, &payload)
require.NoError(t, err)
// Validate Key
require.Equal(t, payload.Key, tt.key)
// Validate that state can be only SUCCESSFUL or FAILED
if payload.State != "SUCCESSFUL" && payload.State != "FAILED" {
require.Fail(t, "Invalid state")
}
// If severity of event is info, state should be SUCCESSFUL
if tt.event.Severity == "info" {
require.Equal(t, "SUCCESSFUL", payload.State)
}
// If severity of event is error, state should be FAILED
if tt.event.Severity == "error" {
require.Equal(t, "FAILED", payload.State)
}
// Validate description
require.Equal(t, "reason", payload.Description)
// Validate name(with description appended)
require.Equal(t, "kustomization/hello-world"+" ["+payload.Description+"]", payload.Name)
require.Contains(t, payload.Url, "/scm/projectfoo/repobar.git")
if tt.testFailReason == "badpost" {
w.WriteHeader(http.StatusUnauthorized)
}
// Sending a bad response here
// This proves that the duplicate commit status is never posted
if tt.name == "Validate duplicate commit status successful match" {
w.WriteHeader(http.StatusUnauthorized)
}
}
}))
defer ts.Close()
c, err := NewBitbucketServer(tt.provideruid, ts.URL+"/scm/projectfoo/repobar.git", tt.token, nil, tt.username, tt.password)
require.NoError(t, err)
err = c.Post(context.TODO(), tt.event)
if tt.testFailReason == "" {
require.NoError(t, err)
} else {
assert.NotNil(t, err)
assert.Equal(t, err.Error(), tt.errorString)
}
})
}
}

View File

@ -85,6 +85,8 @@ func (f Factory) Notifier(provider string) (Interface, error) {
n, err = NewGitLab(f.ProviderUID, f.URL, f.Token, f.CertPool)
case apiv1.GiteaProvider:
n, err = NewGitea(f.ProviderUID, f.URL, f.Token, f.CertPool)
case apiv1.BitbucketServerProvider:
n, err = NewBitbucketServer(f.ProviderUID, f.URL, f.Token, f.CertPool, f.Username, f.Password)
case apiv1.BitbucketProvider:
n, err = NewBitbucket(f.ProviderUID, f.URL, f.Token, f.CertPool)
case apiv1.AzureDevOpsProvider: