Skip to content

Commit

Permalink
feat: Add support for clone/create strategy for Bitbucket (#143)
Browse files Browse the repository at this point in the history
  • Loading branch information
zmotso committed Oct 8, 2024
1 parent e10afd9 commit 876dd82
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 16 deletions.
9 changes: 9 additions & 0 deletions controllers/codebase/service/chain/put_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package chain

import (
"context"
"errors"
"fmt"
"os"
"strconv"
Expand Down Expand Up @@ -368,6 +369,14 @@ func (h *PutProject) setDefaultBranch(
codebase.Spec.GetProjectID(),
codebase.Spec.DefaultBranch,
); err != nil {
if errors.Is(gitprovider.ErrApiNotSupported, err) {
// We can skip this error, because it is not supported by Git provider.
// And this is not critical for the whole process.
log.Error(err, "Setting default branch is not supported by Git provider. Set it manually if needed")

return nil
}

return fmt.Errorf("failed to set default branch: %w", err)
}

Expand Down
92 changes: 79 additions & 13 deletions pkg/gitprovider/bitbucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"

"k8s.io/apimachinery/pkg/util/uuid"
"k8s.io/utils/ptr"

"github.com/epam/edp-codebase-operator/v2/pkg/gitprovider/bitbucket/generated"
)
Expand Down Expand Up @@ -54,7 +54,7 @@ func NewBitbucketClient(token string, opts ...BitbucketClientOptsSetter) (*Bitbu
}, nil
}

func (b BitbucketClient) CreateWebHook(ctx context.Context, _, _, projectID, webHookSecret, webHookURL string, skipTLS bool) (*WebHook, error) {
func (b *BitbucketClient) CreateWebHook(ctx context.Context, _, _, projectID, webHookSecret, webHookURL string, skipTLS bool) (*WebHook, error) {
owner, repo, err := parseProjectID(projectID)
if err != nil {
return nil, err
Expand Down Expand Up @@ -96,7 +96,7 @@ func (b BitbucketClient) CreateWebHook(ctx context.Context, _, _, projectID, web
return nil, fmt.Errorf("failed to create Bitbucket web hook: %w", err)
}

if r.StatusCode() != http.StatusCreated {
if !createObjectStatusOk(r.StatusCode()) {
return nil, fmt.Errorf("failed to create Bitbucket web hook: %s %s", r.Status(), r.Body)
}

Expand All @@ -110,7 +110,7 @@ func (b BitbucketClient) CreateWebHook(ctx context.Context, _, _, projectID, web
}, nil
}

func (b BitbucketClient) CreateWebHookIfNotExists(ctx context.Context, _, _, projectID, webHookSecret, webHookURL string, skipTLS bool) (*WebHook, error) {
func (b *BitbucketClient) CreateWebHookIfNotExists(ctx context.Context, _, _, projectID, webHookSecret, webHookURL string, skipTLS bool) (*WebHook, error) {
webHooks, err := b.GetWebHooks(ctx, "", "", projectID)
if err != nil {
return nil, err
Expand All @@ -125,7 +125,7 @@ func (b BitbucketClient) CreateWebHookIfNotExists(ctx context.Context, _, _, pro
return b.CreateWebHook(ctx, "", "", projectID, webHookSecret, webHookURL, skipTLS)
}

func (b BitbucketClient) GetWebHook(ctx context.Context, _, _, projectID, webHookRef string) (*WebHook, error) {
func (b *BitbucketClient) GetWebHook(ctx context.Context, _, _, projectID, webHookRef string) (*WebHook, error) {
owner, repo, err := parseProjectID(projectID)
if err != nil {
return nil, err
Expand All @@ -150,7 +150,7 @@ func (b BitbucketClient) GetWebHook(ctx context.Context, _, _, projectID, webHoo
}, nil
}

func (b BitbucketClient) GetWebHooks(ctx context.Context, _, _, projectID string) ([]*WebHook, error) {
func (b *BitbucketClient) GetWebHooks(ctx context.Context, _, _, projectID string) ([]*WebHook, error) {
owner, repo, err := parseProjectID(projectID)
if err != nil {
return nil, err
Expand Down Expand Up @@ -185,7 +185,7 @@ func (b BitbucketClient) GetWebHooks(ctx context.Context, _, _, projectID string
return webHooks, nil
}

func (b BitbucketClient) DeleteWebHook(ctx context.Context, _, _, projectID, webHookRef string) error {
func (b *BitbucketClient) DeleteWebHook(ctx context.Context, _, _, projectID, webHookRef string) error {
owner, repo, err := parseProjectID(projectID)
if err != nil {
return err
Expand All @@ -207,14 +207,80 @@ func (b BitbucketClient) DeleteWebHook(ctx context.Context, _, _, projectID, web
return nil
}

func (BitbucketClient) CreateProject(_ context.Context, _, _, _ string) error {
return errors.New("not implemented")
func (b *BitbucketClient) CreateProject(ctx context.Context, _, _, projectID string) error {
owner, repo, err := parseProjectID(projectID)
if err != nil {
return err
}

reqBody := generated.PostRepositoriesWorkspaceRepoSlugJSONRequestBody{
Type: "repository",
IsPrivate: ptr.To(true),
}

r, err := b.client.PostRepositoriesWorkspaceRepoSlugWithResponse(ctx, owner, repo, reqBody)
if err != nil {
return fmt.Errorf("failed to create Bitbucket repository: %w", err)
}

if !createObjectStatusOk(r.StatusCode()) {
return fmt.Errorf("failed to create Bitbucket repository: %s %s", r.Status(), r.Body)
}

return nil
}

func (b *BitbucketClient) ProjectExists(ctx context.Context, _, _, projectID string) (bool, error) {
owner, repo, err := parseProjectID(projectID)
if err != nil {
return false, err
}

r, err := b.client.GetRepositoriesWorkspaceWithResponse(
ctx,
owner,
nil,
func(ctx context.Context, req *http.Request) error {
// nolint: gocritic // Can't use %q instead of "%s" because need double quotes.
req.URL.RawQuery = fmt.Sprintf(`q=slug="%s"`, repo)

return nil
},
)
if err != nil {
return false, fmt.Errorf("failed to get Bitbucket repository: %w", err)
}

if r.StatusCode() != http.StatusOK {
return false, fmt.Errorf("failed to get Bitbucket repository: %s %s", r.Status(), r.Body)
}

if r.JSON200 == nil || r.JSON200.Values == nil {
return false, nil
}

// nolint: gocritic // Force to use copy value.
for _, v := range *r.JSON200.Values {
if v.FullName != nil && *v.FullName == projectID {
return true, nil
}
}

return false, nil
}

func (BitbucketClient) ProjectExists(_ context.Context, _, _, _ string) (bool, error) {
return false, errors.New("not implemented")
func (*BitbucketClient) SetDefaultBranch(_ context.Context, _, _, _, branch string) error {
if branch == "main" {
// Bitbucket uses `main` as a default branch, so we can skip this operation.
return nil
}

// Set default branch is not supported by Bitbucket API.
// Open ticket: https://jira.atlassian.com/browse/BCLOUD-20340
// https://community.atlassian.com/t5/Bitbucket-questions/Get-and-set-default-branch-in-v2-API/qaq-p/2416227
return fmt.Errorf("setting default branch in Bitbucket repository: %w", ErrApiNotSupported)
}

func (BitbucketClient) SetDefaultBranch(_ context.Context, _, _, _, _ string) error {
return errors.New("not implemented")
func createObjectStatusOk(statusCode int) bool {
return statusCode == http.StatusOK || statusCode == http.StatusCreated
}
151 changes: 151 additions & 0 deletions pkg/gitprovider/bitbucket_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,3 +273,154 @@ func TestBitbucketClient_DeleteWebHook(t *testing.T) {
})
}
}

func TestBitbucketClient_CreateProject(t *testing.T) {
t.Parallel()

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "repo/success") {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_, _ = w.Write([]byte(`{}`))

return
}

w.WriteHeader(http.StatusInternalServerError)
}))

t.Cleanup(server.Close)

tests := []struct {
name string
projectID string
wantErr require.ErrorAssertionFunc
}{
{
name: "create project success",
projectID: "repo/success",
wantErr: require.NoError,
},
{
name: "failed to create project",
projectID: "repo/error",
wantErr: func(t require.TestingT, err error, i ...interface{}) {
require.Error(t, err)
require.Contains(t, err.Error(), "failed to create Bitbucket repository")
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b, err := NewBitbucketClient("token", WithBitbucketClientUrl(server.URL))
require.NoError(t, err)

tt.wantErr(t, b.CreateProject(context.Background(), "", "", tt.projectID))
})
}
}

func TestBitbucketClient_ProjectExists(t *testing.T) {
t.Parallel()

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.RawQuery, "success") {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"values": [{"full_name": "repo/success"}]}`))

return
}

if strings.Contains(r.URL.RawQuery, "empty") {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{}`))

return
}

w.WriteHeader(http.StatusInternalServerError)
}))

t.Cleanup(server.Close)

tests := []struct {
name string
projectID string
want bool
wantErr require.ErrorAssertionFunc
}{
{
name: "project exists",
projectID: "repo/success",
want: true,
wantErr: require.NoError,
},
{
name: "project not found",
projectID: "repo/success2",
want: false,
wantErr: require.NoError,
},
{
name: "failed to get project",
projectID: "repo/error",
want: false,
wantErr: func(t require.TestingT, err error, i ...interface{}) {
require.Error(t, err)
require.Contains(t, err.Error(), "failed to get Bitbucket repository")
},
},
{
name: "empty response",
projectID: "repo/empty",
want: false,
wantErr: require.NoError,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b, err := NewBitbucketClient("token", WithBitbucketClientUrl(server.URL))
require.NoError(t, err)

got, err := b.ProjectExists(context.Background(), "", "", tt.projectID)
tt.wantErr(t, err)
assert.Equal(t, tt.want, got)
})
}
}

func TestBitbucketClient_SetDefaultBranch(t *testing.T) {
t.Parallel()

tests := []struct {
name string
branch string
wantErr require.ErrorAssertionFunc
}{
{
name: "set default branch success",
branch: "new-branch",
wantErr: func(t require.TestingT, err error, i ...interface{}) {
require.ErrorIs(t, err, ErrApiNotSupported)
},
},
{
name: "skip main",
branch: "main",
wantErr: require.NoError,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b, err := NewBitbucketClient("token")
require.NoError(t, err)

tt.wantErr(t, b.SetDefaultBranch(context.Background(), "", "", "", tt.branch))
})
}
}
8 changes: 8 additions & 0 deletions pkg/gitprovider/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package gitprovider

import "errors"

var (
ErrWebHookNotFound = errors.New("webhook not found")
ErrApiNotSupported = errors.New("api is not supported")
)
3 changes: 0 additions & 3 deletions pkg/gitprovider/gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package gitprovider

import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
Expand All @@ -11,8 +10,6 @@ import (
"github.com/go-resty/resty/v2"
)

var ErrWebHookNotFound = errors.New("webhook not found")

type gitlabWebHook struct {
ID int `json:"id"`
URL string `json:"url"`
Expand Down

0 comments on commit 876dd82

Please sign in to comment.