From 9cc6e88a65e829ddc1b7bf77fcf690bbc066245c Mon Sep 17 00:00:00 2001 From: Wang Yan Date: Sat, 14 Mar 2020 22:34:36 +0800 Subject: [PATCH] add notification middleware (#11072) the notification is for send out the event after DB transaction complete. It's safe to send hook as this middleware is after transaction in the response path. Signed-off-by: wang yan --- src/core/middlewares/middlewares.go | 3 + src/pkg/notification/notification.go | 33 ++++++++++ .../middleware/notification/notification.go | 52 +++++++++++++++ .../notification/notification_test.go | 65 +++++++++++++++++++ 4 files changed, 153 insertions(+) create mode 100644 src/server/middleware/notification/notification.go create mode 100644 src/server/middleware/notification/notification_test.go diff --git a/src/core/middlewares/middlewares.go b/src/core/middlewares/middlewares.go index 4476adf0f..8e461200f 100644 --- a/src/core/middlewares/middlewares.go +++ b/src/core/middlewares/middlewares.go @@ -16,6 +16,7 @@ package middlewares import ( "github.com/goharbor/harbor/src/server/middleware/csrf" + "github.com/goharbor/harbor/src/server/middleware/notification" "github.com/goharbor/harbor/src/server/middleware/readonly" "net/http" "path" @@ -76,6 +77,8 @@ func MiddleWares() []beego.MiddleWare { requestid.Middleware(), readonly.Middleware(readonlySkippers...), orm.Middleware(legacyAPISkipper), + // notification must ahead of transaction ensure the DB transaction execution complete + notification.Middleware(), transaction.Middleware(legacyAPISkipper, fetchBlobAPISkipper), } } diff --git a/src/pkg/notification/notification.go b/src/pkg/notification/notification.go index cc645f345..342c87075 100755 --- a/src/pkg/notification/notification.go +++ b/src/pkg/notification/notification.go @@ -1,12 +1,15 @@ package notification import ( + "container/list" + "context" "github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/pkg/notification/hook" "github.com/goharbor/harbor/src/pkg/notification/job" jobMgr "github.com/goharbor/harbor/src/pkg/notification/job/manager" "github.com/goharbor/harbor/src/pkg/notification/policy" "github.com/goharbor/harbor/src/pkg/notification/policy/manager" + n_event "github.com/goharbor/harbor/src/pkg/notifier/event" "github.com/goharbor/harbor/src/pkg/notifier/model" ) @@ -61,3 +64,33 @@ func initSupportedNotifyType(notifyTypes ...string) { SupportedNotifyTypes[notifyType] = struct{}{} } } + +type eventKey struct{} + +// EventCtx ... +type EventCtx struct { + Events *list.List + MustNotify bool +} + +// NewContext returns new context with event +func NewContext(ctx context.Context, ec *EventCtx) context.Context { + if ctx == nil { + ctx = context.Background() + } + return context.WithValue(ctx, eventKey{}, ec) +} + +// AddEvent add events into request context, the event will be sent by the notification middleware eventually. +func AddEvent(ctx context.Context, m n_event.Metadata, notify ...bool) { + e, ok := ctx.Value(eventKey{}).(*EventCtx) + if !ok { + log.Debug("request has not event list, cannot add event into context") + return + } + if len(notify) != 0 { + e.MustNotify = notify[0] + } + e.Events.PushBack(m) + return +} diff --git a/src/server/middleware/notification/notification.go b/src/server/middleware/notification/notification.go new file mode 100644 index 000000000..53d08b9fa --- /dev/null +++ b/src/server/middleware/notification/notification.go @@ -0,0 +1,52 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package notification + +import ( + "container/list" + "github.com/goharbor/harbor/src/pkg/notification" + "github.com/goharbor/harbor/src/server/middleware" + "net/http" + + "github.com/goharbor/harbor/src/internal" + evt "github.com/goharbor/harbor/src/pkg/notifier/event" +) + +// publishEvent publishes the events in the context, it ensures publish happens after transaction success. +func publishEvent(es *list.List) { + if es == nil { + return + } + for e := es.Front(); e != nil; e = e.Next() { + evt.BuildAndPublish(e.Value.(evt.Metadata)) + } + return +} + +// Middleware sends the notification after transaction success +func Middleware(skippers ...middleware.Skipper) func(http.Handler) http.Handler { + return middleware.New(func(w http.ResponseWriter, r *http.Request, next http.Handler) { + res := internal.NewResponseRecorder(w) + eveCtx := ¬ification.EventCtx{ + Events: list.New(), + MustNotify: false, + } + ctx := notification.NewContext(r.Context(), eveCtx) + next.ServeHTTP(res, r.WithContext(ctx)) + if res.Success() || eveCtx.MustNotify { + publishEvent(eveCtx.Events) + } + }, skippers...) +} diff --git a/src/server/middleware/notification/notification_test.go b/src/server/middleware/notification/notification_test.go new file mode 100644 index 000000000..ee07931d6 --- /dev/null +++ b/src/server/middleware/notification/notification_test.go @@ -0,0 +1,65 @@ +package notification + +import ( + "context" + "fmt" + "github.com/goharbor/harbor/src/api/event" + pkg_art "github.com/goharbor/harbor/src/pkg/artifact" + "github.com/goharbor/harbor/src/pkg/notification" + "github.com/stretchr/testify/suite" + "net/http" + "net/http/httptest" + "testing" +) + +type NotificatoinMiddlewareTestSuite struct { + suite.Suite +} + +func (suite *NotificatoinMiddlewareTestSuite) TestMiddleware() { + next := func() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusAccepted) + notification.AddEvent(r.Context(), &event.DeleteArtifactEventMetadata{ + Ctx: context.Background(), + Artifact: &pkg_art.Artifact{ + ProjectID: 1, + RepositoryID: 2, + RepositoryName: "library/hello-world", + }, + Tags: []string{"latest"}, + }) + }) + } + path := fmt.Sprintf("/v2/library/photon/manifests/latest") + req := httptest.NewRequest(http.MethodPatch, path, nil) + res := httptest.NewRecorder() + Middleware()(next()).ServeHTTP(res, req) + suite.Equal(http.StatusAccepted, res.Code) +} + +func (suite *NotificatoinMiddlewareTestSuite) TestMiddlewareMustNotify() { + next := func() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + notification.AddEvent(r.Context(), &event.DeleteArtifactEventMetadata{ + Ctx: context.Background(), + Artifact: &pkg_art.Artifact{ + ProjectID: 1, + RepositoryID: 2, + RepositoryName: "library/hello-world", + }, + Tags: []string{"latest"}, + }, true) + }) + } + path := fmt.Sprintf("/v2/library/photon/manifests/latest") + req := httptest.NewRequest(http.MethodPatch, path, nil) + res := httptest.NewRecorder() + Middleware()(next()).ServeHTTP(res, req) + suite.Equal(http.StatusInternalServerError, res.Code) +} + +func TestNotificatoinMiddlewareTestSuite(t *testing.T) { + suite.Run(t, &NotificatoinMiddlewareTestSuite{}) +}