From b86e9dac7da8f7addef427b1b499e7a0546b9593 Mon Sep 17 00:00:00 2001 From: Sergiy Kulanov Date: Tue, 1 Oct 2024 16:45:01 +0300 Subject: [PATCH] feat: Add support for BitBucket Cloud (#311) Signed-off-by: Sergiy Kulanov --- Makefile | 4 +- cmd/interceptor/main.go | 4 +- pkg/event_processor/bitbucket/bitbucket.go | 105 ++++++++++ .../bitbucket/bitbucket_test.go | 188 ++++++++++++++++++ pkg/event_processor/event.go | 69 ++++++- pkg/{ => event_processor}/github/github.go | 0 .../github/github_test.go | 0 pkg/interceptor/edp_interceptor.go | 33 ++- pkg/interceptor/edp_interceptor_test.go | 29 ++- 9 files changed, 409 insertions(+), 23 deletions(-) create mode 100644 pkg/event_processor/bitbucket/bitbucket.go create mode 100644 pkg/event_processor/bitbucket/bitbucket_test.go rename pkg/{ => event_processor}/github/github.go (100%) rename pkg/{ => event_processor}/github/github_test.go (100%) diff --git a/Makefile b/Makefile index 510f6e9e..5c279d57 100644 --- a/Makefile +++ b/Makefile @@ -82,7 +82,9 @@ clean: ## clean up -rm -rf ${DIST_DIR} .PHONY: test ## Run tests -test: test-chart +test: test-chart test-go + +test-go: go test ./... -coverprofile=coverage.out `go list ./...` .PHONY: lint diff --git a/cmd/interceptor/main.go b/cmd/interceptor/main.go index c04024a7..6f6d12c9 100644 --- a/cmd/interceptor/main.go +++ b/cmd/interceptor/main.go @@ -23,9 +23,10 @@ import ( codebaseApiV1 "github.com/epam/edp-codebase-operator/v2/api/v1" buildInfo "github.com/epam/edp-common/pkg/config" + "github.com/epam/edp-tekton/pkg/event_processor/bitbucket" "github.com/epam/edp-tekton/pkg/event_processor/gerrit" + "github.com/epam/edp-tekton/pkg/event_processor/github" "github.com/epam/edp-tekton/pkg/event_processor/gitlab" - "github.com/epam/edp-tekton/pkg/github" "github.com/epam/edp-tekton/pkg/interceptor" ) @@ -102,6 +103,7 @@ func main() { github.NewEventProcessor(client, &github.EventProcessorOptions{Logger: logger}), gitlab.NewEventProcessor(client, logger), gerrit.NewEventProcessor(client, logger), + bitbucket.NewEventProcessor(client, logger), logger, ), Logger: logger, diff --git a/pkg/event_processor/bitbucket/bitbucket.go b/pkg/event_processor/bitbucket/bitbucket.go new file mode 100644 index 00000000..4823edc4 --- /dev/null +++ b/pkg/event_processor/bitbucket/bitbucket.go @@ -0,0 +1,105 @@ +package bitbucket + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "go.uber.org/zap" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/epam/edp-tekton/pkg/event_processor" +) + +type EventProcessor struct { + ksClient ctrlClient.Reader + logger *zap.SugaredLogger +} + +func NewEventProcessor(ksClient ctrlClient.Reader, logger *zap.SugaredLogger) *EventProcessor { + return &EventProcessor{ + ksClient: ksClient, + logger: logger, + } +} + +func (p *EventProcessor) Process(ctx context.Context, body []byte, ns, eventType string) (*event_processor.EventInfo, error) { + switch eventType { + case event_processor.BitbucketEventTypeCommentAdded: + return p.processCommentEvent(ctx, body, ns) + default: + return p.processMergeEvent(ctx, body, ns) + } +} + +func (p *EventProcessor) processMergeEvent(ctx context.Context, body []byte, ns string) (*event_processor.EventInfo, error) { + bitbucketEvent := &event_processor.BitbucketEvent{} + if err := json.Unmarshal(body, bitbucketEvent); err != nil { + return nil, fmt.Errorf("failed to unmarshal Bitbucket event: %w", err) + } + + if bitbucketEvent.Repository.FullName == "" { + return nil, errors.New("bitbucket repository path empty") + } + + if bitbucketEvent.PullRequest.Destination.Branch.Name == "" { + return nil, errors.New("bitbucket target branch empty") + } + + repoPath := event_processor.ConvertRepositoryPath(bitbucketEvent.Repository.FullName) + + codebase, err := event_processor.GetCodebaseByRepoPath(ctx, p.ksClient, ns, repoPath) + if err != nil { + return nil, fmt.Errorf("failed to get codebase by repo path: %w", err) + } + + return &event_processor.EventInfo{ + GitProvider: event_processor.GitProviderBitbucket, + RepoPath: repoPath, + TargetBranch: bitbucketEvent.PullRequest.Destination.Branch.Name, + Type: event_processor.EventTypeMerge, + Codebase: codebase, + PullRequest: &event_processor.PullRequest{ + HeadSha: bitbucketEvent.PullRequest.Source.Commit.Hash, + Title: bitbucketEvent.PullRequest.Title, + HeadRef: bitbucketEvent.PullRequest.Source.Branch.Name, + ChangeNumber: bitbucketEvent.PullRequest.ID, + LastCommitMessage: bitbucketEvent.PullRequest.LastCommit.Hash, + }, + }, nil +} + +func (p *EventProcessor) processCommentEvent(ctx context.Context, body []byte, ns string) (*event_processor.EventInfo, error) { + bitbucketEvent := &event_processor.BitbucketCommentEvent{} + if err := json.Unmarshal(body, bitbucketEvent); err != nil { + return nil, fmt.Errorf("failed to unmarshal Bitbucket event: %w", err) + } + + if bitbucketEvent.Repository.FullName == "" { + return nil, errors.New("bitbucket repository path empty") + } + + repoPath := event_processor.ConvertRepositoryPath(bitbucketEvent.Repository.FullName) + + codebase, err := event_processor.GetCodebaseByRepoPath(ctx, p.ksClient, ns, repoPath) + if err != nil { + return nil, fmt.Errorf("failed to get codebase by repo path: %w", err) + } + + return &event_processor.EventInfo{ + GitProvider: event_processor.GitProviderBitbucket, + RepoPath: repoPath, + TargetBranch: bitbucketEvent.PullRequest.Destination.Branch.Name, + Type: event_processor.EventTypeReviewComment, + Codebase: codebase, + HasPipelineRecheck: event_processor.ContainsPipelineRecheck(bitbucketEvent.Comment.Content.Raw), + PullRequest: &event_processor.PullRequest{ + HeadSha: bitbucketEvent.PullRequest.Source.Commit.Hash, + Title: bitbucketEvent.PullRequest.Title, + HeadRef: bitbucketEvent.PullRequest.Source.Branch.Name, + ChangeNumber: bitbucketEvent.PullRequest.ID, + LastCommitMessage: bitbucketEvent.PullRequest.LastCommit.Hash, + }, + }, nil +} diff --git a/pkg/event_processor/bitbucket/bitbucket_test.go b/pkg/event_processor/bitbucket/bitbucket_test.go new file mode 100644 index 00000000..12d5f20b --- /dev/null +++ b/pkg/event_processor/bitbucket/bitbucket_test.go @@ -0,0 +1,188 @@ +package bitbucket + +import ( + "context" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + codebaseApi "github.com/epam/edp-codebase-operator/v2/api/v1" + + "github.com/epam/edp-tekton/pkg/event_processor" +) + +func TestBitbucketEventProcessor_processCommentEvent(t *testing.T) { + t.Parallel() + + scheme := runtime.NewScheme() + require.NoError(t, codebaseApi.AddToScheme(scheme)) + + type args struct { + body any + } + + tests := []struct { + name string + args args + kubeObjects []client.Object + wantErr require.ErrorAssertionFunc + want *event_processor.EventInfo + }{ + { + name: "comment event process successfully", + args: args{ + body: event_processor.BitbucketCommentEvent{ + BitbucketEvent: event_processor.BitbucketEvent{ + Repository: event_processor.BitbucketRepository{ + FullName: "o/r", + }, + PullRequest: event_processor.BitbucketPullRequest{ + ID: 1, + Title: "fix", + Source: event_processor.BitbucketPullRequestSrc{ + Branch: event_processor.BitbucketBranch{ + Name: "feature1", + }, + Commit: event_processor.BitbucketCommit{ + Hash: "123", + }, + }, + Destination: event_processor.BitbucketPullRequestDest{ + Branch: event_processor.BitbucketBranch{ + Name: "master", + }, + Commit: event_processor.BitbucketCommit{ + Hash: "456", + }, + }, + LastCommit: event_processor.BitbucketCommit{ + Hash: "123", + }, + }, + }, + Comment: event_processor.BitbucketComment{ + Content: event_processor.BitbucketCommentContent{ + Raw: "/recheck", + }, + }, + }, + }, + kubeObjects: []client.Object{ + &codebaseApi.Codebase{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-codebase", + Namespace: "default", + }, + Spec: codebaseApi.CodebaseSpec{ + GitUrlPath: "/o/r", + }, + }, + }, + wantErr: require.NoError, + want: &event_processor.EventInfo{ + GitProvider: event_processor.GitProviderBitbucket, + RepoPath: "/o/r", + TargetBranch: "master", + Type: event_processor.EventTypeReviewComment, + HasPipelineRecheck: true, + Codebase: &codebaseApi.Codebase{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-codebase", + Namespace: "default", + ResourceVersion: "999", + }, + Spec: codebaseApi.CodebaseSpec{ + GitUrlPath: "/o/r", + }, + }, + PullRequest: &event_processor.PullRequest{ + HeadRef: "feature1", + HeadSha: "123", + Title: "fix", + ChangeNumber: 1, + LastCommitMessage: "123", + }, + }, + }, + { + name: "repository path empty", + args: args{ + body: event_processor.BitbucketCommentEvent{}, + }, + wantErr: func(t require.TestingT, err error, i ...interface{}) { + require.Error(t, err) + require.Contains(t, err.Error(), "repository path empty") + }, + }, + { + name: "failed to get codebase", + args: args{ + body: event_processor.BitbucketCommentEvent{ + BitbucketEvent: event_processor.BitbucketEvent{ + Repository: event_processor.BitbucketRepository{ + FullName: "o/r", + }, + PullRequest: event_processor.BitbucketPullRequest{ + ID: 1, + Title: "fix", + Source: event_processor.BitbucketPullRequestSrc{ + Branch: event_processor.BitbucketBranch{ + Name: "feature1", + }, + Commit: event_processor.BitbucketCommit{ + Hash: "123", + }, + }, + Destination: event_processor.BitbucketPullRequestDest{ + Branch: event_processor.BitbucketBranch{ + Name: "master", + }, + Commit: event_processor.BitbucketCommit{ + Hash: "456", + }, + }, + LastCommit: event_processor.BitbucketCommit{ + Hash: "123", + }, + }, + }, + Comment: event_processor.BitbucketComment{ + Content: event_processor.BitbucketCommentContent{ + Raw: "/recheck", + }, + }, + }, + }, + wantErr: func(t require.TestingT, err error, i ...interface{}) { + require.Error(t, err) + require.Contains(t, err.Error(), "failed to get codebase by repo path") + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + body, err := json.Marshal(tt.args.body) + + require.NoError(t, err) + + p := NewEventProcessor( + fake.NewClientBuilder().WithScheme(scheme).WithObjects(tt.kubeObjects...).Build(), + zap.NewNop().Sugar(), + ) + got, err := p.processCommentEvent(context.Background(), body, "default") + tt.wantErr(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/event_processor/event.go b/pkg/event_processor/event.go index 97844baa..5443bba4 100644 --- a/pkg/event_processor/event.go +++ b/pkg/event_processor/event.go @@ -52,14 +52,69 @@ type GitLabComment struct { Note string `json:"note"` } -const ( - GitProviderGitHub = "github" - GitProviderGitLab = "gitlab" - GitProviderGerrit = "gerrit" +type BitbucketEvent struct { + Repository BitbucketRepository `json:"repository"` + PullRequest BitbucketPullRequest `json:"pullrequest"` + Actor BitbucketActor `json:"actor"` +} + +type BitbucketCommentEvent struct { + BitbucketEvent + Comment BitbucketComment `json:"comment"` +} + +type BitbucketRepository struct { + FullName string `json:"full_name"` +} + +type BitbucketPullRequest struct { + ID int `json:"id"` + Title string `json:"title"` + Source BitbucketPullRequestSrc `json:"source"` + Destination BitbucketPullRequestDest `json:"destination"` + LastCommit BitbucketCommit `json:"last_commit"` +} + +type BitbucketPullRequestSrc struct { + Branch BitbucketBranch `json:"branch"` + Commit BitbucketCommit `json:"commit"` +} + +type BitbucketPullRequestDest struct { + Branch BitbucketBranch `json:"branch"` + Commit BitbucketCommit `json:"commit"` +} - GerritEventTypeCommentAdded = "comment-added" - GitHubEventTypeCommentAdded = "issue_comment" - GitLabEventTypeCommentAdded = "Note Hook" +type BitbucketBranch struct { + Name string `json:"name"` +} + +type BitbucketComment struct { + Content BitbucketCommentContent `json:"content"` +} + +type BitbucketCommentContent struct { + Raw string `json:"raw"` +} + +type BitbucketCommit struct { + Hash string `json:"hash"` +} + +type BitbucketActor struct { + DisplayName string `json:"display_name"` +} + +const ( + GitProviderGitHub = "github" + GitProviderGitLab = "gitlab" + GitProviderGerrit = "gerrit" + GitProviderBitbucket = "bitbucket" + + GerritEventTypeCommentAdded = "comment-added" + GitHubEventTypeCommentAdded = "issue_comment" + GitLabEventTypeCommentAdded = "Note Hook" + BitbucketEventTypeCommentAdded = "pullrequest:comment_created" EventTypeReviewComment = "comment" EventTypeMerge = "merge" diff --git a/pkg/github/github.go b/pkg/event_processor/github/github.go similarity index 100% rename from pkg/github/github.go rename to pkg/event_processor/github/github.go diff --git a/pkg/github/github_test.go b/pkg/event_processor/github/github_test.go similarity index 100% rename from pkg/github/github_test.go rename to pkg/event_processor/github/github_test.go diff --git a/pkg/interceptor/edp_interceptor.go b/pkg/interceptor/edp_interceptor.go index e1228344..8394a5df 100644 --- a/pkg/interceptor/edp_interceptor.go +++ b/pkg/interceptor/edp_interceptor.go @@ -34,11 +34,12 @@ type EDPInterceptorInterface interface { // EDPInterceptor is an interceptor for EDP. type EDPInterceptor struct { - gitHubProcessor event_processor.Processor - gitLabProcessor event_processor.Processor - gerritProcessor event_processor.Processor - client ctrlClient.Reader - logger *zap.SugaredLogger + gitHubProcessor event_processor.Processor + gitLabProcessor event_processor.Processor + gerritProcessor event_processor.Processor + bitbucketProcessor event_processor.Processor + client ctrlClient.Reader + logger *zap.SugaredLogger } // NewEDPInterceptor creates a new EDPInterceptor. @@ -47,14 +48,16 @@ func NewEDPInterceptor( gitHubProcessor event_processor.Processor, gitLabProcessor event_processor.Processor, gerritProcessor event_processor.Processor, + bitbucketProcessor event_processor.Processor, l *zap.SugaredLogger, ) *EDPInterceptor { return &EDPInterceptor{ - gitHubProcessor: gitHubProcessor, - gitLabProcessor: gitLabProcessor, - gerritProcessor: gerritProcessor, - client: c, - logger: l, + gitHubProcessor: gitHubProcessor, + gitLabProcessor: gitLabProcessor, + gerritProcessor: gerritProcessor, + bitbucketProcessor: bitbucketProcessor, + client: c, + logger: l, } } @@ -152,6 +155,7 @@ func (i *EDPInterceptor) Process(ctx context.Context, r *triggersv1.InterceptorR func (i *EDPInterceptor) processEvent(ctx context.Context, r *triggersv1.InterceptorRequest) (*event_processor.EventInfo, error) { githubEventType, isGitHubEvent := r.Header["X-Github-Event"] gitLabEventType, isGitLabEvent := r.Header["X-Gitlab-Event"] + bitbucketEventType, isBitbucketEvent := r.Header["X-Event-Key"] ns, _ := triggersv1.ParseTriggerID(r.Context.TriggerID) if isGitLabEvent { @@ -172,6 +176,15 @@ func (i *EDPInterceptor) processEvent(ctx context.Context, r *triggersv1.Interce return event, nil } + if isBitbucketEvent { + event, err := i.bitbucketProcessor.Process(ctx, []byte(r.Body), ns, getEventTypeFromHeader(bitbucketEventType)) + if err != nil { + return nil, fmt.Errorf("failed to process Bitbucket event: %w", err) + } + + return event, nil + } + event, err := i.gerritProcessor.Process(ctx, []byte(r.Body), ns, "") if err != nil { return nil, fmt.Errorf("failed to process Gerrit event: %w", err) diff --git a/pkg/interceptor/edp_interceptor_test.go b/pkg/interceptor/edp_interceptor_test.go index 302ec84e..6c4974d0 100644 --- a/pkg/interceptor/edp_interceptor_test.go +++ b/pkg/interceptor/edp_interceptor_test.go @@ -124,6 +124,7 @@ func TestEDPInterceptor_Execute(t *testing.T) { mocks.NewProcessor(t), mocks.NewProcessor(t), tt.gerritProcessor(t), + mocks.NewProcessor(t), zap.NewNop().Sugar(), ) @@ -149,10 +150,11 @@ func TestEDPInterceptor_Process(t *testing.T) { require.NoError(t, codebaseApi.AddToScheme(scheme)) type fields struct { - gitHubProcessor func(t *testing.T) event_processor.Processor - gitLabProcessor func(t *testing.T) event_processor.Processor - gerritProcessor func(t *testing.T) event_processor.Processor - kubeObjects []client.Object + gitHubProcessor func(t *testing.T) event_processor.Processor + gitLabProcessor func(t *testing.T) event_processor.Processor + gerritProcessor func(t *testing.T) event_processor.Processor + bitBucketProcessor func(t *testing.T) event_processor.Processor + kubeObjects []client.Object } type args struct { @@ -206,6 +208,9 @@ func TestEDPInterceptor_Process(t *testing.T) { gerritProcessor: func(t *testing.T) event_processor.Processor { return &mocks.Processor{} }, + bitBucketProcessor: func(t *testing.T) event_processor.Processor { + return &mocks.Processor{} + }, kubeObjects: []client.Object{ &codebaseApi.CodebaseBranch{ ObjectMeta: metav1.ObjectMeta{ @@ -308,6 +313,9 @@ func TestEDPInterceptor_Process(t *testing.T) { gerritProcessor: func(t *testing.T) event_processor.Processor { return &mocks.Processor{} }, + bitBucketProcessor: func(t *testing.T) event_processor.Processor { + return &mocks.Processor{} + }, kubeObjects: []client.Object{ &codebaseApi.CodebaseBranch{ ObjectMeta: metav1.ObjectMeta{ @@ -405,6 +413,9 @@ func TestEDPInterceptor_Process(t *testing.T) { return m }, + bitBucketProcessor: func(t *testing.T) event_processor.Processor { + return &mocks.Processor{} + }, kubeObjects: []client.Object{ &codebaseApi.CodebaseBranch{ ObjectMeta: metav1.ObjectMeta{ @@ -495,6 +506,9 @@ func TestEDPInterceptor_Process(t *testing.T) { return m }, + bitBucketProcessor: func(t *testing.T) event_processor.Processor { + return &mocks.Processor{} + }, }, args: args{ r: &triggersv1.InterceptorRequest{ @@ -554,6 +568,9 @@ func TestEDPInterceptor_Process(t *testing.T) { return m }, + bitBucketProcessor: func(t *testing.T) event_processor.Processor { + return &mocks.Processor{} + }, }, args: args{ r: &triggersv1.InterceptorRequest{ @@ -598,6 +615,9 @@ func TestEDPInterceptor_Process(t *testing.T) { return m }, + bitBucketProcessor: func(t *testing.T) event_processor.Processor { + return &mocks.Processor{} + }, }, args: args{ r: &triggersv1.InterceptorRequest{ @@ -644,6 +664,7 @@ func TestEDPInterceptor_Process(t *testing.T) { tt.fields.gitHubProcessor(t), tt.fields.gitLabProcessor(t), tt.fields.gerritProcessor(t), + tt.fields.bitBucketProcessor(t), zap.NewNop().Sugar(), )