From 777045cdf5d513b7d96dbc52d45a41942528566a Mon Sep 17 00:00:00 2001 From: StephanHCB Date: Wed, 5 Jun 2024 15:57:20 +0200 Subject: [PATCH 1/6] feat(#292): add webhook and skeleton service --- go.mod | 1 + go.sum | 3 + internal/acorn/service/prvalidatorint.go | 13 +++ internal/service/prvalidator/prvalidator.go | 42 ++++++++++ internal/web/app/app.go | 7 +- .../web/controller/webhookctl/webhookctl.go | 84 +++++++++++++++++-- 6 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 internal/acorn/service/prvalidatorint.go create mode 100644 internal/service/prvalidator/prvalidator.go diff --git a/go.mod b/go.mod index 43cbabe..f5afc7d 100644 --- a/go.mod +++ b/go.mod @@ -51,6 +51,7 @@ require ( github.com/elastic/go-windows v1.0.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-playground/webhooks/v6 v6.3.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect diff --git a/go.sum b/go.sum index d6699f3..a4e14ad 100644 --- a/go.sum +++ b/go.sum @@ -94,9 +94,12 @@ github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZt github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a h1:v6zMvHuY9yue4+QkG/HQ/W67wvtQmWJ4SDo9aK/GIno= github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a/go.mod h1:I79BieaU4fxrw4LMXby6q5OS9XnoR9UIKLOzDFjUmuw= +github.com/go-playground/webhooks/v6 v6.3.0 h1:zBLUxK1Scxwi97TmZt5j/B/rLlard2zY7P77FHg58FE= +github.com/go-playground/webhooks/v6 v6.3.0/go.mod h1:GCocmfMtpJdkEOM1uG9p2nXzg1kY5X/LtvQgtPHUaaA= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogits/go-gogs-client v0.0.0-20200905025246-8bb8a50cb355/go.mod h1:cY2AIrMgHm6oOHmR7jY+9TtjzSjQ3iG7tURJG3Y6XH0= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= diff --git a/internal/acorn/service/prvalidatorint.go b/internal/acorn/service/prvalidatorint.go new file mode 100644 index 0000000..07af0c7 --- /dev/null +++ b/internal/acorn/service/prvalidatorint.go @@ -0,0 +1,13 @@ +package service + +// PRValidator validates pull requests in the underlying repository to prevent bringing invalid content to the mainline. +type PRValidator interface { + IsPRValidator() bool + + // ValidatePullRequest validates the pull request, commenting on it and setting a build result. + // + // Failures to validate a pull request are not considered errors. Errors are only returned if + // the process of validation could not be completed (failure to respond by git server, + // could not obtain file list, etc.) + ValidatePullRequest(id uint64, toRef string, fromRef string) error +} diff --git a/internal/service/prvalidator/prvalidator.go b/internal/service/prvalidator/prvalidator.go new file mode 100644 index 0000000..9433f61 --- /dev/null +++ b/internal/service/prvalidator/prvalidator.go @@ -0,0 +1,42 @@ +package prvalidator + +import ( + "github.com/Interhyp/metadata-service/internal/acorn/config" + "github.com/Interhyp/metadata-service/internal/acorn/repository" + "github.com/Interhyp/metadata-service/internal/acorn/service" + librepo "github.com/StephanHCB/go-backend-service-common/acorns/repository" +) + +type Impl struct { + Configuration librepo.Configuration + CustomConfiguration config.CustomConfiguration + Logging librepo.Logging + Timestamp librepo.Timestamp + BitBucket repository.Bitbucket +} + +func New( + configuration librepo.Configuration, + customConfig config.CustomConfiguration, + logging librepo.Logging, + timestamp librepo.Timestamp, + bitbucket repository.Bitbucket, +) service.PRValidator { + return &Impl{ + Configuration: configuration, + CustomConfiguration: customConfig, + Logging: logging, + Timestamp: timestamp, + BitBucket: bitbucket, + } +} + +func (s *Impl) IsPRValidator() bool { + return true +} + +func (s *Impl) ValidatePullRequest(id uint64, toRef string, fromRef string) error { + // TODO + + return nil +} diff --git a/internal/web/app/app.go b/internal/web/app/app.go index 15f80b4..0c9cd70 100644 --- a/internal/web/app/app.go +++ b/internal/web/app/app.go @@ -17,6 +17,7 @@ import ( "github.com/Interhyp/metadata-service/internal/repository/sshAuthProvider" "github.com/Interhyp/metadata-service/internal/service/mapper" "github.com/Interhyp/metadata-service/internal/service/owners" + "github.com/Interhyp/metadata-service/internal/service/prvalidator" "github.com/Interhyp/metadata-service/internal/service/repositories" "github.com/Interhyp/metadata-service/internal/service/services" "github.com/Interhyp/metadata-service/internal/service/trigger" @@ -59,6 +60,7 @@ type ApplicationImpl struct { Owners service.Owners Services service.Services Repositories service.Repositories + PRValidator service.PRValidator // controllers (incoming connectors) HealthCtl libcontroller.HealthController @@ -228,6 +230,9 @@ func (a *ApplicationImpl) ConstructServices() error { return err } + a.PRValidator = prvalidator.New(a.Config, a.CustomConfig, a.Logging, a.Timestamp, a.Bitbucket) + // no setup required + return nil } @@ -239,7 +244,7 @@ func (a *ApplicationImpl) ConstructControllers() error { a.OwnerCtl = ownerctl.New(a.Config, a.CustomConfig, a.Logging, a.Timestamp, a.Owners) a.ServiceCtl = servicectl.New(a.Config, a.CustomConfig, a.Logging, a.Timestamp, a.Services) a.RepositoryCtl = repositoryctl.New(a.Config, a.CustomConfig, a.Logging, a.Timestamp, a.Repositories) - a.WebhookCtl = webhookctl.New(a.Logging, a.Timestamp, a.Updater) + a.WebhookCtl = webhookctl.New(a.Logging, a.Timestamp, a.Updater, a.PRValidator) a.Server = server.New(a.Config, a.CustomConfig, a.Logging, a.IdentityProvider, a.HealthCtl, a.SwaggerCtl, a.OwnerCtl, a.ServiceCtl, a.RepositoryCtl, a.WebhookCtl) diff --git a/internal/web/controller/webhookctl/webhookctl.go b/internal/web/controller/webhookctl/webhookctl.go index a60cc21..6db4912 100644 --- a/internal/web/controller/webhookctl/webhookctl.go +++ b/internal/web/controller/webhookctl/webhookctl.go @@ -2,9 +2,11 @@ package webhookctl import ( "context" + "fmt" "github.com/Interhyp/metadata-service/internal/acorn/controller" "github.com/Interhyp/metadata-service/internal/acorn/service" "github.com/StephanHCB/go-backend-service-common/web/util/contexthelper" + bitbucketserver "github.com/go-playground/webhooks/v6/bitbucket-server" "net/http" "github.com/Interhyp/metadata-service/internal/web/util" @@ -14,20 +16,23 @@ import ( ) type Impl struct { - Logging librepo.Logging - Timestamp librepo.Timestamp - Updater service.Updater + Logging librepo.Logging + Timestamp librepo.Timestamp + Updater service.Updater + PRValidator service.PRValidator } func New( logging librepo.Logging, timestamp librepo.Timestamp, updater service.Updater, + prValidator service.PRValidator, ) controller.WebhookController { return &Impl{ - Logging: logging, - Timestamp: timestamp, - Updater: updater, + Logging: logging, + Timestamp: timestamp, + Updater: updater, + PRValidator: prValidator, } } @@ -37,10 +42,12 @@ func (c *Impl) IsWebhookController() bool { func (c *Impl) WireUp(_ context.Context, router chi.Router) { router.Post("/webhook", c.Webhook) + router.Post("/webhook/bitbucket", c.WebhookBitBucket) } // --- handlers --- +// Webhook is deprecated and will be removed after the switch func (c *Impl) Webhook(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -55,3 +62,68 @@ func (c *Impl) Webhook(w http.ResponseWriter, r *http.Request) { util.SuccessNoBody(ctx, w, r, http.StatusNoContent) } + +func (c *Impl) WebhookBitBucket(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + routineCtx, routineCtxCancel := contexthelper.AsyncCopyRequestContext(ctx, "webhookBitbucket", "backgroundJob") + go func() { + defer routineCtxCancel() + + aulogging.Logger.Ctx(routineCtx).Info().Printf("received webhook from BitBucket") + webhook, err := bitbucketserver.New() // we don't need signature checking here + if err != nil { + aulogging.Logger.Ctx(routineCtx).Error().WithErr(err).Printf("unexpected error while instantiating bitbucket webhook parser - ignoring webhook") + return + } + + eventPayload, err := webhook.Parse(r, bitbucketserver.DiagnosticsPingEvent, bitbucketserver.PullRequestOpenedEvent, + bitbucketserver.RepositoryReferenceChangedEvent, bitbucketserver.PullRequestModifiedEvent) + if err != nil { + aulogging.Logger.Ctx(routineCtx).Error().WithErr(err).Printf("bad request error while parsing bitbucket webhook payload - ignoring webhook") + return + } + + switch eventPayload.(type) { + case bitbucketserver.PullRequestOpenedPayload: + payload, ok := eventPayload.(bitbucketserver.PullRequestOpenedPayload) + c.validatePullRequest(routineCtx, "opened", ok, payload.PullRequest) + case bitbucketserver.PullRequestModifiedPayload: + payload, ok := eventPayload.(bitbucketserver.PullRequestModifiedPayload) + c.validatePullRequest(routineCtx, "modified", ok, payload.PullRequest) + case bitbucketserver.RepositoryReferenceChangedPayload: + payload, ok := eventPayload.(bitbucketserver.RepositoryReferenceChangedPayload) + if !ok || len(payload.Changes) < 1 || payload.Changes[0].ReferenceID == "" { + aulogging.Logger.Ctx(routineCtx).Error().Printf("bad request while processing bitbucket webhook - got reference changed with invalid info - ignoring webhook") + return + } + aulogging.Logger.Ctx(routineCtx).Info().Printf("got repository reference changed, refreshing caches") + + err = c.Updater.PerformFullUpdateWithNotifications(routineCtx) + if err != nil { + aulogging.Logger.Ctx(routineCtx).Error().WithErr(err).Printf("webhook error") + } + default: + // ignore unknown events + } + }() + + util.SuccessNoBody(ctx, w, r, http.StatusNoContent) +} + +func (c *Impl) validatePullRequest(ctx context.Context, operation string, parsedOk bool, pullRequestPayload bitbucketserver.PullRequest) { + description := fmt.Sprintf("id: %d, toRef: %s, fromRef: %s", pullRequestPayload.ID, pullRequestPayload.ToRef.ID, pullRequestPayload.FromRef.ID) + if !parsedOk || pullRequestPayload.ID == 0 || pullRequestPayload.ToRef.ID == "" || pullRequestPayload.FromRef.ID == "" { + aulogging.Logger.Ctx(ctx).Error().Printf("bad request while processing bitbucket webhook - got pull request %s with invalid info (%s) - ignoring webhook", operation, description) + return + } + aulogging.Logger.Ctx(ctx).Info().Printf("got pull request %s (%s)", operation, description) + + err := c.PRValidator.ValidatePullRequest(pullRequestPayload.ID, pullRequestPayload.ToRef.ID, pullRequestPayload.FromRef.ID) + if err != nil { + aulogging.Logger.Ctx(ctx).Error().WithErr(err).Printf("error while processing bitbucket webhook: pull request %s (%s): %s", operation, description, err.Error()) + return + } + + aulogging.Logger.Ctx(ctx).Info().Printf("successfully processed pull request %s (%s) event", operation, description) +} From e8dba055056aa2340fe1906fb73d045e5310d3d1 Mon Sep 17 00:00:00 2001 From: StephanHCB Date: Thu, 6 Jun 2024 15:25:16 +0200 Subject: [PATCH 2/6] feat(#292): add BB api calls for pullrequest changes --- internal/acorn/config/customconfigint.go | 2 + internal/acorn/repository/bitbucketint.go | 7 + .../repository/bitbucket/bbclient/bbclient.go | 67 ++++++ .../bitbucket/bbclientint/bbclientint.go | 4 + .../bitbucket/bbclientint/bbmodels.go | 194 ++++++++++++++++++ internal/repository/bitbucket/repository.go | 55 ++++- .../repository/bitbucket/repository_test.go | 4 +- internal/repository/config/accessors.go | 36 ++++ internal/repository/config/plumbing.go | 6 +- internal/web/app/app.go | 2 +- test/mock/bbclientmock/bbclientmock.go | 15 ++ test/mock/bitbucketmock/bitbucketmock.go | 4 + test/mock/configmock/configmock.go | 8 + 13 files changed, 393 insertions(+), 11 deletions(-) create mode 100644 internal/repository/bitbucket/bbclientint/bbmodels.go diff --git a/internal/acorn/config/customconfigint.go b/internal/acorn/config/customconfigint.go index 1007b50..b3e73c5 100644 --- a/internal/acorn/config/customconfigint.go +++ b/internal/acorn/config/customconfigint.go @@ -33,6 +33,8 @@ type CustomConfiguration interface { MetadataRepoUrl() string MetadataRepoMainline() string + MetadataRepoProject() string + MetadataRepoName() string UpdateJobIntervalCronPart() string UpdateJobTimeoutSeconds() uint16 diff --git a/internal/acorn/repository/bitbucketint.go b/internal/acorn/repository/bitbucketint.go index 5780c35..eebc3ba 100644 --- a/internal/acorn/repository/bitbucketint.go +++ b/internal/acorn/repository/bitbucketint.go @@ -14,6 +14,8 @@ type Bitbucket interface { GetBitbucketUser(ctx context.Context, username string) (BitbucketUser, error) GetBitbucketUsers(ctx context.Context, usernames []string) ([]BitbucketUser, error) FilterExistingUsernames(ctx context.Context, usernames []string) ([]string, error) + + GetChangedFilesOnPullRequest(ctx context.Context, pullRequestId int) ([]File, error) } type BitbucketUser struct { @@ -21,3 +23,8 @@ type BitbucketUser struct { Name string `json:"name"` Active bool `json:"active"` } + +type File struct { + Path string + Contents string +} diff --git a/internal/repository/bitbucket/bbclient/bbclient.go b/internal/repository/bitbucket/bbclient/bbclient.go index 5f8ccc2..37fc1b5 100644 --- a/internal/repository/bitbucket/bbclient/bbclient.go +++ b/internal/repository/bitbucket/bbclient/bbclient.go @@ -167,3 +167,70 @@ func (c *Impl) GetBitbucketUser(ctx context.Context, username string) (repositor err := c.call(ctx, http.MethodGet, urlExt, nil, &response) return response, err } + +func (c *Impl) GetPullRequest(ctx context.Context, projectKey string, repositorySlug string, pullRequestId int32) (bbclientint.PullRequest, error) { + urlExt := fmt.Sprintf("%s/projects/%s/repos/%s/pull-requests/%d", + bbclientint.CoreApi, + url.PathEscape(projectKey), + url.PathEscape(repositorySlug), + pullRequestId) + response := bbclientint.PullRequest{} + err := c.call(ctx, http.MethodGet, urlExt, nil, &response) + return response, err +} + +func (c *Impl) GetChanges(ctx context.Context, projectKey string, repositorySlug string, sinceHash string, untilHash string) (bbclientint.Changes, error) { + // since : main + // until : pr head + urlExt := fmt.Sprintf("%s/projects/%s/repos/%s/changes?since=%s&until=%s&limit=%d", + bbclientint.CoreApi, + url.PathEscape(projectKey), + url.PathEscape(repositorySlug), + url.QueryEscape(sinceHash), + url.QueryEscape(untilHash), + 1000) // TODO pagination? + response := bbclientint.Changes{} + err := c.call(ctx, http.MethodGet, urlExt, nil, &response) + return response, err +} + +func (c *Impl) getFileContentsPage(ctx context.Context, projectKey string, repositorySlug string, atHash string, path string, start int, limit int) (bbclientint.PaginatedLines, error) { + escapedPath := "" + for _, pathComponent := range strings.Split(path, "/") { + escapedPath += "/" + url.PathEscape(pathComponent) + } + urlExt := fmt.Sprintf("%s/projects/%s/repos/%s/browse/%s?at=%s&start=%d&limit=%d", + bbclientint.CoreApi, + url.PathEscape(projectKey), + url.PathEscape(repositorySlug), + escapedPath, + url.QueryEscape(atHash), + start, + limit) + response := bbclientint.PaginatedLines{} + err := c.call(ctx, http.MethodGet, urlExt, nil, &response) + return response, err +} + +func (c *Impl) GetFileContentsAt(ctx context.Context, projectKey string, repositorySlug string, atHash string, path string) (string, error) { + var contents strings.Builder + var err error + start := 0 + + page := bbclientint.PaginatedLines{ + IsLastPage: false, + NextPageStart: &start, + } + + for !page.IsLastPage && page.NextPageStart != nil { + page, err = c.getFileContentsPage(ctx, projectKey, repositorySlug, atHash, path, *page.NextPageStart, 1000) + if err != nil { + return contents.String(), err + } + for _, line := range page.Lines { + contents.WriteString(line.Text + "\n") + } + } + + return contents.String(), nil +} diff --git a/internal/repository/bitbucket/bbclientint/bbclientint.go b/internal/repository/bitbucket/bbclientint/bbclientint.go index deba004..5958034 100644 --- a/internal/repository/bitbucket/bbclientint/bbclientint.go +++ b/internal/repository/bitbucket/bbclientint/bbclientint.go @@ -9,6 +9,10 @@ type BitbucketClient interface { Setup() error GetBitbucketUser(ctx context.Context, username string) (repository.BitbucketUser, error) + + GetPullRequest(ctx context.Context, projectKey string, repositorySlug string, pullRequestId int32) (PullRequest, error) + GetChanges(ctx context.Context, projectKey string, repositorySlug string, sinceHash string, untilHash string) (Changes, error) + GetFileContentsAt(ctx context.Context, projectKey string, repositorySlug string, atHash string, path string) (string, error) } const ( diff --git a/internal/repository/bitbucket/bbclientint/bbmodels.go b/internal/repository/bitbucket/bbclientint/bbmodels.go new file mode 100644 index 0000000..cf297be --- /dev/null +++ b/internal/repository/bitbucket/bbclientint/bbmodels.go @@ -0,0 +1,194 @@ +package bbclientint + +// not part of spec + +type PaginatedLines struct { + Lines []struct { + Text string `json:"text"` + } `json:"lines"` + Start int `json:"start"` + Size int `json:"size"` + IsLastPage bool `json:"isLastPage"` + Limit int `json:"limit"` + NextPageStart *int `json:"nextPageStart"` +} + +// part of spec, sorted alphabetically + +type Changes struct { + FromHash string `json:"fromHash"` + ToHash string `json:"toHash"` + Values []Change `json:"values,omitempty"` + Size int `json:"size"` + IsLastPage bool `json:"isLastPage"` + Start int `json:"start"` + Limit int `json:"limit"` + NextPageStart *int `json:"nextPageStart"` +} + +type Change struct { + ContentId string `json:"contentId"` + FromContentId string `json:"fromContentId"` + Path struct { + Components []string `json:"components"` + Parent string `json:"parent"` + Name string `json:"name"` + Extension string `json:"extension"` + ToString string `json:"toString"` + } `json:"path"` + Executable bool `json:"executable"` + PercentUnchanged int `json:"percentUnchanged"` + Type string `json:"type"` + NodeType string `json:"nodeType"` + SrcExecutable bool `json:"srcExecutable"` + Links struct { + Self []struct { + Href string `json:"href"` + } `json:"self"` + } `json:"links"` + Properties struct { + GitChangeType string `json:"gitChangeType"` + } `json:"properties"` +} + +type Link struct { + Href string `yaml:"href" json:"href"` + Name *string `yaml:"name,omitempty" json:"name,omitempty"` +} + +type ProjectLinks struct { + Self []Link `yaml:"self,omitempty" json:"self,omitempty"` +} + +type PullRequest struct { + Id int64 `yaml:"id" json:"id"` + Version *int32 `yaml:"version,omitempty" json:"version,omitempty"` + Title string `yaml:"title" json:"title"` + Description string `yaml:"description" json:"description"` + State PullRequestState `yaml:"state" json:"state"` + Open bool `yaml:"open" json:"open"` + Closed bool `yaml:"closed" json:"closed"` + CreatedDate *int64 `yaml:"createdDate,omitempty" json:"createdDate,omitempty"` + UpdatedDate *int64 `yaml:"updatedDate,omitempty" json:"updatedDate,omitempty"` + FromRef RepositoryRef `yaml:"fromRef" json:"fromRef"` + ToRef RepositoryRef `yaml:"toRef" json:"toRef"` + Locked bool `yaml:"locked" json:"locked"` + Author *UserRole `yaml:"author,omitempty" json:"author,omitempty"` + Reviewers []UserRole `yaml:"reviewers,omitempty" json:"reviewers,omitempty"` + Participants []UserRole `yaml:"participants,omitempty" json:"participants,omitempty"` + Links *ProjectLinks `yaml:"links,omitempty" json:"links,omitempty"` +} + +type PullRequestComment struct { + Properties PullRequestCommentProperties `yaml:"properties" json:"properties"` + Id int32 `yaml:"id" json:"id"` + Version int32 `yaml:"version" json:"version"` + Text string `yaml:"text" json:"text"` + Author PullRequestCommentAuthor `yaml:"author" json:"author"` + CreatedDate int64 `yaml:"createdDate" json:"createdDate"` + UpdatedDate int64 `yaml:"updatedDate" json:"updatedDate"` + Comments []PullRequestComment `yaml:"comments" json:"comments"` + Tasks []interface{} `yaml:"tasks" json:"tasks"` + Severity string `yaml:"severity" json:"severity"` + State string `yaml:"state" json:"state"` + PermittedOperations PullRequestCommentPermittedOperations `yaml:"permittedOperations" json:"permittedOperations"` +} + +type PullRequestCommentAuthor struct { + Name string `yaml:"name" json:"name"` + EmailAddress string `yaml:"emailAddress" json:"emailAddress"` + Id int32 `yaml:"id" json:"id"` + DisplayName string `yaml:"displayName" json:"displayName"` + Active bool `yaml:"active" json:"active"` + Slug string `yaml:"slug" json:"slug"` + Type string `yaml:"type" json:"type"` +} + +type PullRequestCommentPage struct { + Size int32 `yaml:"size" json:"size"` + Limit int32 `yaml:"limit" json:"limit"` + Start int32 `yaml:"start" json:"start"` + IsLastPage bool `yaml:"isLastPage" json:"isLastPage"` + NextPageStart *int32 `yaml:"nextPageStart,omitempty" json:"nextPageStart,omitempty"` + Values []PullRequestComment `yaml:"values" json:"values"` +} + +type PullRequestCommentPermittedOperations struct { + Editable bool `yaml:"editable" json:"editable"` + Deletable bool `yaml:"deletable" json:"deletable"` +} + +type PullRequestCommentProperties struct { + Key string `yaml:"key" json:"key"` +} + +type PullRequestCommentRequest struct { + Text string `yaml:"text" json:"text"` + Parent *PullRequestCommentRequestParent `yaml:"parent,omitempty" json:"parent,omitempty"` + Anchor *PullRequestCommentRequestAnchor `yaml:"anchor,omitempty" json:"anchor,omitempty"` + Severity *string `yaml:"severity,omitempty" json:"severity,omitempty"` + State *string `yaml:"state,omitempty" json:"state,omitempty"` +} + +type PullRequestCommentRequestAnchor struct { + Line *int32 `yaml:"line,omitempty" json:"line,omitempty"` + LineType *string `yaml:"lineType,omitempty" json:"lineType,omitempty"` + FileType *string `yaml:"fileType,omitempty" json:"fileType,omitempty"` + Path *string `yaml:"path,omitempty" json:"path,omitempty"` + SrcPath *string `yaml:"srcPath,omitempty" json:"srcPath,omitempty"` +} + +type PullRequestCommentRequestParent struct { + Id int32 `yaml:"id" json:"id"` + Severity *string `yaml:"severity,omitempty" json:"severity,omitempty"` + State *string `yaml:"state,omitempty" json:"state,omitempty"` +} + +type PullRequestState string + +// List of pullRequestState +const ( + OPEN PullRequestState = "OPEN" + MERGED PullRequestState = "MERGED" + DECLINED PullRequestState = "DECLINED" +) + +// All allowed values of PullRequestState enum +var AllowedPullRequestStateEnumValues = []PullRequestState{ + "OPEN", + "MERGED", + "DECLINED", +} + +type RepositoryRef struct { + Id string `yaml:"id" json:"id"` + LatestCommit string `yaml:"latestCommit" json:"latestCommit"` + Repository RepositoryRefRepository `yaml:"repository" json:"repository"` +} + +type RepositoryRefRepository struct { + Slug string `yaml:"slug" json:"slug"` + Name *string `yaml:"name,omitempty" json:"name,omitempty"` + Project RepositoryRefRepositoryProject `yaml:"project" json:"project"` +} + +type RepositoryRefRepositoryProject struct { + Key string `yaml:"key" json:"key"` +} + +type User struct { + Id *int32 `yaml:"id,omitempty" json:"id,omitempty"` + Name string `yaml:"name" json:"name"` + EmailAddress *string `yaml:"emailAddress,omitempty" json:"emailAddress,omitempty"` + DisplayName *string `yaml:"displayName,omitempty" json:"displayName,omitempty"` + Active bool `yaml:"active" json:"active"` + Slug string `yaml:"slug" json:"slug"` + Type *string `yaml:"type,omitempty" json:"type,omitempty"` +} + +type UserRole struct { + User User `yaml:"user" json:"user"` + Role *string `yaml:"role,omitempty" json:"role,omitempty"` + Approved *bool `yaml:"approved,omitempty" json:"approved,omitempty"` + Status *string `yaml:"status,omitempty" json:"status,omitempty"` +} diff --git a/internal/repository/bitbucket/repository.go b/internal/repository/bitbucket/repository.go index 2b15aa8..4027d1c 100644 --- a/internal/repository/bitbucket/repository.go +++ b/internal/repository/bitbucket/repository.go @@ -3,10 +3,12 @@ package bitbucket import ( "context" "fmt" + "github.com/Interhyp/metadata-service/internal/acorn/config" "github.com/Interhyp/metadata-service/internal/acorn/errors/httperror" "github.com/Interhyp/metadata-service/internal/acorn/repository" "github.com/Interhyp/metadata-service/internal/repository/bitbucket/bbclient" "github.com/Interhyp/metadata-service/internal/repository/bitbucket/bbclientint" + aulogging "github.com/StephanHCB/go-autumn-logging" auzerolog "github.com/StephanHCB/go-autumn-logging-zerolog" librepo "github.com/StephanHCB/go-backend-service-common/acorns/repository" "net/http" @@ -14,23 +16,26 @@ import ( ) type Impl struct { - Configuration librepo.Configuration - Logging librepo.Logging - Vault librepo.Vault + Configuration librepo.Configuration + CustomConfiguration config.CustomConfiguration + Logging librepo.Logging + Vault librepo.Vault LowLevel bbclientint.BitbucketClient } func New( configuration librepo.Configuration, + customConfiguration config.CustomConfiguration, logging librepo.Logging, vault librepo.Vault, ) repository.Bitbucket { return &Impl{ - Configuration: configuration, - Logging: logging, - Vault: vault, - LowLevel: bbclient.New(configuration, logging, vault), + Configuration: configuration, + CustomConfiguration: customConfiguration, + Logging: logging, + Vault: vault, + LowLevel: bbclient.New(configuration, logging, vault), } } @@ -108,3 +113,39 @@ func Unique[T comparable](sliceList []T) []T { } return list } + +func (r *Impl) GetChangedFilesOnPullRequest(ctx context.Context, pullRequestId int) ([]repository.File, error) { + aulogging.Logger.Ctx(ctx).Info().Printf("obtaining changes for pull request %d", pullRequestId) + + project := r.CustomConfiguration.MetadataRepoProject() + slug := r.CustomConfiguration.MetadataRepoName() + pullRequest, err := r.LowLevel.GetPullRequest(ctx, project, slug, int32(pullRequestId)) + if err != nil { + return nil, err + } + + prSourceHead := pullRequest.FromRef.LatestCommit + changes, err := r.LowLevel.GetChanges(ctx, project, slug, pullRequest.ToRef.LatestCommit, prSourceHead) + if err != nil { + return nil, err + } + + aulogging.Logger.Ctx(ctx).Info().Printf("pull request had %d changed files", len(changes.Values)) + + result := make([]repository.File, 0) + for _, change := range changes.Values { + contents, err := r.LowLevel.GetFileContentsAt(ctx, project, slug, prSourceHead, change.Path.ToString) + if err != nil { + aulogging.Logger.Ctx(ctx).Info().Printf("failed to retrieve change for %s: %s", change.Path.ToString, err.Error()) + return nil, err + } + + result = append(result, repository.File{ + Path: change.Path.ToString, + Contents: contents, + }) + } + + aulogging.Logger.Ctx(ctx).Info().Printf("successfully obtained changes for pull request %d", pullRequestId) + return result, nil +} diff --git a/internal/repository/bitbucket/repository_test.go b/internal/repository/bitbucket/repository_test.go index eb090f2..bc208ad 100644 --- a/internal/repository/bitbucket/repository_test.go +++ b/internal/repository/bitbucket/repository_test.go @@ -2,6 +2,7 @@ package bitbucket import ( "context" + configint "github.com/Interhyp/metadata-service/internal/acorn/config" "github.com/Interhyp/metadata-service/internal/acorn/errors/httperror" "github.com/Interhyp/metadata-service/internal/acorn/repository" "github.com/Interhyp/metadata-service/internal/repository/config" @@ -29,7 +30,8 @@ func TestNewAndSetup(t *testing.T) { vault := &vaultmock.VaultImpl{} logger := &logging.LoggingImpl{} conf := tstConfig(t) - cut := New(conf, logger, vault) + customConf := configint.Custom(conf) + cut := New(conf, customConf, logger, vault) lowLevel := &bbclientmock.BitbucketClientMock{} cut.(*Impl).LowLevel = lowLevel diff --git a/internal/repository/config/accessors.go b/internal/repository/config/accessors.go index e545efb..4405b84 100644 --- a/internal/repository/config/accessors.go +++ b/internal/repository/config/accessors.go @@ -174,3 +174,39 @@ func (c *CustomConfigImpl) RedisUrl() string { func (c *CustomConfigImpl) RedisPassword() string { return c.VRedisPassword } + +func (c *CustomConfigImpl) MetadataRepoProject() string { + sshUrl := c.SSHMetadataRepositoryUrl() + if sshUrl != "" { + match := c.BitbucketGitUrlMatcher.FindStringSubmatch(sshUrl) + if len(match) == 3 { + return match[1] + } + } + httpUrl := c.MetadataRepoUrl() + if httpUrl != "" { + match := c.BitbucketGitUrlMatcher.FindStringSubmatch(sshUrl) + if len(match) == 3 { + return match[1] + } + } + return "" +} + +func (c *CustomConfigImpl) MetadataRepoName() string { + sshUrl := c.SSHMetadataRepositoryUrl() + if sshUrl != "" { + match := c.BitbucketGitUrlMatcher.FindStringSubmatch(sshUrl) + if len(match) == 3 { + return match[2] + } + } + httpUrl := c.MetadataRepoUrl() + if httpUrl != "" { + match := c.BitbucketGitUrlMatcher.FindStringSubmatch(sshUrl) + if len(match) == 3 { + return match[2] + } + } + return "" +} diff --git a/internal/repository/config/plumbing.go b/internal/repository/config/plumbing.go index 33bbfd0..b377536 100644 --- a/internal/repository/config/plumbing.go +++ b/internal/repository/config/plumbing.go @@ -64,12 +64,14 @@ type CustomConfigImpl struct { VRedisUrl string VRedisPassword string - VKafkaConfig *aukafka.Config + VKafkaConfig *aukafka.Config + BitbucketGitUrlMatcher *regexp.Regexp } func New() (librepo.Configuration, config.CustomConfiguration) { instance := &CustomConfigImpl{ - VKafkaConfig: aukafka.NewConfig(), + VKafkaConfig: aukafka.NewConfig(), + BitbucketGitUrlMatcher: regexp.MustCompile(`/([^/]+)/([^/]+).git$`), } configItems := make([]auconfigapi.ConfigItem, 0) configItems = append(configItems, CustomConfigItems...) diff --git a/internal/web/app/app.go b/internal/web/app/app.go index 0c9cd70..f490c4e 100644 --- a/internal/web/app/app.go +++ b/internal/web/app/app.go @@ -178,7 +178,7 @@ func (a *ApplicationImpl) ConstructRepositories() error { } if a.Bitbucket == nil { - a.Bitbucket = bitbucket.New(a.Config, a.Logging, a.Vault) + a.Bitbucket = bitbucket.New(a.Config, a.CustomConfig, a.Logging, a.Vault) } if err := a.Bitbucket.Setup(); err != nil { return err diff --git a/test/mock/bbclientmock/bbclientmock.go b/test/mock/bbclientmock/bbclientmock.go index 6640c8d..84ad159 100644 --- a/test/mock/bbclientmock/bbclientmock.go +++ b/test/mock/bbclientmock/bbclientmock.go @@ -5,6 +5,7 @@ import ( "errors" "github.com/Interhyp/metadata-service/internal/acorn/errors/httperror" "github.com/Interhyp/metadata-service/internal/acorn/repository" + "github.com/Interhyp/metadata-service/internal/repository/bitbucket/bbclientint" "strings" ) @@ -38,3 +39,17 @@ func (m *BitbucketClientMock) GetBitbucketUser(ctx context.Context, username str func (m *BitbucketClientMock) Setup() error { return nil } + +func (c *BitbucketClientMock) GetPullRequest(ctx context.Context, projectKey string, repositorySlug string, pullRequestId int32) (bbclientint.PullRequest, error) { + response := bbclientint.PullRequest{} + return response, nil +} + +func (c *BitbucketClientMock) GetChanges(ctx context.Context, projectKey string, repositorySlug string, sinceHash string, untilHash string) (bbclientint.Changes, error) { + response := bbclientint.Changes{} + return response, nil +} + +func (c *BitbucketClientMock) GetFileContentsAt(ctx context.Context, projectKey string, repositorySlug string, atHash string, path string) (string, error) { + return "", nil +} diff --git a/test/mock/bitbucketmock/bitbucketmock.go b/test/mock/bitbucketmock/bitbucketmock.go index 59304e0..4080c3b 100644 --- a/test/mock/bitbucketmock/bitbucketmock.go +++ b/test/mock/bitbucketmock/bitbucketmock.go @@ -49,3 +49,7 @@ func (b *BitbucketMock) FilterExistingUsernames(ctx context.Context, usernames [ } return usernames, nil } + +func (b *BitbucketMock) GetChangedFilesOnPullRequest(ctx context.Context, pullRequestId int) ([]repository.File, error) { + return []repository.File{}, nil +} diff --git a/test/mock/configmock/configmock.go b/test/mock/configmock/configmock.go index 5e8f5f0..571d088 100644 --- a/test/mock/configmock/configmock.go +++ b/test/mock/configmock/configmock.go @@ -256,3 +256,11 @@ func (c *MockConfig) RedisUrl() string { func (c *MockConfig) RedisPassword() string { return "" } + +func (c *MockConfig) MetadataRepoProject() string { + return "sample" +} + +func (c *MockConfig) MetadataRepoName() string { + return "sample-repo" +} From d5732e506bf2d3efb23b1d9ace85bdd5bcffbd13 Mon Sep 17 00:00:00 2001 From: StephanHCB Date: Fri, 7 Jun 2024 10:48:14 +0200 Subject: [PATCH 3/6] feat(#292): add BB api calls for build status and pr comment --- internal/acorn/repository/bitbucketint.go | 8 +++- .../repository/bitbucket/bbclient/bbclient.go | 29 ++++++++++++ .../bitbucket/bbclientint/bbclientint.go | 5 ++ .../bitbucket/bbclientint/bbmodels.go | 22 ++++++++- internal/repository/bitbucket/repository.go | 47 +++++++++++++++++-- test/mock/bbclientmock/bbclientmock.go | 14 ++++++ test/mock/bitbucketmock/bitbucketmock.go | 12 ++++- 7 files changed, 128 insertions(+), 9 deletions(-) diff --git a/internal/acorn/repository/bitbucketint.go b/internal/acorn/repository/bitbucketint.go index eebc3ba..655f086 100644 --- a/internal/acorn/repository/bitbucketint.go +++ b/internal/acorn/repository/bitbucketint.go @@ -15,7 +15,13 @@ type Bitbucket interface { GetBitbucketUsers(ctx context.Context, usernames []string) ([]BitbucketUser, error) FilterExistingUsernames(ctx context.Context, usernames []string) ([]string, error) - GetChangedFilesOnPullRequest(ctx context.Context, pullRequestId int) ([]File, error) + // GetChangedFilesOnPullRequest returns the file paths and contents list of changed files, and the + // head commit hash of the pull request source for which the files were obtained. + GetChangedFilesOnPullRequest(ctx context.Context, pullRequestId int) ([]File, string, error) + + AddCommitBuildStatus(ctx context.Context, commitHash string, url string, key string, success bool) error + + CreatePullRequestComment(ctx context.Context, pullRequestId int, comment string) error } type BitbucketUser struct { diff --git a/internal/repository/bitbucket/bbclient/bbclient.go b/internal/repository/bitbucket/bbclient/bbclient.go index 37fc1b5..69908df 100644 --- a/internal/repository/bitbucket/bbclient/bbclient.go +++ b/internal/repository/bitbucket/bbclient/bbclient.go @@ -234,3 +234,32 @@ func (c *Impl) GetFileContentsAt(ctx context.Context, projectKey string, reposit return contents.String(), nil } + +func (c *Impl) AddProjectRepositoryCommitBuildStatus(ctx context.Context, projectKey string, repositorySlug string, commitId string, commitBuildStatusRequest bbclientint.CommitBuildStatusRequest) (aurestclientapi.ParsedResponse, error) { + urlExt := fmt.Sprintf("%s/projects/%s/repos/%s/commits/%s/builds", + bbclientint.CoreApi, + url.PathEscape(projectKey), + url.PathEscape(repositorySlug), + url.PathEscape(commitId)) + + emptyResponse := make([]byte, 0) + responseBodyPointer := &emptyResponse + response := aurestclientapi.ParsedResponse{ + Body: &responseBodyPointer, + } + + err := c.call(ctx, http.MethodPost, urlExt, commitBuildStatusRequest, &response) + return response, err +} + +func (c *Impl) CreatePullRequestComment(ctx context.Context, projectKey string, repositorySlug string, pullRequestId int64, pullRequestCommentRequest bbclientint.PullRequestCommentRequest) (bbclientint.PullRequestComment, error) { + urlExt := fmt.Sprintf("%s/projects/%s/repos/%s/pull-requests/%d/comments", + bbclientint.CoreApi, + url.PathEscape(projectKey), + url.PathEscape(repositorySlug), + pullRequestId) + + response := bbclientint.PullRequestComment{} + err := c.call(ctx, http.MethodPost, urlExt, pullRequestCommentRequest, &response) + return response, err +} diff --git a/internal/repository/bitbucket/bbclientint/bbclientint.go b/internal/repository/bitbucket/bbclientint/bbclientint.go index 5958034..feee23a 100644 --- a/internal/repository/bitbucket/bbclientint/bbclientint.go +++ b/internal/repository/bitbucket/bbclientint/bbclientint.go @@ -3,6 +3,7 @@ package bbclientint import ( "context" "github.com/Interhyp/metadata-service/internal/acorn/repository" + aurestclientapi "github.com/StephanHCB/go-autumn-restclient/api" ) type BitbucketClient interface { @@ -13,6 +14,10 @@ type BitbucketClient interface { GetPullRequest(ctx context.Context, projectKey string, repositorySlug string, pullRequestId int32) (PullRequest, error) GetChanges(ctx context.Context, projectKey string, repositorySlug string, sinceHash string, untilHash string) (Changes, error) GetFileContentsAt(ctx context.Context, projectKey string, repositorySlug string, atHash string, path string) (string, error) + + AddProjectRepositoryCommitBuildStatus(ctx context.Context, projectKey string, repositorySlug string, commitId string, commitBuildStatusRequest CommitBuildStatusRequest) (aurestclientapi.ParsedResponse, error) + + CreatePullRequestComment(ctx context.Context, projectKey string, repositorySlug string, pullRequestId int64, pullRequestCommentRequest PullRequestCommentRequest) (PullRequestComment, error) } const ( diff --git a/internal/repository/bitbucket/bbclientint/bbmodels.go b/internal/repository/bitbucket/bbclientint/bbmodels.go index cf297be..bbef3b0 100644 --- a/internal/repository/bitbucket/bbclientint/bbmodels.go +++ b/internal/repository/bitbucket/bbclientint/bbmodels.go @@ -1,6 +1,6 @@ package bbclientint -// not part of spec +// not part of spec - FileOrDirectory is missing useful fields type PaginatedLines struct { Lines []struct { @@ -51,6 +51,20 @@ type Change struct { } `json:"properties"` } +type CommitBuildStatusRequest struct { + Key string `yaml:"key" json:"key"` + State string `yaml:"state" json:"state"` + Url string `yaml:"url" json:"url"` + BuildNumber *int32 `yaml:"buildNumber,omitempty" json:"buildNumber,omitempty"` + Description *string `yaml:"description,omitempty" json:"description,omitempty"` + Duration *int32 `yaml:"duration,omitempty" json:"duration,omitempty"` + LastUpdated *int32 `yaml:"lastUpdated,omitempty" json:"lastUpdated,omitempty"` + Name *string `yaml:"name,omitempty" json:"name,omitempty"` + Parent *string `yaml:"parent,omitempty" json:"parent,omitempty"` + Ref *string `yaml:"ref,omitempty" json:"ref,omitempty"` + TestResults *TestResults `yaml:"testResults,omitempty" json:"testResults,omitempty"` +} + type Link struct { Href string `yaml:"href" json:"href"` Name *string `yaml:"name,omitempty" json:"name,omitempty"` @@ -176,6 +190,12 @@ type RepositoryRefRepositoryProject struct { Key string `yaml:"key" json:"key"` } +type TestResults struct { + Failed int32 `yaml:"failed" json:"failed"` + Skipped int32 `yaml:"skipped" json:"skipped"` + Successful int32 `yaml:"successful" json:"successful"` +} + type User struct { Id *int32 `yaml:"id,omitempty" json:"id,omitempty"` Name string `yaml:"name" json:"name"` diff --git a/internal/repository/bitbucket/repository.go b/internal/repository/bitbucket/repository.go index 4027d1c..210064a 100644 --- a/internal/repository/bitbucket/repository.go +++ b/internal/repository/bitbucket/repository.go @@ -114,20 +114,20 @@ func Unique[T comparable](sliceList []T) []T { return list } -func (r *Impl) GetChangedFilesOnPullRequest(ctx context.Context, pullRequestId int) ([]repository.File, error) { +func (r *Impl) GetChangedFilesOnPullRequest(ctx context.Context, pullRequestId int) ([]repository.File, string, error) { aulogging.Logger.Ctx(ctx).Info().Printf("obtaining changes for pull request %d", pullRequestId) project := r.CustomConfiguration.MetadataRepoProject() slug := r.CustomConfiguration.MetadataRepoName() pullRequest, err := r.LowLevel.GetPullRequest(ctx, project, slug, int32(pullRequestId)) if err != nil { - return nil, err + return nil, "", err } prSourceHead := pullRequest.FromRef.LatestCommit changes, err := r.LowLevel.GetChanges(ctx, project, slug, pullRequest.ToRef.LatestCommit, prSourceHead) if err != nil { - return nil, err + return nil, prSourceHead, err } aulogging.Logger.Ctx(ctx).Info().Printf("pull request had %d changed files", len(changes.Values)) @@ -137,7 +137,7 @@ func (r *Impl) GetChangedFilesOnPullRequest(ctx context.Context, pullRequestId i contents, err := r.LowLevel.GetFileContentsAt(ctx, project, slug, prSourceHead, change.Path.ToString) if err != nil { aulogging.Logger.Ctx(ctx).Info().Printf("failed to retrieve change for %s: %s", change.Path.ToString, err.Error()) - return nil, err + return nil, prSourceHead, err } result = append(result, repository.File{ @@ -147,5 +147,42 @@ func (r *Impl) GetChangedFilesOnPullRequest(ctx context.Context, pullRequestId i } aulogging.Logger.Ctx(ctx).Info().Printf("successfully obtained changes for pull request %d", pullRequestId) - return result, nil + return result, prSourceHead, nil +} + +func (r *Impl) AddCommitBuildStatus(ctx context.Context, commitHash string, url string, key string, success bool) error { + project := r.CustomConfiguration.MetadataRepoProject() + slug := r.CustomConfiguration.MetadataRepoName() + + state := "FAILED" + if success { + state = "SUCCESS" + } + + request := bbclientint.CommitBuildStatusRequest{ + Key: key, + State: state, + Url: url, + } + + response, err := r.LowLevel.AddProjectRepositoryCommitBuildStatus(ctx, project, slug, commitHash, request) + if err != nil { + return err + } + if response.Status != http.StatusNoContent { + return fmt.Errorf("could not add build status to commit: %d", response.Status) + } + return nil +} + +func (r *Impl) CreatePullRequestComment(ctx context.Context, pullRequestId int, comment string) error { + project := r.CustomConfiguration.MetadataRepoProject() + slug := r.CustomConfiguration.MetadataRepoName() + + request := bbclientint.PullRequestCommentRequest{ + Text: comment, + } + + _, err := r.LowLevel.CreatePullRequestComment(ctx, project, slug, int64(pullRequestId), request) + return err } diff --git a/test/mock/bbclientmock/bbclientmock.go b/test/mock/bbclientmock/bbclientmock.go index 84ad159..09081fd 100644 --- a/test/mock/bbclientmock/bbclientmock.go +++ b/test/mock/bbclientmock/bbclientmock.go @@ -6,6 +6,8 @@ import ( "github.com/Interhyp/metadata-service/internal/acorn/errors/httperror" "github.com/Interhyp/metadata-service/internal/acorn/repository" "github.com/Interhyp/metadata-service/internal/repository/bitbucket/bbclientint" + aurestclientapi "github.com/StephanHCB/go-autumn-restclient/api" + "net/http" "strings" ) @@ -53,3 +55,15 @@ func (c *BitbucketClientMock) GetChanges(ctx context.Context, projectKey string, func (c *BitbucketClientMock) GetFileContentsAt(ctx context.Context, projectKey string, repositorySlug string, atHash string, path string) (string, error) { return "", nil } + +func (c *BitbucketClientMock) AddProjectRepositoryCommitBuildStatus(ctx context.Context, projectKey string, repositorySlug string, commitId string, commitBuildStatusRequest bbclientint.CommitBuildStatusRequest) (aurestclientapi.ParsedResponse, error) { + response := aurestclientapi.ParsedResponse{ + Status: http.StatusCreated, + } + return response, nil +} + +func (c *BitbucketClientMock) CreatePullRequestComment(ctx context.Context, projectKey string, repositorySlug string, pullRequestId int64, pullRequestCommentRequest bbclientint.PullRequestCommentRequest) (bbclientint.PullRequestComment, error) { + response := bbclientint.PullRequestComment{} + return response, nil +} diff --git a/test/mock/bitbucketmock/bitbucketmock.go b/test/mock/bitbucketmock/bitbucketmock.go index 4080c3b..98e1d89 100644 --- a/test/mock/bitbucketmock/bitbucketmock.go +++ b/test/mock/bitbucketmock/bitbucketmock.go @@ -50,6 +50,14 @@ func (b *BitbucketMock) FilterExistingUsernames(ctx context.Context, usernames [ return usernames, nil } -func (b *BitbucketMock) GetChangedFilesOnPullRequest(ctx context.Context, pullRequestId int) ([]repository.File, error) { - return []repository.File{}, nil +func (b *BitbucketMock) GetChangedFilesOnPullRequest(ctx context.Context, pullRequestId int) ([]repository.File, string, error) { + return []repository.File{}, "", nil +} + +func (r *BitbucketMock) AddCommitBuildStatus(ctx context.Context, commitHash string, url string, key string, success bool) error { + return nil +} + +func (r *BitbucketMock) CreatePullRequestComment(ctx context.Context, pullRequestId int, comment string) error { + return nil } From 94a9b9c4598340d6abcfa23fd5c26ff2a049d7a3 Mon Sep 17 00:00:00 2001 From: StephanHCB Date: Fri, 7 Jun 2024 13:18:41 +0200 Subject: [PATCH 4/6] feat(#292): add pr build config (key and url) --- README.md | 2 ++ internal/acorn/config/customconfigint.go | 5 +++++ internal/repository/config/accessors.go | 8 ++++++++ internal/repository/config/config.go | 14 ++++++++++++++ internal/repository/config/plumbing.go | 4 ++++ internal/repository/config/validation_test.go | 2 +- local-config.template.yaml | 3 +++ test/mock/configmock/configmock.go | 8 ++++++++ test/resources/valid-config-unique.yaml | 1 + test/resources/valid-config.yaml | 1 + 10 files changed, 47 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e5e2db4..de11d26 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,8 @@ the [`local-config.yaml`][config] can be used to set the variables. | | | | | `BITBUCKET_USERNAME` | | Name of the user used for basic git authentication to pull and update the metadata repository. | | `BITBUCKET_PASSWORD` | | Password of the user used for basic git authentication to pull and update the metadata repository. | +| `PULL_REQUEST_BUILD_URL` | | Url to link to on pull request builds. Should probably be the public URL of this service. | +| `PULL_REQUEST_BUILD_KEY` | `metadata-service` | Key for pull request builds on pull requests in the underlying git repository. Changed files are syntactically validated. | | | | | | `GIT_COMMITTER_NAME` | | Name of the user used to create the Git commits. | | `GIT_COMMITTER_EMAIL` | | E-Mail of the user used to create the Git commits. | diff --git a/internal/acorn/config/customconfigint.go b/internal/acorn/config/customconfigint.go index b3e73c5..675905a 100644 --- a/internal/acorn/config/customconfigint.go +++ b/internal/acorn/config/customconfigint.go @@ -68,6 +68,9 @@ type CustomConfiguration interface { RedisUrl() string RedisPassword() string + + PullRequestBuildUrl() string + PullRequestBuildKey() string } type NotificationConsumerConfig struct { @@ -125,4 +128,6 @@ const ( KeyAllowedFileCategories = "ALLOWED_FILE_CATEGORIES" KeyRedisUrl = "REDIS_URL" KeyRedisPassword = "REDIS_PASSWORD" + KeyPullRequestBuildUrl = "PULL_REQUEST_BUILD_URL" + KeyPullRequestBuildKey = "PULL_REQUEST_BUILD_KEY" ) diff --git a/internal/repository/config/accessors.go b/internal/repository/config/accessors.go index 4405b84..f17a66b 100644 --- a/internal/repository/config/accessors.go +++ b/internal/repository/config/accessors.go @@ -210,3 +210,11 @@ func (c *CustomConfigImpl) MetadataRepoName() string { } return "" } + +func (c *CustomConfigImpl) PullRequestBuildUrl() string { + return c.VPullRequestBuildUrl +} + +func (c *CustomConfigImpl) PullRequestBuildKey() string { + return c.VPullRequestBuildKey +} diff --git a/internal/repository/config/config.go b/internal/repository/config/config.go index 82a263e..f227e39 100644 --- a/internal/repository/config/config.go +++ b/internal/repository/config/config.go @@ -294,4 +294,18 @@ var CustomConfigItems = []auconfigapi.ConfigItem{ Description: "password used to access the redis", Validate: auconfigapi.ConfigNeedsNoValidation, }, + { + Key: config.KeyPullRequestBuildUrl, + EnvName: config.KeyPullRequestBuildUrl, + Default: "", + Description: "Url that pull request builds should link to.", + Validate: auconfigenv.ObtainPatternValidator("^https?://.*$"), + }, + { + Key: config.KeyPullRequestBuildKey, + EnvName: config.KeyPullRequestBuildKey, + Default: "metadata-service", + Description: "Key to use for pull request builds.", + Validate: auconfigapi.ConfigNeedsNoValidation, + }, } diff --git a/internal/repository/config/plumbing.go b/internal/repository/config/plumbing.go index b377536..e6fc5ff 100644 --- a/internal/repository/config/plumbing.go +++ b/internal/repository/config/plumbing.go @@ -63,6 +63,8 @@ type CustomConfigImpl struct { VAllowedFileCategories []string VRedisUrl string VRedisPassword string + VPullRequestBuildUrl string + VPullRequestBuildKey string VKafkaConfig *aukafka.Config BitbucketGitUrlMatcher *regexp.Regexp @@ -125,6 +127,8 @@ func (c *CustomConfigImpl) Obtain(getter func(key string) string) { c.VAllowedFileCategories, _ = parseAllowedFileCategories(getter(config.KeyAllowedFileCategories)) c.VRedisUrl = getter(config.KeyRedisUrl) c.VRedisPassword = getter(config.KeyRedisPassword) + c.VPullRequestBuildUrl = getter(config.KeyPullRequestBuildUrl) + c.VPullRequestBuildKey = getter(config.KeyPullRequestBuildKey) c.VKafkaConfig.Obtain(getter) } diff --git a/internal/repository/config/validation_test.go b/internal/repository/config/validation_test.go index 29b2688..5ae1b0e 100644 --- a/internal/repository/config/validation_test.go +++ b/internal/repository/config/validation_test.go @@ -78,7 +78,7 @@ func TestValidate_LotsOfErrors(t *testing.T) { _, err := tstSetupCutAndLogRecorder(t, "invalid-config-values.yaml") require.NotNil(t, err) - require.Contains(t, err.Error(), "some configuration values failed to validate or parse. There were 27 error(s). See details above") + require.Contains(t, err.Error(), "some configuration values failed to validate or parse. There were 28 error(s). See details above") actualLog := goauzerolog.RecordedLogForTesting.String() diff --git a/local-config.template.yaml b/local-config.template.yaml index a6e7460..59b5376 100644 --- a/local-config.template.yaml +++ b/local-config.template.yaml @@ -13,6 +13,9 @@ BITBUCKET_SERVER: https://bitbucket.subdomain.com BITBUCKET_CACHE_SIZE: 1000 BITBUCKET_CACHE_RETENTION_SECONDS: 3600 +# Url to this service, used as link in Pull Request validation builds +PULL_REQUEST_BUILD_URL: https://metadata-service.example.com + GIT_COMMITTER_NAME: GIT_COMMITTER_EMAIL: diff --git a/test/mock/configmock/configmock.go b/test/mock/configmock/configmock.go index 571d088..59952f3 100644 --- a/test/mock/configmock/configmock.go +++ b/test/mock/configmock/configmock.go @@ -264,3 +264,11 @@ func (c *MockConfig) MetadataRepoProject() string { func (c *MockConfig) MetadataRepoName() string { return "sample-repo" } + +func (c *MockConfig) PullRequestBuildUrl() string { + return "https://example.com" +} + +func (c *MockConfig) PullRequestBuildKey() string { + return "metadata-service" +} diff --git a/test/resources/valid-config-unique.yaml b/test/resources/valid-config-unique.yaml index 73cf071..a464416 100644 --- a/test/resources/valid-config-unique.yaml +++ b/test/resources/valid-config-unique.yaml @@ -9,6 +9,7 @@ BASIC_AUTH_PASSWORD: some-basic-auth-password BITBUCKET_USERNAME: some-bitbucket-username BITBUCKET_PASSWORD: some-bitbucket-password BITBUCKET_REVIEWER_FALLBACK: username +PULL_REQUEST_BUILD_URL: https://metadata-service.example.com GIT_COMMITTER_NAME: 'Body, Some' GIT_COMMITTER_EMAIL: 'somebody@somewhere.com' diff --git a/test/resources/valid-config.yaml b/test/resources/valid-config.yaml index 76521a9..6e77ce6 100644 --- a/test/resources/valid-config.yaml +++ b/test/resources/valid-config.yaml @@ -4,6 +4,7 @@ LOGSTYLE: plain BITBUCKET_USERNAME: localuser BITBUCKET_REVIEWER_FALLBACK: username +PULL_REQUEST_BUILD_URL: https://metadata-service.example.com AUTH_OIDC_TOKEN_AUDIENCE: some-audience AUTH_GROUP_WRITE: admin From dfed8237a1da742e263b6a93633fe4ba56a705ec Mon Sep 17 00:00:00 2001 From: StephanHCB Date: Fri, 7 Jun 2024 15:35:01 +0200 Subject: [PATCH 5/6] feat(#292): implement success case --- internal/acorn/service/prvalidatorint.go | 4 +- internal/service/prvalidator/prvalidator.go | 65 ++++++++++++++++++- .../web/controller/webhookctl/webhookctl.go | 2 +- 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/internal/acorn/service/prvalidatorint.go b/internal/acorn/service/prvalidatorint.go index 07af0c7..5343940 100644 --- a/internal/acorn/service/prvalidatorint.go +++ b/internal/acorn/service/prvalidatorint.go @@ -1,5 +1,7 @@ package service +import "context" + // PRValidator validates pull requests in the underlying repository to prevent bringing invalid content to the mainline. type PRValidator interface { IsPRValidator() bool @@ -9,5 +11,5 @@ type PRValidator interface { // Failures to validate a pull request are not considered errors. Errors are only returned if // the process of validation could not be completed (failure to respond by git server, // could not obtain file list, etc.) - ValidatePullRequest(id uint64, toRef string, fromRef string) error + ValidatePullRequest(ctx context.Context, id uint64, toRef string, fromRef string) error } diff --git a/internal/service/prvalidator/prvalidator.go b/internal/service/prvalidator/prvalidator.go index 9433f61..697f7b3 100644 --- a/internal/service/prvalidator/prvalidator.go +++ b/internal/service/prvalidator/prvalidator.go @@ -1,10 +1,16 @@ package prvalidator import ( + "context" + "fmt" + openapi "github.com/Interhyp/metadata-service/api" "github.com/Interhyp/metadata-service/internal/acorn/config" "github.com/Interhyp/metadata-service/internal/acorn/repository" "github.com/Interhyp/metadata-service/internal/acorn/service" + aulogging "github.com/StephanHCB/go-autumn-logging" librepo "github.com/StephanHCB/go-backend-service-common/acorns/repository" + "gopkg.in/yaml.v3" + "strings" ) type Impl struct { @@ -35,8 +41,63 @@ func (s *Impl) IsPRValidator() bool { return true } -func (s *Impl) ValidatePullRequest(id uint64, toRef string, fromRef string) error { - // TODO +func (s *Impl) ValidatePullRequest(ctx context.Context, id uint64, toRef string, fromRef string) error { + fileInfos, prHead, err := s.BitBucket.GetChangedFilesOnPullRequest(ctx, int(id)) + if err != nil { + return fmt.Errorf("error getting changed files on pull request: %v", err) + } + + var errorMessages []string + for _, fileInfo := range fileInfos { + err := s.validateYamlFile(ctx, fileInfo.Path, fileInfo.Contents) + if err != nil { + errorMessages = append(errorMessages, err.Error()) + } + } + + buildUrl := s.CustomConfiguration.PullRequestBuildUrl() + buildKey := s.CustomConfiguration.PullRequestBuildKey() + message := "all changed files are valid\n" + if len(errorMessages) > 0 { + message = "failed to validate changed files. Please fix yaml syntax and/or remove unknown fields:\n\n" + + strings.Join(errorMessages, "\n") + "\n" + } + err = s.BitBucket.CreatePullRequestComment(ctx, int(id), message) + if err != nil { + return fmt.Errorf("error creating pull request comment: %v", err) + } + err = s.BitBucket.AddCommitBuildStatus(ctx, prHead, buildUrl, buildKey, len(errorMessages) == 0) + if err != nil { + return fmt.Errorf("error adding commit build status: %v", err) + } return nil } + +func (s *Impl) validateYamlFile(ctx context.Context, path string, contents string) error { + if strings.HasPrefix(path, "owners/") && strings.HasSuffix(path, ".yaml") { + if strings.Contains(path, "owner.info.yaml") { + return parseStrict(ctx, path, contents, &openapi.OwnerDto{}) + } else if strings.Contains(path, "/services/") { + return parseStrict(ctx, path, contents, &openapi.ServiceDto{}) + } else if strings.Contains(path, "/repositories/") { + return parseStrict(ctx, path, contents, &openapi.RepositoryDto{}) + } else { + aulogging.Logger.Ctx(ctx).Info().Printf("ignoring changed file %s in pull request (neither owner info, nor service nor repository)", path) + return nil + } + } else { + aulogging.Logger.Ctx(ctx).Info().Printf("ignoring changed file %s in pull request (not in owners/ or not .yaml)", path) + return nil + } +} + +func parseStrict[T openapi.OwnerDto | openapi.ServiceDto | openapi.RepositoryDto](_ context.Context, path string, contents string, resultPtr *T) error { + decoder := yaml.NewDecoder(strings.NewReader(contents)) + decoder.KnownFields(true) + err := decoder.Decode(resultPtr) + if err != nil { + return fmt.Errorf("failed to parse %s as yaml from metadata: %s", path, err.Error()) + } + return nil +} diff --git a/internal/web/controller/webhookctl/webhookctl.go b/internal/web/controller/webhookctl/webhookctl.go index 6db4912..8a1772d 100644 --- a/internal/web/controller/webhookctl/webhookctl.go +++ b/internal/web/controller/webhookctl/webhookctl.go @@ -119,7 +119,7 @@ func (c *Impl) validatePullRequest(ctx context.Context, operation string, parsed } aulogging.Logger.Ctx(ctx).Info().Printf("got pull request %s (%s)", operation, description) - err := c.PRValidator.ValidatePullRequest(pullRequestPayload.ID, pullRequestPayload.ToRef.ID, pullRequestPayload.FromRef.ID) + err := c.PRValidator.ValidatePullRequest(ctx, pullRequestPayload.ID, pullRequestPayload.ToRef.ID, pullRequestPayload.FromRef.ID) if err != nil { aulogging.Logger.Ctx(ctx).Error().WithErr(err).Printf("error while processing bitbucket webhook: pull request %s (%s): %s", operation, description, err.Error()) return From 72ac7326dfb02a8ca1ec0f346572a07751b2b0b0 Mon Sep 17 00:00:00 2001 From: StephanHCB Date: Mon, 10 Jun 2024 16:35:55 +0200 Subject: [PATCH 6/6] feat(#292): small fixes --- internal/acorn/errors/httperror/error.go | 1 + .../repository/bitbucket/bbclient/bbclient.go | 15 +++------ .../bitbucket/bbclientint/bbclientint.go | 3 +- internal/repository/bitbucket/repository.go | 21 ++++++------ internal/service/prvalidator/prvalidator.go | 6 ++-- .../web/controller/webhookctl/webhookctl.go | 33 +++++++++++-------- internal/web/server/server.go | 1 + test/mock/bbclientmock/bbclientmock.go | 9 ++--- 8 files changed, 41 insertions(+), 48 deletions(-) diff --git a/internal/acorn/errors/httperror/error.go b/internal/acorn/errors/httperror/error.go index 9160722..d4efae0 100644 --- a/internal/acorn/errors/httperror/error.go +++ b/internal/acorn/errors/httperror/error.go @@ -8,6 +8,7 @@ import ( type Error interface { Ctx() context.Context IsHttpError() bool + Status() int } // this also implements the error interface diff --git a/internal/repository/bitbucket/bbclient/bbclient.go b/internal/repository/bitbucket/bbclient/bbclient.go index 69908df..6d77974 100644 --- a/internal/repository/bitbucket/bbclient/bbclient.go +++ b/internal/repository/bitbucket/bbclient/bbclient.go @@ -140,10 +140,10 @@ func (c *Impl) betweenFailureAndRetry() aurestclientapi.BeforeRetryCallback { func (c *Impl) call(ctx context.Context, method string, requestUrlExtension string, requestBody interface{}, responseBodyPointer interface{}) error { remoteUrl := fmt.Sprintf("%s/%s", c.apiBaseUrl, requestUrlExtension) - response := &aurestclientapi.ParsedResponse{ + response := aurestclientapi.ParsedResponse{ Body: responseBodyPointer, } - err := c.Client.Perform(ctx, method, remoteUrl, requestBody, response) + err := c.Client.Perform(ctx, method, remoteUrl, requestBody, &response) if err != nil { return err } @@ -235,21 +235,14 @@ func (c *Impl) GetFileContentsAt(ctx context.Context, projectKey string, reposit return contents.String(), nil } -func (c *Impl) AddProjectRepositoryCommitBuildStatus(ctx context.Context, projectKey string, repositorySlug string, commitId string, commitBuildStatusRequest bbclientint.CommitBuildStatusRequest) (aurestclientapi.ParsedResponse, error) { +func (c *Impl) AddProjectRepositoryCommitBuildStatus(ctx context.Context, projectKey string, repositorySlug string, commitId string, commitBuildStatusRequest bbclientint.CommitBuildStatusRequest) error { urlExt := fmt.Sprintf("%s/projects/%s/repos/%s/commits/%s/builds", bbclientint.CoreApi, url.PathEscape(projectKey), url.PathEscape(repositorySlug), url.PathEscape(commitId)) - emptyResponse := make([]byte, 0) - responseBodyPointer := &emptyResponse - response := aurestclientapi.ParsedResponse{ - Body: &responseBodyPointer, - } - - err := c.call(ctx, http.MethodPost, urlExt, commitBuildStatusRequest, &response) - return response, err + return c.call(ctx, http.MethodPost, urlExt, commitBuildStatusRequest, nil) } func (c *Impl) CreatePullRequestComment(ctx context.Context, projectKey string, repositorySlug string, pullRequestId int64, pullRequestCommentRequest bbclientint.PullRequestCommentRequest) (bbclientint.PullRequestComment, error) { diff --git a/internal/repository/bitbucket/bbclientint/bbclientint.go b/internal/repository/bitbucket/bbclientint/bbclientint.go index feee23a..4619f23 100644 --- a/internal/repository/bitbucket/bbclientint/bbclientint.go +++ b/internal/repository/bitbucket/bbclientint/bbclientint.go @@ -3,7 +3,6 @@ package bbclientint import ( "context" "github.com/Interhyp/metadata-service/internal/acorn/repository" - aurestclientapi "github.com/StephanHCB/go-autumn-restclient/api" ) type BitbucketClient interface { @@ -15,7 +14,7 @@ type BitbucketClient interface { GetChanges(ctx context.Context, projectKey string, repositorySlug string, sinceHash string, untilHash string) (Changes, error) GetFileContentsAt(ctx context.Context, projectKey string, repositorySlug string, atHash string, path string) (string, error) - AddProjectRepositoryCommitBuildStatus(ctx context.Context, projectKey string, repositorySlug string, commitId string, commitBuildStatusRequest CommitBuildStatusRequest) (aurestclientapi.ParsedResponse, error) + AddProjectRepositoryCommitBuildStatus(ctx context.Context, projectKey string, repositorySlug string, commitId string, commitBuildStatusRequest CommitBuildStatusRequest) error CreatePullRequestComment(ctx context.Context, projectKey string, repositorySlug string, pullRequestId int64, pullRequestCommentRequest PullRequestCommentRequest) (PullRequestComment, error) } diff --git a/internal/repository/bitbucket/repository.go b/internal/repository/bitbucket/repository.go index 210064a..39f6b0e 100644 --- a/internal/repository/bitbucket/repository.go +++ b/internal/repository/bitbucket/repository.go @@ -136,8 +136,14 @@ func (r *Impl) GetChangedFilesOnPullRequest(ctx context.Context, pullRequestId i for _, change := range changes.Values { contents, err := r.LowLevel.GetFileContentsAt(ctx, project, slug, prSourceHead, change.Path.ToString) if err != nil { - aulogging.Logger.Ctx(ctx).Info().Printf("failed to retrieve change for %s: %s", change.Path.ToString, err.Error()) - return nil, prSourceHead, err + asHttpError, ok := err.(httperror.Error) + if ok && asHttpError.Status() == http.StatusNotFound { + aulogging.Logger.Ctx(ctx).Debug().Printf("path %s not present on PR head - skipping and continuing", change.Path.ToString) + continue // expected situation - happens for deleted files, or for files added on mainline after fork (which show up in changes) + } else { + aulogging.Logger.Ctx(ctx).Info().Printf("failed to retrieve change for %s: %s", change.Path.ToString, err.Error()) + return nil, prSourceHead, err + } } result = append(result, repository.File{ @@ -146,7 +152,7 @@ func (r *Impl) GetChangedFilesOnPullRequest(ctx context.Context, pullRequestId i }) } - aulogging.Logger.Ctx(ctx).Info().Printf("successfully obtained changes for pull request %d", pullRequestId) + aulogging.Logger.Ctx(ctx).Info().Printf("successfully obtained %d changes for pull request %d", len(result), pullRequestId) return result, prSourceHead, nil } @@ -165,14 +171,7 @@ func (r *Impl) AddCommitBuildStatus(ctx context.Context, commitHash string, url Url: url, } - response, err := r.LowLevel.AddProjectRepositoryCommitBuildStatus(ctx, project, slug, commitHash, request) - if err != nil { - return err - } - if response.Status != http.StatusNoContent { - return fmt.Errorf("could not add build status to commit: %d", response.Status) - } - return nil + return r.LowLevel.AddProjectRepositoryCommitBuildStatus(ctx, project, slug, commitHash, request) } func (r *Impl) CreatePullRequestComment(ctx context.Context, pullRequestId int, comment string) error { diff --git a/internal/service/prvalidator/prvalidator.go b/internal/service/prvalidator/prvalidator.go index 697f7b3..cebdd02 100644 --- a/internal/service/prvalidator/prvalidator.go +++ b/internal/service/prvalidator/prvalidator.go @@ -59,8 +59,8 @@ func (s *Impl) ValidatePullRequest(ctx context.Context, id uint64, toRef string, buildKey := s.CustomConfiguration.PullRequestBuildKey() message := "all changed files are valid\n" if len(errorMessages) > 0 { - message = "failed to validate changed files. Please fix yaml syntax and/or remove unknown fields:\n\n" + - strings.Join(errorMessages, "\n") + "\n" + message = "# yaml validation failure\n\nThere were validation errors in changed files. Please fix yaml syntax and/or remove unknown fields:\n\n" + + strings.Join(errorMessages, "\n\n") + "\n" } err = s.BitBucket.CreatePullRequestComment(ctx, int(id), message) if err != nil { @@ -97,7 +97,7 @@ func parseStrict[T openapi.OwnerDto | openapi.ServiceDto | openapi.RepositoryDto decoder.KnownFields(true) err := decoder.Decode(resultPtr) if err != nil { - return fmt.Errorf("failed to parse %s as yaml from metadata: %s", path, err.Error()) + return fmt.Errorf(" - failed to parse `%s`:\n %s", path, strings.ReplaceAll(err.Error(), "\n", "\n ")) } return nil } diff --git a/internal/web/controller/webhookctl/webhookctl.go b/internal/web/controller/webhookctl/webhookctl.go index 8a1772d..d2daf16 100644 --- a/internal/web/controller/webhookctl/webhookctl.go +++ b/internal/web/controller/webhookctl/webhookctl.go @@ -66,24 +66,26 @@ func (c *Impl) Webhook(w http.ResponseWriter, r *http.Request) { func (c *Impl) WebhookBitBucket(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + aulogging.Logger.Ctx(ctx).Info().Printf("received webhook from BitBucket") + webhook, err := bitbucketserver.New() // we don't need signature checking here + if err != nil { + aulogging.Logger.Ctx(ctx).Info().WithErr(err).Printf("unexpected error while instantiating bitbucket webhook parser - ignoring webhook") + util.UnexpectedErrorHandler(ctx, w, r, err, c.Timestamp.Now()) + return + } + + eventPayload, err := webhook.Parse(r, bitbucketserver.DiagnosticsPingEvent, bitbucketserver.PullRequestOpenedEvent, + bitbucketserver.RepositoryReferenceChangedEvent, bitbucketserver.PullRequestModifiedEvent, bitbucketserver.PullRequestFromReferenceUpdatedEvent) + if err != nil { + aulogging.Logger.Ctx(ctx).Info().WithErr(err).Printf("bad request error while parsing bitbucket webhook payload - ignoring webhook") + util.ErrorHandler(ctx, w, r, "webhook.payload.invalid", http.StatusBadRequest, "parse payload error", c.Timestamp.Now()) + return + } + routineCtx, routineCtxCancel := contexthelper.AsyncCopyRequestContext(ctx, "webhookBitbucket", "backgroundJob") go func() { defer routineCtxCancel() - aulogging.Logger.Ctx(routineCtx).Info().Printf("received webhook from BitBucket") - webhook, err := bitbucketserver.New() // we don't need signature checking here - if err != nil { - aulogging.Logger.Ctx(routineCtx).Error().WithErr(err).Printf("unexpected error while instantiating bitbucket webhook parser - ignoring webhook") - return - } - - eventPayload, err := webhook.Parse(r, bitbucketserver.DiagnosticsPingEvent, bitbucketserver.PullRequestOpenedEvent, - bitbucketserver.RepositoryReferenceChangedEvent, bitbucketserver.PullRequestModifiedEvent) - if err != nil { - aulogging.Logger.Ctx(routineCtx).Error().WithErr(err).Printf("bad request error while parsing bitbucket webhook payload - ignoring webhook") - return - } - switch eventPayload.(type) { case bitbucketserver.PullRequestOpenedPayload: payload, ok := eventPayload.(bitbucketserver.PullRequestOpenedPayload) @@ -91,6 +93,9 @@ func (c *Impl) WebhookBitBucket(w http.ResponseWriter, r *http.Request) { case bitbucketserver.PullRequestModifiedPayload: payload, ok := eventPayload.(bitbucketserver.PullRequestModifiedPayload) c.validatePullRequest(routineCtx, "modified", ok, payload.PullRequest) + case bitbucketserver.PullRequestFromReferenceUpdatedPayload: + payload, ok := eventPayload.(bitbucketserver.PullRequestFromReferenceUpdatedPayload) + c.validatePullRequest(routineCtx, "from_reference", ok, payload.PullRequest) case bitbucketserver.RepositoryReferenceChangedPayload: payload, ok := eventPayload.(bitbucketserver.RepositoryReferenceChangedPayload) if !ok || len(payload.Changes) < 1 || payload.Changes[0].ReferenceID == "" { diff --git a/internal/web/server/server.go b/internal/web/server/server.go index 2e7cce6..ebc3f04 100644 --- a/internal/web/server/server.go +++ b/internal/web/server/server.go @@ -117,6 +117,7 @@ func (s *Impl) WireUp(ctx context.Context) { "GET /rest/api/v1/services.*", "GET /rest/api/v1/repositories.*", "POST /webhook", + "POST /webhook/bitbucket", // health (provides just up) "GET /", "GET /health", diff --git a/test/mock/bbclientmock/bbclientmock.go b/test/mock/bbclientmock/bbclientmock.go index 09081fd..381db61 100644 --- a/test/mock/bbclientmock/bbclientmock.go +++ b/test/mock/bbclientmock/bbclientmock.go @@ -6,8 +6,6 @@ import ( "github.com/Interhyp/metadata-service/internal/acorn/errors/httperror" "github.com/Interhyp/metadata-service/internal/acorn/repository" "github.com/Interhyp/metadata-service/internal/repository/bitbucket/bbclientint" - aurestclientapi "github.com/StephanHCB/go-autumn-restclient/api" - "net/http" "strings" ) @@ -56,11 +54,8 @@ func (c *BitbucketClientMock) GetFileContentsAt(ctx context.Context, projectKey return "", nil } -func (c *BitbucketClientMock) AddProjectRepositoryCommitBuildStatus(ctx context.Context, projectKey string, repositorySlug string, commitId string, commitBuildStatusRequest bbclientint.CommitBuildStatusRequest) (aurestclientapi.ParsedResponse, error) { - response := aurestclientapi.ParsedResponse{ - Status: http.StatusCreated, - } - return response, nil +func (c *BitbucketClientMock) AddProjectRepositoryCommitBuildStatus(ctx context.Context, projectKey string, repositorySlug string, commitId string, commitBuildStatusRequest bbclientint.CommitBuildStatusRequest) error { + return nil } func (c *BitbucketClientMock) CreatePullRequestComment(ctx context.Context, projectKey string, repositorySlug string, pullRequestId int64, pullRequestCommentRequest bbclientint.PullRequestCommentRequest) (bbclientint.PullRequestComment, error) {