Adding bitbucketserver provider for git commit status
Signed-off-by: gdasson <gaurav.dasson@gmail.com>
This commit is contained in:
parent
e425a6e307
commit
504dc991cc
|
|
@ -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"`
|
||||
|
||||
|
|
|
|||
|
|
@ -291,6 +291,7 @@ spec:
|
|||
- github
|
||||
- gitlab
|
||||
- gitea
|
||||
- bitbucketserver
|
||||
- bitbucket
|
||||
- azuredevops
|
||||
- googlechat
|
||||
|
|
|
|||
|
|
@ -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
1
go.mod
|
|
@ -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
3
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue