diff --git a/src/api/artifact/controller.go b/src/api/artifact/controller.go index a7a075c6f7..d68b7e9849 100644 --- a/src/api/artifact/controller.go +++ b/src/api/artifact/controller.go @@ -20,7 +20,7 @@ import ( "errors" "fmt" "github.com/goharbor/harbor/src/api/artifact/processor" - "github.com/goharbor/harbor/src/api/event" + "github.com/goharbor/harbor/src/api/event/metadata" "github.com/goharbor/harbor/src/api/tag" "github.com/goharbor/harbor/src/internal" "github.com/goharbor/harbor/src/internal/orm" @@ -30,7 +30,7 @@ import ( "github.com/goharbor/harbor/src/pkg/immutabletag/match" "github.com/goharbor/harbor/src/pkg/immutabletag/match/rule" "github.com/goharbor/harbor/src/pkg/label" - evt "github.com/goharbor/harbor/src/pkg/notifier/event" + "github.com/goharbor/harbor/src/pkg/notification" "github.com/goharbor/harbor/src/pkg/registry" "github.com/goharbor/harbor/src/pkg/signature" "github.com/opencontainers/go-digest" @@ -142,14 +142,14 @@ func (c *controller) Ensure(ctx context.Context, repository, digest string, tags } } // fire event - e := &event.PushArtifactEventMetadata{ + e := &metadata.PushArtifactEventMetadata{ Ctx: ctx, Artifact: artifact, } if len(tags) > 0 { e.Tag = tags[0] } - evt.BuildAndPublish(e) + notification.AddEvent(ctx, e) return created, artifact.ID, nil } @@ -380,7 +380,7 @@ func (c *controller) deleteDeeply(ctx context.Context, id int64, isRoot bool) er for _, tag := range art.Tags { tags = append(tags, tag.Name) } - evt.BuildAndPublish(&event.DeleteArtifactEventMetadata{ + notification.AddEvent(ctx, &metadata.DeleteArtifactEventMetadata{ Ctx: ctx, Artifact: &art.Artifact, Tags: tags, diff --git a/src/api/event/handler/auditlog/auditlog.go b/src/api/event/handler/auditlog/auditlog.go new file mode 100644 index 0000000000..01437916e8 --- /dev/null +++ b/src/api/event/handler/auditlog/auditlog.go @@ -0,0 +1,66 @@ +// 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 auditlog + +import ( + "context" + beegorm "github.com/astaxie/beego/orm" + "github.com/goharbor/harbor/src/api/event" + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/internal/orm" + "github.com/goharbor/harbor/src/pkg/audit" + am "github.com/goharbor/harbor/src/pkg/audit/model" +) + +// Handler - audit log handler +type Handler struct { +} + +// AuditResolver - interface to resolve to AuditLog +type AuditResolver interface { + ResolveToAuditLog() (*am.AuditLog, error) +} + +// Handle ... +func (h *Handler) Handle(value interface{}) error { + ctx := orm.NewContext(context.Background(), beegorm.NewOrm()) + var auditLog *am.AuditLog + switch v := value.(type) { + case *event.PushArtifactEvent, *event.PullArtifactEvent, *event.DeleteArtifactEvent, + *event.DeleteRepositoryEvent, *event.CreateProjectEvent, *event.DeleteProjectEvent, + *event.DeleteTagEvent, *event.CreateTagEvent: + resolver := value.(AuditResolver) + al, err := resolver.ResolveToAuditLog() + if err != nil { + log.Errorf("failed to handler event %v", err) + return err + } + auditLog = al + default: + log.Errorf("Can not handler this event type! %#v", v) + } + if auditLog != nil { + _, err := audit.Mgr.Create(ctx, auditLog) + if err != nil { + log.Debugf("add audit log err: %v", err) + } + } + return nil +} + +// IsStateful ... +func (h *Handler) IsStateful() bool { + return false +} diff --git a/src/pkg/notifier/handler/auditlog/handler_test.go b/src/api/event/handler/auditlog/auditlog_test.go similarity index 68% rename from src/pkg/notifier/handler/auditlog/handler_test.go rename to src/api/event/handler/auditlog/auditlog_test.go index 3c386f9cff..c0faae3ced 100644 --- a/src/pkg/notifier/handler/auditlog/handler_test.go +++ b/src/api/event/handler/auditlog/auditlog_test.go @@ -1,20 +1,33 @@ +// 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 auditlog import ( "context" + "github.com/goharbor/harbor/src/api/event/metadata" + "github.com/goharbor/harbor/src/api/event" common_dao "github.com/goharbor/harbor/src/common/dao" - "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/pkg/audit/model" "github.com/goharbor/harbor/src/pkg/notifier" - "github.com/goharbor/harbor/src/pkg/notifier/event" - nm "github.com/goharbor/harbor/src/pkg/notifier/model" + ne "github.com/goharbor/harbor/src/pkg/notifier/event" "github.com/goharbor/harbor/src/pkg/q" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" "testing" - "time" ) type MockAuditLogManager struct { @@ -55,7 +68,7 @@ type AuditLogHandlerTestSuite struct { func (suite *AuditLogHandlerTestSuite) SetupSuite() { common_dao.PrepareTestForPostgresSQL() suite.logMgr = &MockAuditLogManager{} - suite.auditLogHandler = &Handler{AuditLogMgr: suite.logMgr} + suite.auditLogHandler = &Handler{} log.SetLevel(log.DebugLevel) } @@ -66,26 +79,13 @@ func (suite *AuditLogHandlerTestSuite) TestSubscribeTagEvent() { // sample code to use the event framework. - notifier.Subscribe(nm.PushTagTopic, suite.auditLogHandler) + notifier.Subscribe(event.TopicCreateProject, suite.auditLogHandler) // event data should implement the interface TopicEvent - data := &nm.TagEvent{ - TargetTopic: nm.PushTagTopic, // Topic is a attribute of event - Project: &models.Project{ - ProjectID: 1, - Name: "library", - }, - RepoName: "busybox", - Digest: "abcdef", - TagName: "dev", - OccurAt: time.Now(), + ne.BuildAndPublish(&metadata.CreateProjectEventMetadata{ + ProjectID: 1, + Project: "test", Operator: "admin", - Operation: "push", // Use Operation instead of event type. - } - // No EventMetadata anymore and there is no need to call resolve - // The handler receives the TagEvent - // The handler should use switch type interface to get TagEvent - event.New().WithTopicEvent(data).Publish() - + }) cnt, err := suite.logMgr.Count(nil, nil) suite.Nil(err) diff --git a/src/api/event/handler/init.go b/src/api/event/handler/init.go new file mode 100644 index 0000000000..6d7924a4e7 --- /dev/null +++ b/src/api/event/handler/init.go @@ -0,0 +1,42 @@ +package handler + +import ( + "github.com/goharbor/harbor/src/api/event" + "github.com/goharbor/harbor/src/api/event/handler/auditlog" + "github.com/goharbor/harbor/src/api/event/handler/replication" + "github.com/goharbor/harbor/src/api/event/handler/webhook/artifact" + "github.com/goharbor/harbor/src/api/event/handler/webhook/chart" + "github.com/goharbor/harbor/src/api/event/handler/webhook/quota" + "github.com/goharbor/harbor/src/api/event/handler/webhook/scan" + "github.com/goharbor/harbor/src/pkg/notifier" +) + +func init() { + // notification + notifier.Subscribe(event.TopicPushArtifact, &artifact.Handler{}) + notifier.Subscribe(event.TopicPullArtifact, &artifact.Handler{}) + notifier.Subscribe(event.TopicDeleteArtifact, &artifact.Handler{}) + notifier.Subscribe(event.TopicUploadChart, &chart.Handler{}) + notifier.Subscribe(event.TopicDeleteChart, &chart.Handler{}) + notifier.Subscribe(event.TopicDownloadChart, &chart.Handler{}) + notifier.Subscribe(event.TopicQuotaExceed, "a.Handler{}) + notifier.Subscribe(event.TopicQuotaWarning, "a.Handler{}) + notifier.Subscribe(event.TopicScanningFailed, &scan.Handler{}) + notifier.Subscribe(event.TopicScanningCompleted, &scan.Handler{}) + notifier.Subscribe(event.TopicDeleteArtifact, &scan.DelArtHandler{}) + + // replication + notifier.Subscribe(event.TopicPushArtifact, &replication.Handler{}) + notifier.Subscribe(event.TopicDeleteArtifact, &replication.Handler{}) + notifier.Subscribe(event.TopicCreateTag, &replication.Handler{}) + + // audit logs + notifier.Subscribe(event.TopicPushArtifact, &auditlog.Handler{}) + notifier.Subscribe(event.TopicPullArtifact, &auditlog.Handler{}) + notifier.Subscribe(event.TopicDeleteArtifact, &auditlog.Handler{}) + notifier.Subscribe(event.TopicCreateProject, &auditlog.Handler{}) + notifier.Subscribe(event.TopicDeleteProject, &auditlog.Handler{}) + notifier.Subscribe(event.TopicDeleteRepository, &auditlog.Handler{}) + notifier.Subscribe(event.TopicCreateTag, &auditlog.Handler{}) + notifier.Subscribe(event.TopicDeleteTag, &auditlog.Handler{}) +} diff --git a/src/api/event/handler/replication.go b/src/api/event/handler/replication/replication.go similarity index 78% rename from src/api/event/handler/replication.go rename to src/api/event/handler/replication/replication.go index 5bd96898bf..d2a1ca51b0 100644 --- a/src/api/event/handler/replication.go +++ b/src/api/event/handler/replication/replication.go @@ -1,3 +1,4 @@ +// 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. @@ -11,12 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -package handler +package replication import ( "github.com/goharbor/harbor/src/api/event" "github.com/goharbor/harbor/src/common/utils/log" - "github.com/goharbor/harbor/src/pkg/notifier" "github.com/goharbor/harbor/src/pkg/project" "github.com/goharbor/harbor/src/replication" repevent "github.com/goharbor/harbor/src/replication/event" @@ -24,20 +24,12 @@ import ( "strconv" ) -func init() { - handler := &replicationHandler{ - proMgr: project.Mgr, - } - notifier.Subscribe(event.TopicPushArtifact, handler) - notifier.Subscribe(event.TopicDeleteArtifact, handler) - notifier.Subscribe(event.TopicCreateTag, handler) +// Handler ... +type Handler struct { } -type replicationHandler struct { - proMgr project.Manager -} - -func (r *replicationHandler) Handle(value interface{}) error { +// Handle ... +func (r *Handler) Handle(value interface{}) error { pushArtEvent, ok := value.(*event.PushArtifactEvent) if ok { return r.handlePushArtifact(pushArtEvent) @@ -53,16 +45,17 @@ func (r *replicationHandler) Handle(value interface{}) error { return nil } -func (r *replicationHandler) IsStateful() bool { +// IsStateful ... +func (r *Handler) IsStateful() bool { return false } // TODO handle create tag -func (r *replicationHandler) handlePushArtifact(event *event.PushArtifactEvent) error { +func (r *Handler) handlePushArtifact(event *event.PushArtifactEvent) error { art := event.Artifact public := false - project, err := r.proMgr.Get(art.ProjectID) + project, err := project.Mgr.Get(art.ProjectID) if err == nil && project != nil { public = project.IsPublic() } else { @@ -84,7 +77,7 @@ func (r *replicationHandler) handlePushArtifact(event *event.PushArtifactEvent) { Type: art.Type, Digest: art.Digest, - Tags: []string{event.Tag}, + Tags: event.Tags, }}, }, }, @@ -92,7 +85,7 @@ func (r *replicationHandler) handlePushArtifact(event *event.PushArtifactEvent) return replication.EventHandler.Handle(e) } -func (r *replicationHandler) handleDeleteArtifact(event *event.DeleteArtifactEvent) error { +func (r *Handler) handleDeleteArtifact(event *event.DeleteArtifactEvent) error { art := event.Artifact e := &repevent.Event{ Type: repevent.EventTypeArtifactDelete, @@ -115,10 +108,10 @@ func (r *replicationHandler) handleDeleteArtifact(event *event.DeleteArtifactEve return replication.EventHandler.Handle(e) } -func (r *replicationHandler) handleCreateTag(event *event.CreateTagEvent) error { +func (r *Handler) handleCreateTag(event *event.CreateTagEvent) error { art := event.AttachedArtifact public := false - project, err := r.proMgr.Get(art.ProjectID) + project, err := project.Mgr.Get(art.ProjectID) if err == nil && project != nil { public = project.IsPublic() } else { diff --git a/src/api/event/handler/util/util.go b/src/api/event/handler/util/util.go new file mode 100644 index 0000000000..1adc47dc68 --- /dev/null +++ b/src/api/event/handler/util/util.go @@ -0,0 +1,66 @@ +package util + +import ( + "errors" + "fmt" + "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/core/config" + "github.com/goharbor/harbor/src/pkg/notifier/event" + notifyModel "github.com/goharbor/harbor/src/pkg/notifier/model" + "strings" +) + +// SendHookWithPolicies send hook by publishing topic of specified target type(notify type) +func SendHookWithPolicies(policies []*models.NotificationPolicy, payload *notifyModel.Payload, eventType string) error { + // if global notification configured disabled, return directly + if !config.NotificationEnable() { + log.Debug("notification feature is not enabled") + return nil + } + + errRet := false + for _, ply := range policies { + targets := ply.Targets + for _, target := range targets { + evt := &event.Event{} + hookMetadata := &event.HookMetaData{ + EventType: eventType, + PolicyID: ply.ID, + Payload: payload, + Target: &target, + } + // It should never affect evaluating other policies when one is failed, but error should return + if err := evt.Build(hookMetadata); err == nil { + if err := evt.Publish(); err != nil { + errRet = true + log.Errorf("failed to publish hook notify event: %v", err) + } + } else { + errRet = true + log.Errorf("failed to build hook notify event metadata: %v", err) + } + log.Debugf("published image event %s by topic %s", payload.Type, target.Type) + } + } + if errRet { + return errors.New("failed to trigger some of the events") + } + return nil +} + +// GetNameFromImgRepoFullName gets image name from repo full name with format `repoName/imageName` +func GetNameFromImgRepoFullName(repo string) string { + idx := strings.Index(repo, "/") + return repo[idx+1:] +} + +// BuildImageResourceURL ... +func BuildImageResourceURL(repoName, tag string) (string, error) { + extURL, err := config.ExtURL() + if err != nil { + return "", fmt.Errorf("get external endpoint failed: %v", err) + } + resURL := fmt.Sprintf("%s/%s:%s", extURL, repoName, tag) + return resURL, nil +} diff --git a/src/api/event/handler/webhook/artifact/artifact.go b/src/api/event/handler/webhook/artifact/artifact.go new file mode 100644 index 0000000000..2ed4b26efd --- /dev/null +++ b/src/api/event/handler/webhook/artifact/artifact.go @@ -0,0 +1,142 @@ +// 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 artifact + +import ( + "context" + "fmt" + beegorm "github.com/astaxie/beego/orm" + "github.com/goharbor/harbor/src/api/event" + "github.com/goharbor/harbor/src/api/event/handler/util" + "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/internal/orm" + "github.com/goharbor/harbor/src/pkg/notification" + "github.com/goharbor/harbor/src/pkg/notifier/model" + notifyModel "github.com/goharbor/harbor/src/pkg/notifier/model" + "github.com/goharbor/harbor/src/pkg/project" + "github.com/goharbor/harbor/src/pkg/repository" +) + +// Handler preprocess artifact event data +type Handler struct { + project *models.Project +} + +// Handle preprocess artifact event data and then publish hook event +func (a *Handler) Handle(value interface{}) error { + switch v := value.(type) { + case *event.PushArtifactEvent: + return a.handle(v.ArtifactEvent) + case *event.PullArtifactEvent: + return a.handle(v.ArtifactEvent) + case *event.DeleteArtifactEvent: + return a.handle(v.ArtifactEvent) + default: + log.Errorf("Can not handler this event type! %#v", v) + } + return nil +} + +// IsStateful ... +func (a *Handler) IsStateful() bool { + return false +} + +func (a *Handler) handle(event *event.ArtifactEvent) error { + var err error + a.project, err = project.Mgr.Get(event.Artifact.ProjectID) + if err != nil { + log.Errorf("failed to get project:%d, error: %v", event.Artifact.ProjectID, err) + return err + } + policies, err := notification.PolicyMgr.GetRelatedPolices(a.project.ProjectID, event.EventType) + if err != nil { + log.Errorf("failed to find policy for %s event: %v", event.EventType, err) + return err + } + if len(policies) == 0 { + log.Debugf("cannot find policy for %s event: %v", event.EventType, event) + return nil + } + + payload, err := a.constructArtifactPayload(event) + if err != nil { + return err + } + + err = util.SendHookWithPolicies(policies, payload, event.EventType) + if err != nil { + return err + } + return nil +} + +func (a *Handler) constructArtifactPayload(event *event.ArtifactEvent) (*model.Payload, error) { + repoName := event.Repository + if repoName == "" { + return nil, fmt.Errorf("invalid %s event with empty repo name", event.EventType) + } + + repoType := models.ProjectPrivate + if a.project.IsPublic() { + repoType = models.ProjectPublic + } + + imageName := util.GetNameFromImgRepoFullName(repoName) + + payload := ¬ifyModel.Payload{ + Type: event.EventType, + OccurAt: event.OccurAt.Unix(), + EventData: ¬ifyModel.EventData{ + Repository: ¬ifyModel.Repository{ + Name: imageName, + Namespace: a.project.Name, + RepoFullName: repoName, + RepoType: repoType, + }, + }, + Operator: event.Operator, + } + + ctx := orm.NewContext(context.Background(), beegorm.NewOrm()) + repoRecord, err := repository.Mgr.GetByName(ctx, repoName) + if err != nil { + log.Errorf("failed to get repository with name %s: %v", repoName, err) + return nil, err + } + payload.EventData.Repository.DateCreated = repoRecord.CreationTime.Unix() + + var reference string + if len(event.Tags) == 0 { + reference = event.Artifact.Digest + } else { + reference = event.Tags[0] + } + resURL, err := util.BuildImageResourceURL(repoName, reference) + if err != nil { + log.Errorf("get resource URL failed: %v", err) + return nil, err + } + + resource := ¬ifyModel.Resource{ + Tag: reference, + Digest: event.Artifact.Digest, + ResourceURL: resURL, + } + payload.EventData.Resources = append(payload.EventData.Resources, resource) + + return payload, nil +} diff --git a/src/pkg/scan/event/init.go b/src/api/event/handler/webhook/artifact/artifact_test.go similarity index 53% rename from src/pkg/scan/event/init.go rename to src/api/event/handler/webhook/artifact/artifact_test.go index c01c026fcf..a76d5c7d61 100644 --- a/src/pkg/scan/event/init.go +++ b/src/api/event/handler/webhook/artifact/artifact_test.go @@ -12,21 +12,4 @@ // See the License for the specific language governing permissions and // limitations under the License. -package event - -import ( - "github.com/goharbor/harbor/src/common/utils/log" - "github.com/goharbor/harbor/src/pkg/notifier" - "github.com/goharbor/harbor/src/pkg/notifier/model" - "github.com/pkg/errors" -) - -// Init the events for scan -func Init() { - log.Debugf("Subscribe topic %s for cascade deletion of scan reports", model.DeleteImageTopic) - - err := notifier.Subscribe(model.DeleteImageTopic, NewOnDelImageHandler()) - if err != nil { - log.Error(errors.Wrap(err, "register on delete image handler: init: scan")) - } -} +package artifact diff --git a/src/pkg/notifier/handler/notification/chart_handler.go b/src/api/event/handler/webhook/chart/chart.go similarity index 68% rename from src/pkg/notifier/handler/notification/chart_handler.go rename to src/api/event/handler/webhook/chart/chart.go index 5cfc815658..09c849bf5d 100644 --- a/src/pkg/notifier/handler/notification/chart_handler.go +++ b/src/api/event/handler/webhook/chart/chart.go @@ -1,28 +1,39 @@ -package notification +// 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 chart import ( "errors" "fmt" + "github.com/goharbor/harbor/src/api/event" + "github.com/goharbor/harbor/src/api/event/handler/util" "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/core/config" "github.com/goharbor/harbor/src/pkg/notification" "github.com/goharbor/harbor/src/pkg/notifier/model" + "github.com/goharbor/harbor/src/pkg/project" ) -// ChartPreprocessHandler preprocess chart event data -type ChartPreprocessHandler struct { +// Handler preprocess chart event data +type Handler struct { } // Handle preprocess chart event data and then publish hook event -func (cph *ChartPreprocessHandler) Handle(value interface{}) error { - // if global notification configured disabled, return directly - if !config.NotificationEnable() { - log.Debug("notification feature is not enabled") - return nil - } - - chartEvent, ok := value.(*model.ChartEvent) +func (cph *Handler) Handle(value interface{}) error { + chartEvent, ok := value.(*event.ChartEvent) if !ok { return errors.New("invalid chart event type") } @@ -31,7 +42,7 @@ func (cph *ChartPreprocessHandler) Handle(value interface{}) error { return fmt.Errorf("data miss in chart event: %v", chartEvent) } - project, err := config.GlobalProjectMgr.Get(chartEvent.ProjectName) + project, err := project.Mgr.Get(chartEvent.ProjectName) if err != nil { log.Errorf("failed to find project[%s] for chart event: %v", chartEvent.ProjectName, err) return err @@ -55,7 +66,7 @@ func (cph *ChartPreprocessHandler) Handle(value interface{}) error { return err } - err = sendHookWithPolicies(policies, payload, chartEvent.EventType) + err = util.SendHookWithPolicies(policies, payload, chartEvent.EventType) if err != nil { return err } @@ -64,11 +75,11 @@ func (cph *ChartPreprocessHandler) Handle(value interface{}) error { } // IsStateful ... -func (cph *ChartPreprocessHandler) IsStateful() bool { +func (cph *Handler) IsStateful() bool { return false } -func constructChartPayload(event *model.ChartEvent, project *models.Project) (*model.Payload, error) { +func constructChartPayload(event *event.ChartEvent, project *models.Project) (*model.Payload, error) { repoType := models.ProjectPrivate if project.IsPublic() { repoType = models.ProjectPublic diff --git a/src/pkg/notifier/handler/notification/chart_handler_test.go b/src/api/event/handler/webhook/chart/chart_test.go similarity index 54% rename from src/pkg/notifier/handler/notification/chart_handler_test.go rename to src/api/event/handler/webhook/chart/chart_test.go index d39cece6b0..d1dd200621 100644 --- a/src/pkg/notifier/handler/notification/chart_handler_test.go +++ b/src/api/event/handler/webhook/chart/chart_test.go @@ -1,76 +1,41 @@ -package notification +// 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 chart import ( + "github.com/goharbor/harbor/src/api/event" + common_dao "github.com/goharbor/harbor/src/common/dao" "testing" "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/core/config" "github.com/goharbor/harbor/src/pkg/notification" - "github.com/goharbor/harbor/src/pkg/notifier/model" + testingnotification "github.com/goharbor/harbor/src/testing/pkg/notification" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -type fakedPolicyMgr struct { -} - -func (f *fakedPolicyMgr) Create(*models.NotificationPolicy) (int64, error) { - return 0, nil -} - -func (f *fakedPolicyMgr) List(id int64) ([]*models.NotificationPolicy, error) { - return nil, nil -} - -func (f *fakedPolicyMgr) Get(id int64) (*models.NotificationPolicy, error) { - return nil, nil -} - -func (f *fakedPolicyMgr) GetByNameAndProjectID(string, int64) (*models.NotificationPolicy, error) { - return nil, nil -} - -func (f *fakedPolicyMgr) Update(*models.NotificationPolicy) error { - return nil -} - -func (f *fakedPolicyMgr) Delete(int64) error { - return nil -} - -func (f *fakedPolicyMgr) Test(*models.NotificationPolicy) error { - return nil -} - -func (f *fakedPolicyMgr) GetRelatedPolices(id int64, eventType string) ([]*models.NotificationPolicy, error) { - return []*models.NotificationPolicy{ - { - ID: 1, - EventTypes: []string{ - model.EventTypeUploadChart, - model.EventTypeDownloadChart, - model.EventTypeDeleteChart, - model.EventTypeScanningCompleted, - model.EventTypeScanningFailed, - }, - Targets: []models.EventTarget{ - { - Type: "http", - Address: "http://127.0.0.1:8080", - }, - }, - }, - }, nil -} - func TestChartPreprocessHandler_Handle(t *testing.T) { + common_dao.PrepareTestForPostgresSQL() PolicyMgr := notification.PolicyMgr defer func() { notification.PolicyMgr = PolicyMgr }() - notification.PolicyMgr = &fakedPolicyMgr{} + notification.PolicyMgr = &testingnotification.FakedPolicyMgr{} - handler := &ChartPreprocessHandler{} + handler := &Handler{} config.Init() name := "project_for_test_chart_event_preprocess" @@ -99,23 +64,23 @@ func TestChartPreprocessHandler_Handle(t *testing.T) { wantErr bool }{ { - name: "ChartPreprocessHandler Want Error 1", + name: "Handler Want Error 1", args: args{ data: nil, }, wantErr: true, }, { - name: "ChartPreprocessHandler Want Error 2", + name: "Handler Want Error 2", args: args{ - data: &model.ChartEvent{}, + data: &event.ChartEvent{}, }, wantErr: true, }, { - name: "ChartPreprocessHandler Want Error 3", + name: "Handler Want Error 3", args: args{ - data: &model.ChartEvent{ + data: &event.ChartEvent{ Versions: []string{ "v1.2.1", }, @@ -125,9 +90,9 @@ func TestChartPreprocessHandler_Handle(t *testing.T) { wantErr: true, }, { - name: "ChartPreprocessHandler Want Error 4", + name: "Handler Want Error 4", args: args{ - data: &model.ChartEvent{ + data: &event.ChartEvent{ Versions: []string{ "v1.2.1", }, @@ -138,9 +103,9 @@ func TestChartPreprocessHandler_Handle(t *testing.T) { wantErr: true, }, { - name: "ChartPreprocessHandler Want Error 5", + name: "Handler Want Error 5", args: args{ - data: &model.ChartEvent{ + data: &event.ChartEvent{ Versions: []string{ "v1.2.1", }, @@ -166,6 +131,6 @@ func TestChartPreprocessHandler_Handle(t *testing.T) { } func TestChartPreprocessHandler_IsStateful(t *testing.T) { - handler := &ChartPreprocessHandler{} + handler := &Handler{} assert.False(t, handler.IsStateful()) } diff --git a/src/pkg/notifier/handler/notification/quota_handler.go b/src/api/event/handler/webhook/quota/quota.go similarity index 61% rename from src/pkg/notifier/handler/notification/quota_handler.go rename to src/api/event/handler/webhook/quota/quota.go index 16f84ca471..eb4242d622 100644 --- a/src/pkg/notifier/handler/notification/quota_handler.go +++ b/src/api/event/handler/webhook/quota/quota.go @@ -1,28 +1,39 @@ -package notification +// 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 quota import ( "errors" "fmt" + "github.com/goharbor/harbor/src/api/event" + "github.com/goharbor/harbor/src/api/event/handler/util" "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/utils/log" - "github.com/goharbor/harbor/src/core/config" "github.com/goharbor/harbor/src/pkg/notification" "github.com/goharbor/harbor/src/pkg/notifier/model" notifyModel "github.com/goharbor/harbor/src/pkg/notifier/model" + "github.com/goharbor/harbor/src/pkg/project" ) -// QuotaPreprocessHandler preprocess image event data -type QuotaPreprocessHandler struct { +// Handler preprocess image event data +type Handler struct { } // Handle ... -func (qp *QuotaPreprocessHandler) Handle(value interface{}) error { - if !config.NotificationEnable() { - log.Debug("notification feature is not enabled") - return nil - } - - quotaEvent, ok := value.(*model.QuotaEvent) +func (qp *Handler) Handle(value interface{}) error { + quotaEvent, ok := value.(*event.QuotaEvent) if !ok { return errors.New("invalid quota event type") } @@ -30,14 +41,11 @@ func (qp *QuotaPreprocessHandler) Handle(value interface{}) error { return fmt.Errorf("nil quota event") } - project, err := config.GlobalProjectMgr.Get(quotaEvent.Project.Name) + project, err := project.Mgr.Get(quotaEvent.Project.Name) if err != nil { log.Errorf("failed to get project:%s, error: %v", quotaEvent.Project.Name, err) return err } - if project == nil { - return fmt.Errorf("project not found of quota event: %s", quotaEvent.Project.Name) - } policies, err := notification.PolicyMgr.GetRelatedPolices(project.ProjectID, quotaEvent.EventType) if err != nil { log.Errorf("failed to find policy for %s event: %v", quotaEvent.EventType, err) @@ -53,7 +61,7 @@ func (qp *QuotaPreprocessHandler) Handle(value interface{}) error { return err } - err = sendHookWithPolicies(policies, payload, quotaEvent.EventType) + err = util.SendHookWithPolicies(policies, payload, quotaEvent.EventType) if err != nil { return err } @@ -61,11 +69,11 @@ func (qp *QuotaPreprocessHandler) Handle(value interface{}) error { } // IsStateful ... -func (qp *QuotaPreprocessHandler) IsStateful() bool { +func (qp *Handler) IsStateful() bool { return false } -func constructQuotaPayload(event *model.QuotaEvent) (*model.Payload, error) { +func constructQuotaPayload(event *event.QuotaEvent) (*model.Payload, error) { repoName := event.RepoName if repoName == "" { return nil, fmt.Errorf("invalid %s event with empty repo name", event.EventType) @@ -76,7 +84,7 @@ func constructQuotaPayload(event *model.QuotaEvent) (*model.Payload, error) { repoType = models.ProjectPublic } - imageName := getNameFromImgRepoFullName(repoName) + imageName := util.GetNameFromImgRepoFullName(repoName) quotaCustom := make(map[string]string) quotaCustom["Details"] = event.Msg diff --git a/src/pkg/notifier/handler/notification/quota_handler_test.go b/src/api/event/handler/webhook/quota/quota_test.go similarity index 63% rename from src/pkg/notifier/handler/notification/quota_handler_test.go rename to src/api/event/handler/webhook/quota/quota_test.go index 9416625276..10583a950f 100644 --- a/src/pkg/notifier/handler/notification/quota_handler_test.go +++ b/src/api/event/handler/webhook/quota/quota_test.go @@ -1,6 +1,22 @@ -package notification +// 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 quota import ( + "github.com/goharbor/harbor/src/api/event" + common_dao "github.com/goharbor/harbor/src/common/dao" "testing" "time" @@ -11,6 +27,7 @@ import ( "github.com/goharbor/harbor/src/pkg/notification/policy" "github.com/goharbor/harbor/src/pkg/notifier" "github.com/goharbor/harbor/src/pkg/notifier/model" + testing_notification "github.com/goharbor/harbor/src/testing/pkg/notification" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) @@ -19,7 +36,7 @@ import ( type QuotaPreprocessHandlerSuite struct { suite.Suite om policy.Manager - evt *model.QuotaEvent + evt *event.QuotaEvent } // TestQuotaPreprocessHandler ... @@ -29,17 +46,18 @@ func TestQuotaPreprocessHandler(t *testing.T) { // SetupSuite prepares env for test suite. func (suite *QuotaPreprocessHandlerSuite) SetupSuite() { + common_dao.PrepareTestForPostgresSQL() cfg := map[string]interface{}{ common.NotificationEnable: true, } config.InitWithSettings(cfg) - res := &model.ImgResource{ + res := &event.ImgResource{ Digest: "sha256:abcd", Tag: "latest", } - suite.evt = &model.QuotaEvent{ - EventType: model.EventTypeProjectQuota, + suite.evt = &event.QuotaEvent{ + EventType: event.TopicQuotaExceed, OccurAt: time.Now().UTC(), RepoName: "hello-world", Resource: res, @@ -51,7 +69,7 @@ func (suite *QuotaPreprocessHandlerSuite) SetupSuite() { } suite.om = notification.PolicyMgr - mp := &fakedPolicyMgr{} + mp := &testing_notification.FakedPolicyMgr{} notification.PolicyMgr = mp h := &MockHandler{} @@ -67,7 +85,7 @@ func (suite *QuotaPreprocessHandlerSuite) TearDownSuite() { // TestHandle ... func (suite *QuotaPreprocessHandlerSuite) TestHandle() { - handler := &QuotaPreprocessHandler{} + handler := &Handler{} err := handler.Handle(suite.evt) suite.NoError(err) } diff --git a/src/api/event/handler/webhook/scan/delete.go b/src/api/event/handler/webhook/scan/delete.go new file mode 100644 index 0000000000..437a69f663 --- /dev/null +++ b/src/api/event/handler/webhook/scan/delete.go @@ -0,0 +1,78 @@ +// 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 scan + +import ( + "context" + bo "github.com/astaxie/beego/orm" + "github.com/goharbor/harbor/src/api/artifact" + "github.com/goharbor/harbor/src/api/event" + "github.com/goharbor/harbor/src/api/scan" + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/internal/orm" + "github.com/goharbor/harbor/src/pkg/q" + "github.com/pkg/errors" +) + +// DelArtHandler is a handler to listen to the internal delete image event. +type DelArtHandler struct { +} + +// Handle ... +func (o *DelArtHandler) Handle(value interface{}) error { + if value == nil { + return errors.New("delete image event handler: nil value ") + } + + evt, ok := value.(*event.ArtifactEvent) + if !ok { + return errors.New("delete image event handler: malformed image event model") + } + + log.Debugf("clear the scan reports as receiving event %s", evt.EventType) + + digests := make([]string, 0) + query := &q.Query{ + Keywords: make(map[string]interface{}), + } + + ctx := orm.NewContext(context.TODO(), bo.NewOrm()) + // Check if it is safe to delete the reports. + query.Keywords["digest"] = evt.Artifact.Digest + l, err := artifact.Ctl.List(ctx, query, nil) + + if err != nil && len(l) != 0 { + // Just logged + log.Error(errors.Wrap(err, "delete image event handler")) + // Passed for safe consideration + } else { + if len(l) == 0 { + digests = append(digests, evt.Artifact.Digest) + log.Debugf("prepare to remove the scan report linked with artifact: %s", evt.Artifact.Digest) + + } + } + + if err := scan.DefaultController.DeleteReports(digests...); err != nil { + return errors.Wrap(err, "delete image event handler") + } + + return nil +} + +// IsStateful ... +func (o *DelArtHandler) IsStateful() bool { + return false +} diff --git a/src/pkg/notifier/handler/notification/scan_image_handler.go b/src/api/event/handler/webhook/scan/scan.go similarity index 69% rename from src/pkg/notifier/handler/notification/scan_image_handler.go rename to src/api/event/handler/webhook/scan/scan.go index afa4ce4309..20bff8e0e8 100644 --- a/src/pkg/notifier/handler/notification/scan_image_handler.go +++ b/src/api/event/handler/webhook/scan/scan.go @@ -1,41 +1,51 @@ -package notification +// 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 scan import ( "context" - "time" - o "github.com/astaxie/beego/orm" "github.com/goharbor/harbor/src/api/artifact" + "github.com/goharbor/harbor/src/api/event" + "github.com/goharbor/harbor/src/api/event/handler/util" + "github.com/goharbor/harbor/src/internal/orm" + "github.com/goharbor/harbor/src/pkg/notifier/model" + "time" + "github.com/goharbor/harbor/src/api/scan" "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/utils/log" - "github.com/goharbor/harbor/src/core/config" - "github.com/goharbor/harbor/src/internal/orm" "github.com/goharbor/harbor/src/pkg/notification" - "github.com/goharbor/harbor/src/pkg/notifier/model" + "github.com/goharbor/harbor/src/pkg/project" v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" "github.com/pkg/errors" ) -// ScanImagePreprocessHandler preprocess chart event data -type ScanImagePreprocessHandler struct { +// Handler preprocess scan artifact event +type Handler struct { } // Handle preprocess chart event data and then publish hook event -func (si *ScanImagePreprocessHandler) Handle(value interface{}) error { - // if global notification configured disabled, return directly - if !config.NotificationEnable() { - log.Debug("notification feature is not enabled") - return nil - } - +func (si *Handler) Handle(value interface{}) error { if value == nil { - return errors.New("empty scan image event") + return errors.New("empty scan artifact event") } - e, ok := value.(*model.ScanImageEvent) + e, ok := value.(*event.ScanImageEvent) if !ok { - return errors.New("invalid scan image event type") + return errors.New("invalid scan artifact event type") } policies, err := notification.PolicyMgr.GetRelatedPolices(e.Artifact.NamespaceID, e.EventType) @@ -50,7 +60,7 @@ func (si *ScanImagePreprocessHandler) Handle(value interface{}) error { } // Get project - project, err := config.GlobalProjectMgr.Get(e.Artifact.NamespaceID) + project, err := project.Mgr.Get(e.Artifact.NamespaceID) if err != nil { return errors.Wrap(err, "scan preprocess handler") } @@ -60,7 +70,7 @@ func (si *ScanImagePreprocessHandler) Handle(value interface{}) error { return errors.Wrap(err, "scan preprocess handler") } - err = sendHookWithPolicies(policies, payload, e.EventType) + err = util.SendHookWithPolicies(policies, payload, e.EventType) if err != nil { return errors.Wrap(err, "scan preprocess handler") } @@ -69,17 +79,17 @@ func (si *ScanImagePreprocessHandler) Handle(value interface{}) error { } // IsStateful ... -func (si *ScanImagePreprocessHandler) IsStateful() bool { +func (si *Handler) IsStateful() bool { return false } -func constructScanImagePayload(event *model.ScanImageEvent, project *models.Project) (*model.Payload, error) { +func constructScanImagePayload(event *event.ScanImageEvent, project *models.Project) (*model.Payload, error) { repoType := models.ProjectPrivate if project.IsPublic() { repoType = models.ProjectPublic } - repoName := getNameFromImgRepoFullName(event.Artifact.Repository) + repoName := util.GetNameFromImgRepoFullName(event.Artifact.Repository) payload := &model.Payload{ Type: event.EventType, @@ -95,12 +105,7 @@ func constructScanImagePayload(event *model.ScanImageEvent, project *models.Proj Operator: event.Operator, } - extURL, err := config.ExtURL() - if err != nil { - return nil, errors.Wrap(err, "construct scan payload") - } - - resURL, err := buildImageResourceURL(extURL, event.Artifact.Repository, event.Artifact.Tag) + resURL, err := util.BuildImageResourceURL(event.Artifact.Repository, event.Artifact.Tag) if err != nil { return nil, errors.Wrap(err, "construct scan payload") } @@ -116,7 +121,6 @@ func constructScanImagePayload(event *model.ScanImageEvent, project *models.Proj if err != nil { return nil, err } - // Wait for reasonable time to make sure the report is ready // Interval=500ms and total time = 5s // If the report is still not ready in the total time, then failed at then diff --git a/src/pkg/notifier/handler/notification/scan_image_handler_test.go b/src/api/event/handler/webhook/scan/scan_test.go similarity index 90% rename from src/pkg/notifier/handler/notification/scan_image_handler_test.go rename to src/api/event/handler/webhook/scan/scan_test.go index 204aa870ee..8574307835 100644 --- a/src/pkg/notifier/handler/notification/scan_image_handler_test.go +++ b/src/api/event/handler/webhook/scan/scan_test.go @@ -12,9 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -package notification +package scan import ( + "github.com/goharbor/harbor/src/api/event" + common_dao "github.com/goharbor/harbor/src/common/dao" "testing" "time" @@ -32,6 +34,7 @@ import ( artifacttesting "github.com/goharbor/harbor/src/testing/api/artifact" scantesting "github.com/goharbor/harbor/src/testing/api/scan" "github.com/goharbor/harbor/src/testing/mock" + notificationtesting "github.com/goharbor/harbor/src/testing/pkg/notification" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) @@ -42,7 +45,7 @@ type ScanImagePreprocessHandlerSuite struct { om policy.Manager pid int64 - evt *model.ScanImageEvent + evt *event.ScanImageEvent c sc.Controller artifactCtl artifact.Controller } @@ -54,6 +57,7 @@ func TestScanImagePreprocessHandler(t *testing.T) { // SetupSuite prepares env for test suite. func (suite *ScanImagePreprocessHandlerSuite) SetupSuite() { + common_dao.PrepareTestForPostgresSQL() cfg := map[string]interface{}{ common.NotificationEnable: true, } @@ -66,8 +70,8 @@ func (suite *ScanImagePreprocessHandlerSuite) SetupSuite() { Digest: "digest-code", MimeType: v1.MimeTypeDockerArtifact, } - suite.evt = &model.ScanImageEvent{ - EventType: model.EventTypeScanningCompleted, + suite.evt = &event.ScanImageEvent{ + EventType: event.TopicScanningCompleted, OccurAt: time.Now().UTC(), Operator: "admin", Artifact: a, @@ -104,7 +108,7 @@ func (suite *ScanImagePreprocessHandlerSuite) SetupSuite() { artifact.Ctl = artifactCtl suite.om = notification.PolicyMgr - mp := &fakedPolicyMgr{} + mp := ¬ificationtesting.FakedPolicyMgr{} notification.PolicyMgr = mp h := &MockHTTPHandler{} @@ -122,7 +126,7 @@ func (suite *ScanImagePreprocessHandlerSuite) TearDownSuite() { // TestHandle ... func (suite *ScanImagePreprocessHandlerSuite) TestHandle() { - handler := &ScanImagePreprocessHandler{} + handler := &Handler{} err := handler.Handle(suite.evt) suite.NoError(err) diff --git a/src/api/event/artifact.go b/src/api/event/metadata/artifact.go similarity index 69% rename from src/api/event/artifact.go rename to src/api/event/metadata/artifact.go index e5c4b25255..8f116d2465 100644 --- a/src/api/event/artifact.go +++ b/src/api/event/metadata/artifact.go @@ -12,10 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -package event +package metadata import ( "context" + event2 "github.com/goharbor/harbor/src/api/event" "github.com/goharbor/harbor/src/common/security" "github.com/goharbor/harbor/src/pkg/artifact" "github.com/goharbor/harbor/src/pkg/notifier/event" @@ -31,17 +32,20 @@ type PushArtifactEventMetadata struct { // Resolve to the event from the metadata func (p *PushArtifactEventMetadata) Resolve(event *event.Event) error { - data := &PushArtifactEvent{ - Repository: p.Artifact.RepositoryName, - Artifact: p.Artifact, - Tag: p.Tag, - OccurAt: time.Now(), + data := &event2.PushArtifactEvent{ + ArtifactEvent: &event2.ArtifactEvent{ + EventType: event2.TopicPushArtifact, + Repository: p.Artifact.RepositoryName, + Artifact: p.Artifact, + Tags: []string{p.Tag}, + OccurAt: time.Now(), + }, } ctx, exist := security.FromContext(p.Ctx) if exist { data.Operator = ctx.GetUsername() } - event.Topic = TopicPushArtifact + event.Topic = event2.TopicPushArtifact event.Data = data return nil } @@ -55,17 +59,20 @@ type PullArtifactEventMetadata struct { // Resolve to the event from the metadata func (p *PullArtifactEventMetadata) Resolve(event *event.Event) error { - data := &PullArtifactEvent{ - Repository: p.Artifact.RepositoryName, - Artifact: p.Artifact, - Tag: p.Tag, - OccurAt: time.Now(), + data := &event2.PullArtifactEvent{ + ArtifactEvent: &event2.ArtifactEvent{ + EventType: event2.TopicPullArtifact, + Repository: p.Artifact.RepositoryName, + Artifact: p.Artifact, + Tags: []string{p.Tag}, + OccurAt: time.Now(), + }, } ctx, exist := security.FromContext(p.Ctx) if exist { data.Operator = ctx.GetUsername() } - event.Topic = TopicPullArtifact + event.Topic = event2.TopicPullArtifact event.Data = data return nil } @@ -79,17 +86,20 @@ type DeleteArtifactEventMetadata struct { // Resolve to the event from the metadata func (d *DeleteArtifactEventMetadata) Resolve(event *event.Event) error { - data := &DeleteArtifactEvent{ - Repository: d.Artifact.RepositoryName, - Artifact: d.Artifact, - Tags: d.Tags, - OccurAt: time.Now(), + data := &event2.DeleteArtifactEvent{ + ArtifactEvent: &event2.ArtifactEvent{ + EventType: event2.TopicDeleteArtifact, + Repository: d.Artifact.RepositoryName, + Artifact: d.Artifact, + Tags: d.Tags, + OccurAt: time.Now(), + }, } ctx, exist := security.FromContext(d.Ctx) if exist { data.Operator = ctx.GetUsername() } - event.Topic = TopicDeleteArtifact + event.Topic = event2.TopicDeleteArtifact event.Data = data return nil } diff --git a/src/api/event/artifact_test.go b/src/api/event/metadata/artifact_test.go similarity index 83% rename from src/api/event/artifact_test.go rename to src/api/event/metadata/artifact_test.go index a6cd231de4..191da55907 100644 --- a/src/api/event/artifact_test.go +++ b/src/api/event/metadata/artifact_test.go @@ -12,10 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -package event +package metadata import ( "context" + event2 "github.com/goharbor/harbor/src/api/event" "github.com/goharbor/harbor/src/pkg/artifact" "github.com/goharbor/harbor/src/pkg/notifier/event" "github.com/stretchr/testify/suite" @@ -35,12 +36,12 @@ func (a *artifactEventTestSuite) TestResolveOfPushArtifactEventMetadata() { } err := metadata.Resolve(e) a.Require().Nil(err) - a.Equal(TopicPushArtifact, e.Topic) + a.Equal(event2.TopicPushArtifact, e.Topic) a.Require().NotNil(e.Data) - data, ok := e.Data.(*PushArtifactEvent) + data, ok := e.Data.(*event2.PushArtifactEvent) a.Require().True(ok) a.Equal(int64(1), data.Artifact.ID) - a.Equal("latest", data.Tag) + a.Equal("latest", data.Tags[0]) } func (a *artifactEventTestSuite) TestResolveOfPullArtifactEventMetadata() { @@ -52,12 +53,12 @@ func (a *artifactEventTestSuite) TestResolveOfPullArtifactEventMetadata() { } err := metadata.Resolve(e) a.Require().Nil(err) - a.Equal(TopicPullArtifact, e.Topic) + a.Equal(event2.TopicPullArtifact, e.Topic) a.Require().NotNil(e.Data) - data, ok := e.Data.(*PullArtifactEvent) + data, ok := e.Data.(*event2.PullArtifactEvent) a.Require().True(ok) a.Equal(int64(1), data.Artifact.ID) - a.Equal("latest", data.Tag) + a.Equal("latest", data.Tags[0]) } func (a *artifactEventTestSuite) TestResolveOfDeleteArtifactEventMetadata() { @@ -69,9 +70,9 @@ func (a *artifactEventTestSuite) TestResolveOfDeleteArtifactEventMetadata() { } err := metadata.Resolve(e) a.Require().Nil(err) - a.Equal(TopicDeleteArtifact, e.Topic) + a.Equal(event2.TopicDeleteArtifact, e.Topic) a.Require().NotNil(e.Data) - data, ok := e.Data.(*DeleteArtifactEvent) + data, ok := e.Data.(*event2.DeleteArtifactEvent) a.Require().True(ok) a.Equal(int64(1), data.Artifact.ID) a.Require().Len(data.Tags, 1) diff --git a/src/api/event/metadata/chart.go b/src/api/event/metadata/chart.go new file mode 100644 index 0000000000..eed1935217 --- /dev/null +++ b/src/api/event/metadata/chart.go @@ -0,0 +1,75 @@ +package metadata + +import ( + event2 "github.com/goharbor/harbor/src/api/event" + "github.com/goharbor/harbor/src/pkg/notifier/event" + "time" +) + +// ChartMetaData defines meta data of chart event +type ChartMetaData struct { + ProjectName string + ChartName string + Versions []string + OccurAt time.Time + Operator string +} + +func (cmd *ChartMetaData) convert(evt *event2.ChartEvent) { + evt.ProjectName = cmd.ProjectName + evt.OccurAt = cmd.OccurAt + evt.Operator = cmd.Operator + evt.ChartName = cmd.ChartName + evt.Versions = cmd.Versions +} + +// ChartUploadMetaData defines meta data of chart upload event +type ChartUploadMetaData struct { + ChartMetaData +} + +// Resolve chart uploading metadata into common chart event +func (cu *ChartUploadMetaData) Resolve(event *event.Event) error { + data := &event2.ChartEvent{ + EventType: event2.TopicUploadChart, + } + cu.convert(data) + + event.Topic = event2.TopicUploadChart + event.Data = data + return nil +} + +// ChartDownloadMetaData defines meta data of chart download event +type ChartDownloadMetaData struct { + ChartMetaData +} + +// Resolve chart download metadata into common chart event +func (cd *ChartDownloadMetaData) Resolve(evt *event.Event) error { + data := &event2.ChartEvent{ + EventType: event2.TopicDownloadChart, + } + cd.convert(data) + + evt.Topic = event2.TopicDownloadChart + evt.Data = data + return nil +} + +// ChartDeleteMetaData defines meta data of chart delete event +type ChartDeleteMetaData struct { + ChartMetaData +} + +// Resolve chart delete metadata into common chart event +func (cd *ChartDeleteMetaData) Resolve(evt *event.Event) error { + data := &event2.ChartEvent{ + EventType: event2.TopicDeleteChart, + } + cd.convert(data) + + evt.Topic = event2.TopicDeleteChart + evt.Data = data + return nil +} diff --git a/src/api/event/metadata/chart_test.go b/src/api/event/metadata/chart_test.go new file mode 100644 index 0000000000..74df6b0fce --- /dev/null +++ b/src/api/event/metadata/chart_test.go @@ -0,0 +1,71 @@ +// 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 metadata + +import ( + event2 "github.com/goharbor/harbor/src/api/event" + "github.com/goharbor/harbor/src/pkg/notifier/event" + "github.com/stretchr/testify/suite" + "testing" + "time" +) + +type chartEventTestSuite struct { + suite.Suite +} + +func (r *chartEventTestSuite) TestResolveOfUploadChartEventMetadata() { + e := &event.Event{} + metadata := &ChartUploadMetaData{ + ChartMetaData{ + ProjectName: "library", + ChartName: "redis-v2.0", + Versions: nil, + OccurAt: time.Time{}, + Operator: "admin", + }, + } + err := metadata.Resolve(e) + r.Require().Nil(err) + r.Equal(event2.TopicUploadChart, e.Topic) + r.Require().NotNil(e.Data) + data, ok := e.Data.(*event2.ChartEvent) + r.Require().True(ok) + r.Equal("redis-v2.0", data.ChartName) +} + +func (r *chartEventTestSuite) TestResolveOfDownloadChartEventMetadata() { + e := &event.Event{} + metadata := &ChartDownloadMetaData{ + ChartMetaData{ + ProjectName: "library", + ChartName: "redis-v2.0", + Versions: nil, + OccurAt: time.Time{}, + Operator: "admin", + }, + } + err := metadata.Resolve(e) + r.Require().Nil(err) + r.Equal(event2.TopicDownloadChart, e.Topic) + r.Require().NotNil(e.Data) + data, ok := e.Data.(*event2.ChartEvent) + r.Require().True(ok) + r.Equal("redis-v2.0", data.ChartName) +} + +func TestChartEventTestSuite(t *testing.T) { + suite.Run(t, &chartEventTestSuite{}) +} diff --git a/src/api/event/project.go b/src/api/event/metadata/project.go similarity index 66% rename from src/api/event/project.go rename to src/api/event/metadata/project.go index 2493b00b1a..22f1d2dbbb 100644 --- a/src/api/event/project.go +++ b/src/api/event/metadata/project.go @@ -12,43 +12,50 @@ // See the License for the specific language governing permissions and // limitations under the License. -package event +package metadata import ( + event2 "github.com/goharbor/harbor/src/api/event" "github.com/goharbor/harbor/src/pkg/notifier/event" "time" ) // CreateProjectEventMetadata is the metadata from which the create project event can be resolved type CreateProjectEventMetadata struct { - Project string - Operator string + ProjectID int64 + Project string + Operator string } // Resolve to the event from the metadata func (c *CreateProjectEventMetadata) Resolve(event *event.Event) error { - event.Topic = TopicCreateProject - event.Data = &CreateProjectEvent{ - Project: c.Project, - Operator: c.Operator, - OccurAt: time.Now(), + event.Topic = event2.TopicCreateProject + event.Data = &event2.CreateProjectEvent{ + EventType: event2.TopicCreateProject, + ProjectID: c.ProjectID, + Project: c.Project, + Operator: c.Operator, + OccurAt: time.Now(), } return nil } // DeleteProjectEventMetadata is the metadata from which the delete project event can be resolved type DeleteProjectEventMetadata struct { - Project string - Operator string + ProjectID int64 + Project string + Operator string } // Resolve to the event from the metadata func (d *DeleteProjectEventMetadata) Resolve(event *event.Event) error { - event.Topic = TopicDeleteProject - event.Data = &DeleteProjectEvent{ - Project: d.Project, - Operator: d.Operator, - OccurAt: time.Now(), + event.Topic = event2.TopicDeleteProject + event.Data = &event2.DeleteProjectEvent{ + EventType: event2.TopicDeleteProject, + ProjectID: d.ProjectID, + Project: d.Project, + Operator: d.Operator, + OccurAt: time.Now(), } return nil } diff --git a/src/api/event/project_test.go b/src/api/event/metadata/project_test.go similarity index 85% rename from src/api/event/project_test.go rename to src/api/event/metadata/project_test.go index cd69fa1677..5326c599cd 100644 --- a/src/api/event/project_test.go +++ b/src/api/event/metadata/project_test.go @@ -12,9 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -package event +package metadata import ( + event2 "github.com/goharbor/harbor/src/api/event" "github.com/goharbor/harbor/src/pkg/notifier/event" "github.com/stretchr/testify/suite" "testing" @@ -32,9 +33,9 @@ func (p *projectEventTestSuite) TestResolveOfCreateProjectEventMetadata() { } err := metadata.Resolve(e) p.Require().Nil(err) - p.Equal(TopicCreateProject, e.Topic) + p.Equal(event2.TopicCreateProject, e.Topic) p.Require().NotNil(e.Data) - data, ok := e.Data.(*CreateProjectEvent) + data, ok := e.Data.(*event2.CreateProjectEvent) p.Require().True(ok) p.Equal("library", data.Project) p.Equal("admin", data.Operator) @@ -48,9 +49,9 @@ func (p *projectEventTestSuite) TestResolveOfDeleteProjectEventMetadata() { } err := metadata.Resolve(e) p.Require().Nil(err) - p.Equal(TopicDeleteProject, e.Topic) + p.Equal(event2.TopicDeleteProject, e.Topic) p.Require().NotNil(e.Data) - data, ok := e.Data.(*DeleteProjectEvent) + data, ok := e.Data.(*event2.DeleteProjectEvent) p.Require().True(ok) p.Equal("library", data.Project) p.Equal("admin", data.Operator) diff --git a/src/api/event/metadata/quota.go b/src/api/event/metadata/quota.go new file mode 100644 index 0000000000..319106a42b --- /dev/null +++ b/src/api/event/metadata/quota.go @@ -0,0 +1,51 @@ +package metadata + +import ( + event2 "github.com/goharbor/harbor/src/api/event" + "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/pkg/notifier/event" + "github.com/pkg/errors" + "time" +) + +// QuotaMetaData defines quota related event data +type QuotaMetaData struct { + Project *models.Project + RepoName string + Tag string + Digest string + // used to define the event topic + Level int + // the msg contains the limitation and current usage of quota + Msg string + OccurAt time.Time +} + +// Resolve quota exceed into common image event +func (q *QuotaMetaData) Resolve(evt *event.Event) error { + var topic string + data := &event2.QuotaEvent{ + EventType: event2.TopicQuotaExceed, + Project: q.Project, + Resource: &event2.ImgResource{ + Tag: q.Tag, + Digest: q.Digest, + }, + OccurAt: q.OccurAt, + RepoName: q.RepoName, + Msg: q.Msg, + } + + switch q.Level { + case 1: + topic = event2.TopicQuotaExceed + case 2: + topic = event2.TopicQuotaWarning + default: + return errors.New("not supported quota status") + } + + evt.Topic = topic + evt.Data = data + return nil +} diff --git a/src/api/event/metadata/quota_test.go b/src/api/event/metadata/quota_test.go new file mode 100644 index 0000000000..a41a393d6f --- /dev/null +++ b/src/api/event/metadata/quota_test.go @@ -0,0 +1,48 @@ +// 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 metadata + +import ( + event2 "github.com/goharbor/harbor/src/api/event" + "github.com/goharbor/harbor/src/pkg/notifier/event" + "github.com/stretchr/testify/suite" + "testing" +) + +type quotaEventTestSuite struct { + suite.Suite +} + +func (r *quotaEventTestSuite) TestResolveOfDeleteRepositoryEventMetadata() { + e := &event.Event{} + metadata := &QuotaMetaData{ + RepoName: "library/hello-world", + Tag: "latest", + Digest: "sha256:123absd", + Level: 1, + Msg: "quota exceed", + } + err := metadata.Resolve(e) + r.Require().Nil(err) + r.Equal(event2.TopicQuotaExceed, e.Topic) + r.Require().NotNil(e.Data) + data, ok := e.Data.(*event2.QuotaEvent) + r.Require().True(ok) + r.Equal("library/hello-world", data.Resource) +} + +func TestQuotaEventTestSuite(t *testing.T) { + suite.Run(t, &repositoryEventTestSuite{}) +} diff --git a/src/api/event/repository.go b/src/api/event/metadata/repository.go similarity index 86% rename from src/api/event/repository.go rename to src/api/event/metadata/repository.go index a5882b710d..861b223a04 100644 --- a/src/api/event/repository.go +++ b/src/api/event/metadata/repository.go @@ -12,10 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -package event +package metadata import ( "context" + event2 "github.com/goharbor/harbor/src/api/event" "github.com/goharbor/harbor/src/common/security" "github.com/goharbor/harbor/src/pkg/notifier/event" "time" @@ -25,19 +26,21 @@ import ( type DeleteRepositoryEventMetadata struct { Ctx context.Context Repository string + ProjectID int64 } // Resolve to the event from the metadata func (d *DeleteRepositoryEventMetadata) Resolve(event *event.Event) error { - data := &DeleteRepositoryEvent{ + data := &event2.DeleteRepositoryEvent{ Repository: d.Repository, + ProjectID: d.ProjectID, OccurAt: time.Now(), } cx, exist := security.FromContext(d.Ctx) if exist { data.Operator = cx.GetUsername() } - event.Topic = TopicDeleteRepository + event.Topic = event2.TopicDeleteRepository event.Data = data return nil } diff --git a/src/api/event/repository_test.go b/src/api/event/metadata/repository_test.go similarity index 88% rename from src/api/event/repository_test.go rename to src/api/event/metadata/repository_test.go index e4cc427a14..ca26a1268e 100644 --- a/src/api/event/repository_test.go +++ b/src/api/event/metadata/repository_test.go @@ -12,10 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -package event +package metadata import ( "context" + event2 "github.com/goharbor/harbor/src/api/event" "github.com/goharbor/harbor/src/pkg/notifier/event" "github.com/stretchr/testify/suite" "testing" @@ -33,9 +34,9 @@ func (r *repositoryEventTestSuite) TestResolveOfDeleteRepositoryEventMetadata() } err := metadata.Resolve(e) r.Require().Nil(err) - r.Equal(TopicDeleteRepository, e.Topic) + r.Equal(event2.TopicDeleteRepository, e.Topic) r.Require().NotNil(e.Data) - data, ok := e.Data.(*DeleteRepositoryEvent) + data, ok := e.Data.(*event2.DeleteRepositoryEvent) r.Require().True(ok) r.Equal("library/hello-world", data.Repository) } diff --git a/src/api/event/metadata/scan.go b/src/api/event/metadata/scan.go new file mode 100644 index 0000000000..83cdd47f49 --- /dev/null +++ b/src/api/event/metadata/scan.go @@ -0,0 +1,48 @@ +package metadata + +import ( + event2 "github.com/goharbor/harbor/src/api/event" + "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/pkg/notifier/event" + v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" + "github.com/pkg/errors" + "time" +) + +const ( + autoTriggeredOperator = "auto" +) + +// ScanImageMetaData defines meta data of image scanning event +type ScanImageMetaData struct { + Artifact *v1.Artifact + Status string +} + +// Resolve image scanning metadata into common chart event +func (si *ScanImageMetaData) Resolve(evt *event.Event) error { + var eventType string + var topic string + + switch si.Status { + case models.JobFinished: + eventType = event2.TopicScanningCompleted + topic = event2.TopicScanningCompleted + case models.JobError, models.JobStopped: + eventType = event2.TopicScanningFailed + topic = event2.TopicScanningFailed + default: + return errors.New("not supported scan hook status") + } + + data := &event2.ScanImageEvent{ + EventType: eventType, + Artifact: si.Artifact, + OccurAt: time.Now(), + Operator: autoTriggeredOperator, + } + + evt.Topic = topic + evt.Data = data + return nil +} diff --git a/src/api/event/metadata/scan_test.go b/src/api/event/metadata/scan_test.go new file mode 100644 index 0000000000..ff459fe27d --- /dev/null +++ b/src/api/event/metadata/scan_test.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 metadata + +import ( + event2 "github.com/goharbor/harbor/src/api/event" + "github.com/goharbor/harbor/src/pkg/notifier/event" + v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" + "github.com/stretchr/testify/suite" + "testing" +) + +type scanEventTestSuite struct { + suite.Suite +} + +func (r *scanEventTestSuite) TestResolveOfScanImageEventMetadata() { + e := &event.Event{} + metadata := &ScanImageMetaData{ + Artifact: &v1.Artifact{ + NamespaceID: 0, + Repository: "library/hello-world", + Tag: "latest", + Digest: "sha256:absdfd87123", + MimeType: "docker.chart", + }, + Status: "finished", + } + err := metadata.Resolve(e) + r.Require().Nil(err) + r.Equal(event2.TopicScanningCompleted, e.Topic) + r.Require().NotNil(e.Data) + data, ok := e.Data.(*event2.ScanImageEvent) + r.Require().True(ok) + r.Equal("library/hello-world", data.Artifact.Repository) +} + +func TestScanEventTestSuite(t *testing.T) { + suite.Run(t, &scanEventTestSuite{}) +} diff --git a/src/api/event/tag.go b/src/api/event/metadata/tag.go similarity index 87% rename from src/api/event/tag.go rename to src/api/event/metadata/tag.go index d84bbbe58e..7df2da2f15 100644 --- a/src/api/event/tag.go +++ b/src/api/event/metadata/tag.go @@ -12,10 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -package event +package metadata import ( "context" + event2 "github.com/goharbor/harbor/src/api/event" "github.com/goharbor/harbor/src/common/security" "github.com/goharbor/harbor/src/pkg/artifact" "github.com/goharbor/harbor/src/pkg/notifier/event" @@ -31,7 +32,8 @@ type CreateTagEventMetadata struct { // Resolve to the event from the metadata func (c *CreateTagEventMetadata) Resolve(event *event.Event) error { - data := &CreateTagEvent{ + data := &event2.CreateTagEvent{ + EventType: event2.TopicCreateTag, Repository: c.AttachedArtifact.RepositoryName, Tag: c.Tag, AttachedArtifact: c.AttachedArtifact, @@ -41,7 +43,7 @@ func (c *CreateTagEventMetadata) Resolve(event *event.Event) error { if exist { data.Operator = cx.GetUsername() } - event.Topic = TopicCreateTag + event.Topic = event2.TopicCreateTag event.Data = data return nil } @@ -55,7 +57,8 @@ type DeleteTagEventMetadata struct { // Resolve to the event from the metadata func (d *DeleteTagEventMetadata) Resolve(event *event.Event) error { - data := &DeleteTagEvent{ + data := &event2.DeleteTagEvent{ + EventType: event2.TopicDeleteTag, Repository: d.AttachedArtifact.RepositoryName, Tag: d.Tag, AttachedArtifact: d.AttachedArtifact, @@ -65,7 +68,7 @@ func (d *DeleteTagEventMetadata) Resolve(event *event.Event) error { if exist { data.Operator = ctx.GetUsername() } - event.Topic = TopicDeleteTag + event.Topic = event2.TopicDeleteTag event.Data = data return nil } diff --git a/src/api/event/tag_test.go b/src/api/event/metadata/tag_test.go similarity index 87% rename from src/api/event/tag_test.go rename to src/api/event/metadata/tag_test.go index 97220cfea1..f66a807ddd 100644 --- a/src/api/event/tag_test.go +++ b/src/api/event/metadata/tag_test.go @@ -12,10 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -package event +package metadata import ( "context" + event2 "github.com/goharbor/harbor/src/api/event" "github.com/goharbor/harbor/src/pkg/artifact" "github.com/goharbor/harbor/src/pkg/notifier/event" "github.com/stretchr/testify/suite" @@ -35,9 +36,9 @@ func (t *tagEventTestSuite) TestResolveOfCreateTagEventMetadata() { } err := metadata.Resolve(e) t.Require().Nil(err) - t.Equal(TopicCreateTag, e.Topic) + t.Equal(event2.TopicCreateTag, e.Topic) t.Require().NotNil(e.Data) - data, ok := e.Data.(*CreateTagEvent) + data, ok := e.Data.(*event2.CreateTagEvent) t.Require().True(ok) t.Equal(int64(1), data.AttachedArtifact.ID) t.Equal("latest", data.Tag) @@ -52,9 +53,9 @@ func (t *tagEventTestSuite) TestResolveOfDeleteTagEventMetadata() { } err := metadata.Resolve(e) t.Require().Nil(err) - t.Equal(TopicDeleteTag, e.Topic) + t.Equal(event2.TopicDeleteTag, e.Topic) t.Require().NotNil(e.Data) - data, ok := e.Data.(*DeleteTagEvent) + data, ok := e.Data.(*event2.DeleteTagEvent) t.Require().True(ok) t.Equal(int64(1), data.AttachedArtifact.ID) t.Equal("latest", data.Tag) diff --git a/src/api/event/topic.go b/src/api/event/topic.go index ed86ad77d1..0aa0f9c6d6 100644 --- a/src/api/event/topic.go +++ b/src/api/event/topic.go @@ -15,7 +15,11 @@ package event import ( + "fmt" + "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/pkg/artifact" + "github.com/goharbor/harbor/src/pkg/audit/model" + v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" "time" ) @@ -23,66 +27,168 @@ import ( // const definition const ( - TopicCreateProject = "CREATE_PROJECT" - TopicDeleteProject = "DELETE_PROJECT" - TopicDeleteRepository = "DELETE_REPOSITORY" - TopicPushArtifact = "PUSH_ARTIFACT" - TopicPullArtifact = "PULL_ARTIFACT" - TopicDeleteArtifact = "DELETE_ARTIFACT" - TopicCreateTag = "CREATE_TAG" - TopicDeleteTag = "DELETE_TAG" + TopicCreateProject = "CREATE_PROJECT" + TopicDeleteProject = "DELETE_PROJECT" + TopicPushArtifact = "PUSH_ARTIFACT" + TopicPullArtifact = "PULL_ARTIFACT" + TopicDeleteArtifact = "DELETE_ARTIFACT" + TopicDeleteRepository = "DELETE_REPOSITORY" + TopicCreateTag = "CREATE_TAG" + TopicDeleteTag = "DELETE_TAG" + TopicScanningFailed = "SCANNING_FAILED" + TopicScanningCompleted = "SCANNING_COMPLETED" + // QuotaExceedTopic is topic for quota warning event, the usage reaches the warning bar of limitation, like 85% + TopicQuotaWarning = "QUOTA_WARNNING" + TopicQuotaExceed = "QUOTA_EXCEED" + TopicUploadChart = "UPLOAD_CHART" + TopicDownloadChart = "DOWNLOAD_CHART" + TopicDeleteChart = "DELETE_CHART" ) // CreateProjectEvent is the creating project event type CreateProjectEvent struct { - Project string - Operator string - OccurAt time.Time + EventType string + ProjectID int64 + Project string + Operator string + OccurAt time.Time +} + +// ResolveToAuditLog ... +func (c *CreateProjectEvent) ResolveToAuditLog() (*model.AuditLog, error) { + auditLog := &model.AuditLog{ + ProjectID: c.ProjectID, + OpTime: c.OccurAt, + Operation: "create", + Username: c.Operator, + ResourceType: "project", + Resource: c.Project} + return auditLog, nil } // DeleteProjectEvent is the deleting project event type DeleteProjectEvent struct { - Project string - Operator string - OccurAt time.Time + EventType string + ProjectID int64 + Project string + Operator string + OccurAt time.Time +} + +// ResolveToAuditLog ... +func (d *DeleteProjectEvent) ResolveToAuditLog() (*model.AuditLog, error) { + auditLog := &model.AuditLog{ + ProjectID: d.ProjectID, + OpTime: d.OccurAt, + Operation: "delete", + Username: d.Operator, + ResourceType: "project", + Resource: d.Project} + return auditLog, nil } // DeleteRepositoryEvent is the deleting repository event type DeleteRepositoryEvent struct { + EventType string + ProjectID int64 Repository string Operator string OccurAt time.Time } +// ResolveToAuditLog ... +func (d *DeleteRepositoryEvent) ResolveToAuditLog() (*model.AuditLog, error) { + auditLog := &model.AuditLog{ + ProjectID: d.ProjectID, + OpTime: d.OccurAt, + Operation: "delete", + Username: d.Operator, + ResourceType: "repository", + Resource: d.Repository, + } + return auditLog, nil +} + +// ArtifactEvent is the pushing/pulling artifact event +type ArtifactEvent struct { + EventType string + Repository string + Artifact *artifact.Artifact + Tags []string // when the artifact is pushed by digest, the tag here will be null + Operator string + OccurAt time.Time +} + // PushArtifactEvent is the pushing artifact event type PushArtifactEvent struct { - Repository string - Artifact *artifact.Artifact - Tag string // when the artifact is pushed by digest, the tag here will be null - Operator string - OccurAt time.Time + *ArtifactEvent +} + +// ResolveToAuditLog ... +func (p *PushArtifactEvent) ResolveToAuditLog() (*model.AuditLog, error) { + auditLog := &model.AuditLog{ + ProjectID: p.Artifact.ProjectID, + OpTime: p.OccurAt, + Operation: "create", + Username: p.Operator, + ResourceType: "artifact"} + + if len(p.Tags) == 0 { + auditLog.Resource = fmt.Sprintf("%s:%s", + p.Artifact.RepositoryName, p.Artifact.Digest) + } else { + auditLog.Resource = fmt.Sprintf("%s:%s", + p.Artifact.RepositoryName, p.Tags[0]) + } + + return auditLog, nil } // PullArtifactEvent is the pulling artifact event type PullArtifactEvent struct { - Repository string - Artifact *artifact.Artifact - Tag string // when the artifact is pulled by digest, the tag here will be null - Operator string - OccurAt time.Time + *ArtifactEvent +} + +// ResolveToAuditLog ... +func (p *PullArtifactEvent) ResolveToAuditLog() (*model.AuditLog, error) { + auditLog := &model.AuditLog{ + ProjectID: p.Artifact.ProjectID, + OpTime: p.OccurAt, + Operation: "pull", + Username: p.Operator, + ResourceType: "artifact"} + + if len(p.Tags) == 0 { + auditLog.Resource = fmt.Sprintf("%s:%s", + p.Artifact.RepositoryName, p.Artifact.Digest) + } else { + auditLog.Resource = fmt.Sprintf("%s:%s", + p.Artifact.RepositoryName, p.Tags[0]) + } + + return auditLog, nil } // DeleteArtifactEvent is the deleting artifact event type DeleteArtifactEvent struct { - Repository string - Artifact *artifact.Artifact - Tags []string // all the tags that attached to the deleted artifact - Operator string - OccurAt time.Time + *ArtifactEvent +} + +// ResolveToAuditLog ... +func (p *DeleteArtifactEvent) ResolveToAuditLog() (*model.AuditLog, error) { + auditLog := &model.AuditLog{ + ProjectID: p.Artifact.ProjectID, + OpTime: p.OccurAt, + Operation: "delete", + Username: p.Operator, + ResourceType: "artifact", + Resource: fmt.Sprintf("%s:%s", p.Artifact.RepositoryName, p.Artifact.Digest)} + return auditLog, nil } // CreateTagEvent is the creating tag event type CreateTagEvent struct { + EventType string Repository string Tag string AttachedArtifact *artifact.Artifact @@ -90,11 +196,70 @@ type CreateTagEvent struct { OccurAt time.Time } +// ResolveToAuditLog ... +func (c *CreateTagEvent) ResolveToAuditLog() (*model.AuditLog, error) { + auditLog := &model.AuditLog{ + ProjectID: c.AttachedArtifact.ProjectID, + OpTime: c.OccurAt, + Operation: "create", + Username: c.Operator, + ResourceType: "tag", + Resource: fmt.Sprintf("%s:%s", c.Repository, c.Tag)} + return auditLog, nil +} + // DeleteTagEvent is the deleting tag event type DeleteTagEvent struct { + EventType string Repository string Tag string AttachedArtifact *artifact.Artifact Operator string OccurAt time.Time } + +// ResolveToAuditLog ... +func (d *DeleteTagEvent) ResolveToAuditLog() (*model.AuditLog, error) { + auditLog := &model.AuditLog{ + ProjectID: d.AttachedArtifact.ProjectID, + OpTime: d.OccurAt, + Operation: "delete", + Username: d.Operator, + ResourceType: "tag", + Resource: fmt.Sprintf("%s:%s", d.Repository, d.Tag)} + return auditLog, nil +} + +// ScanImageEvent is scanning image related event data to publish +type ScanImageEvent struct { + EventType string + Artifact *v1.Artifact + OccurAt time.Time + Operator string +} + +// ChartEvent is chart related event data to publish +type ChartEvent struct { + EventType string + ProjectName string + ChartName string + Versions []string + OccurAt time.Time + Operator string +} + +// QuotaEvent is project quota related event data to publish +type QuotaEvent struct { + EventType string + Project *models.Project + Resource *ImgResource + OccurAt time.Time + RepoName string + Msg string +} + +// ImgResource include image digest and tag +type ImgResource struct { + Digest string + Tag string +} diff --git a/src/chartserver/reverse_proxy.go b/src/chartserver/reverse_proxy.go index 22bc965cc8..dc42423fb3 100644 --- a/src/chartserver/reverse_proxy.go +++ b/src/chartserver/reverse_proxy.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "github.com/goharbor/harbor/src/api/event/metadata" "io/ioutil" "log" "net/http" @@ -123,8 +124,8 @@ func modifyResponse(res *http.Response) error { if e != nil && e.Resource != nil && e.Resource.Metadata != nil && len(e.Resource.Metadata.Vtags) > 0 && len(e.Resource.ExtendedInfo) > 0 { event := &n_event.Event{} - metaData := &n_event.ChartUploadMetaData{ - ChartMetaData: n_event.ChartMetaData{ + metaData := &metadata.ChartUploadMetaData{ + ChartMetaData: metadata.ChartMetaData{ ProjectName: e.Resource.ExtendedInfo["projectName"].(string), ChartName: e.Resource.ExtendedInfo["chartName"].(string), Versions: e.Resource.Metadata.Vtags, @@ -146,7 +147,7 @@ func modifyResponse(res *http.Response) error { // Process downloading chart success webhook event if res.StatusCode == http.StatusOK { chartDownloadEvent := res.Request.Context().Value(common.ChartDownloadCtxKey) - eventMetaData, ok := chartDownloadEvent.(*n_event.ChartDownloadMetaData) + eventMetaData, ok := chartDownloadEvent.(*metadata.ChartDownloadMetaData) if ok && eventMetaData != nil { // Trigger harbor webhook event := &n_event.Event{} diff --git a/src/core/api/chart_repository.go b/src/core/api/chart_repository.go index 21c96e80ae..ad0cacb49a 100755 --- a/src/core/api/chart_repository.go +++ b/src/core/api/chart_repository.go @@ -5,6 +5,7 @@ import ( "context" "errors" "fmt" + "github.com/goharbor/harbor/src/api/event/metadata" "io" "io/ioutil" "mime/multipart" @@ -287,8 +288,8 @@ func (cra *ChartRepositoryAPI) DeleteChartVersion() { } event := &n_event.Event{} - metaData := &n_event.ChartDeleteMetaData{ - ChartMetaData: n_event.ChartMetaData{ + metaData := &metadata.ChartDeleteMetaData{ + ChartMetaData: metadata.ChartMetaData{ ProjectName: cra.namespace, ChartName: chartName, Versions: []string{version}, @@ -395,8 +396,8 @@ func (cra *ChartRepositoryAPI) DeleteChart() { } event := &n_event.Event{} - metaData := &n_event.ChartDeleteMetaData{ - ChartMetaData: n_event.ChartMetaData{ + metaData := &metadata.ChartDeleteMetaData{ + ChartMetaData: metadata.ChartMetaData{ ProjectName: cra.namespace, ChartName: chartName, Versions: versions, @@ -527,8 +528,8 @@ func (cra *ChartRepositoryAPI) addEventContext(files []formFile, request *http.R func (cra *ChartRepositoryAPI) addDownloadChartEventContext(fileName, namespace string, request *http.Request) { chartName, version := parseChartVersionFromFilename(fileName) - event := &n_event.ChartDownloadMetaData{ - ChartMetaData: n_event.ChartMetaData{ + event := &metadata.ChartDownloadMetaData{ + ChartMetaData: metadata.ChartMetaData{ ProjectName: namespace, ChartName: chartName, Versions: []string{version}, diff --git a/src/core/api/notification_job_test.go b/src/core/api/notification_job_test.go index ce6d4ee1f1..88c2fbfece 100644 --- a/src/core/api/notification_job_test.go +++ b/src/core/api/notification_job_test.go @@ -1,13 +1,13 @@ package api import ( + "github.com/goharbor/harbor/src/api/event" "net/http" "testing" "time" "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/pkg/notification" - "github.com/goharbor/harbor/src/pkg/notifier/model" ) type fakedNotificationJobMgr struct { @@ -28,11 +28,11 @@ func (f *fakedNotificationJobMgr) Update(job *models.NotificationJob, props ...s func (f *fakedNotificationJobMgr) ListJobsGroupByEventType(policyID int64) ([]*models.NotificationJob, error) { return []*models.NotificationJob{ { - EventType: model.EventTypePullImage, + EventType: event.TopicPullArtifact, CreationTime: time.Now(), }, { - EventType: model.EventTypeDeleteImage, + EventType: event.TopicDeleteArtifact, CreationTime: time.Now(), }, }, nil diff --git a/src/core/api/notification_policy.go b/src/core/api/notification_policy.go index 67eceaec9e..b1e230771b 100755 --- a/src/core/api/notification_policy.go +++ b/src/core/api/notification_policy.go @@ -3,6 +3,7 @@ package api import ( "errors" "fmt" + "github.com/goharbor/harbor/src/api/event" "net/http" "strconv" "time" @@ -18,7 +19,8 @@ import ( // NotificationPolicyAPI ... type NotificationPolicyAPI struct { BaseController - project *models.Project + project *models.Project + supportedEvents map[string]struct{} } // notificationPolicyForUI defines the structure of notification policy info display in UI @@ -63,6 +65,7 @@ func (w *NotificationPolicyAPI) Prepare() { return } w.project = project + w.supportedEvents = initSupportedEvents() } // Get ... @@ -274,7 +277,7 @@ func (w *NotificationPolicyAPI) GetSupportedEventTypes() { notificationTypes.NotifyType = append(notificationTypes.NotifyType, key) } - for key := range notification.SupportedEventTypes { + for key := range w.supportedEvents { notificationTypes.EventType = append(notificationTypes.EventType, key) } w.WriteJSONData(notificationTypes) @@ -345,7 +348,7 @@ func (w *NotificationPolicyAPI) validateEventTypes(policy *models.NotificationPo } for _, eventType := range policy.EventTypes { - _, ok := notification.SupportedEventTypes[eventType] + _, ok := w.supportedEvents[eventType] if !ok { w.SendBadRequestError(fmt.Errorf("unsupport event type %s", eventType)) return false @@ -369,6 +372,19 @@ func getLastTriggerTimeGroupByEventType(eventType string, policyID int64) (time. return time.Time{}, nil } +func initSupportedEvents() map[string]struct{} { + var supportedEventTypes = make(map[string]struct{}) + eventTypes := []string{event.TopicPushArtifact, event.TopicPullArtifact, + event.TopicDeleteArtifact, event.TopicUploadChart, event.TopicDeleteChart, + event.TopicDownloadChart, event.TopicQuotaExceed, event.TopicScanningFailed, + event.TopicScanningCompleted} + + for _, eventType := range eventTypes { + supportedEventTypes[eventType] = struct{}{} + } + return supportedEventTypes +} + // constructPolicyWithTriggerTime construct notification policy information displayed in UI // including event type, enabled, creation time, last trigger time func constructPolicyWithTriggerTime(policies []*models.NotificationPolicy) ([]*notificationPolicyForUI, error) { diff --git a/src/core/api/notification_policy_test.go b/src/core/api/notification_policy_test.go index e946276e91..b0d59ea21e 100644 --- a/src/core/api/notification_policy_test.go +++ b/src/core/api/notification_policy_test.go @@ -1,13 +1,12 @@ package api import ( + "github.com/goharbor/harbor/src/api/event" "net/http" "testing" "github.com/pkg/errors" - "github.com/goharbor/harbor/src/pkg/notifier/model" - "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/pkg/notification" ) @@ -24,8 +23,8 @@ func (f *fakedNotificationPlyMgr) List(id int64) ([]*models.NotificationPolicy, { ID: 1, EventTypes: []string{ - model.EventTypePullImage, - model.EventTypePushImage, + event.TopicPullArtifact, + event.TopicPushArtifact, }, }, }, nil @@ -168,7 +167,7 @@ func TestNotificationPolicyAPI_Post(t *testing.T) { url: "/api/projects/1/webhook/policies", credential: sysAdmin, bodyJSON: &models.NotificationPolicy{ - EventTypes: []string{"pullImage", "pushImage", "deleteImage"}, + EventTypes: []string{"PULL_ARTIFACT", "PUSH_ARTIFACT", "DELETE_ARTIFACT"}, Targets: []models.EventTarget{ { Address: "tcp://127.0.0.1:8080", @@ -184,7 +183,7 @@ func TestNotificationPolicyAPI_Post(t *testing.T) { url: "/api/projects/1/webhook/policies", credential: sysAdmin, bodyJSON: &models.NotificationPolicy{ - EventTypes: []string{"pullImage", "pushImage", "deleteImage"}, + EventTypes: []string{"PULL_ARTIFACT", "PUSH_ARTIFACT", "DELETE_ARTIFACT"}, Targets: []models.EventTarget{ { Type: "smn", @@ -218,7 +217,7 @@ func TestNotificationPolicyAPI_Post(t *testing.T) { credential: sysAdmin, bodyJSON: &models.NotificationPolicy{ ID: 111, - EventTypes: []string{"pullImage", "pushImage", "deleteImage"}, + EventTypes: []string{"PULL_ARTIFACT", "PUSH_ARTIFACT", "DELETE_ARTIFACT"}, Targets: []models.EventTarget{ { Type: "http", @@ -238,7 +237,7 @@ func TestNotificationPolicyAPI_Post(t *testing.T) { url: "/api/projects/1/webhook/policies", credential: sysAdmin, bodyJSON: &models.NotificationPolicy{ - EventTypes: []string{"pullImage", "pushImage", "deleteImage"}, + EventTypes: []string{"PULL_ARTIFACT", "PUSH_ARTIFACT", "DELETE_ARTIFACT"}, Targets: []models.EventTarget{ { Type: "http", @@ -373,7 +372,7 @@ func TestNotificationPolicyAPI_Put(t *testing.T) { url: "/api/projects/1/webhook/policies/1", credential: sysAdmin, bodyJSON: &models.NotificationPolicy{ - EventTypes: []string{"pullImage", "pushImage", "deleteImage"}, + EventTypes: []string{"PULL_ARTIFACT", "PUSH_ARTIFACT", "DELETE_ARTIFACT"}, Targets: []models.EventTarget{}, }}, code: http.StatusBadRequest, @@ -385,7 +384,7 @@ func TestNotificationPolicyAPI_Put(t *testing.T) { url: "/api/projects/1/webhook/policies/1", credential: sysAdmin, bodyJSON: &models.NotificationPolicy{ - EventTypes: []string{"pullImage", "pushImage", "deleteImage"}, + EventTypes: []string{"PULL_ARTIFACT", "PUSH_ARTIFACT", "DELETE_ARTIFACT"}, Targets: []models.EventTarget{ { Address: "tcp://127.0.0.1:8080", @@ -401,7 +400,7 @@ func TestNotificationPolicyAPI_Put(t *testing.T) { url: "/api/projects/1/webhook/policies/1", credential: sysAdmin, bodyJSON: &models.NotificationPolicy{ - EventTypes: []string{"pullImage", "pushImage", "deleteImage"}, + EventTypes: []string{"PULL_ARTIFACT", "PUSH_ARTIFACT", "DELETE_ARTIFACT"}, Targets: []models.EventTarget{ { Type: "smn", @@ -435,7 +434,7 @@ func TestNotificationPolicyAPI_Put(t *testing.T) { credential: sysAdmin, bodyJSON: &models.NotificationPolicy{ Name: "imagePolicyTest", - EventTypes: []string{"pullImage", "pushImage", "deleteImage"}, + EventTypes: []string{"PULL_ARTIFACT", "PUSH_ARTIFACT", "DELETE_ARTIFACT"}, Targets: []models.EventTarget{ { Type: "http", diff --git a/src/core/api/project.go b/src/core/api/project.go index 830c9ba485..78b04c83b4 100644 --- a/src/core/api/project.go +++ b/src/core/api/project.go @@ -22,7 +22,7 @@ import ( "strings" "sync" - "github.com/goharbor/harbor/src/api/event" + "github.com/goharbor/harbor/src/api/event/metadata" "github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/dao/project" @@ -213,9 +213,10 @@ func (p *ProjectAPI) Post() { } // fire event - evt.BuildAndPublish(&event.CreateProjectEventMetadata{ - Project: pro.Name, - Operator: owner, + evt.BuildAndPublish(&metadata.CreateProjectEventMetadata{ + ProjectID: projectID, + Project: pro.Name, + Operator: owner, }) p.Redirect(http.StatusCreated, strconv.FormatInt(projectID, 10)) @@ -295,9 +296,10 @@ func (p *ProjectAPI) Delete() { } // fire event - evt.BuildAndPublish(&event.DeleteProjectEventMetadata{ - Project: p.project.Name, - Operator: p.SecurityCtx.GetUsername(), + evt.BuildAndPublish(&metadata.DeleteProjectEventMetadata{ + ProjectID: p.project.ProjectID, + Project: p.project.Name, + Operator: p.SecurityCtx.GetUsername(), }) } diff --git a/src/core/main.go b/src/core/main.go index c44fb05cfc..e132332668 100755 --- a/src/core/main.go +++ b/src/core/main.go @@ -45,7 +45,6 @@ import ( _ "github.com/goharbor/harbor/src/pkg/notifier/topic" "github.com/goharbor/harbor/src/pkg/scan" "github.com/goharbor/harbor/src/pkg/scan/dao/scanner" - "github.com/goharbor/harbor/src/pkg/scan/event" "github.com/goharbor/harbor/src/pkg/scheduler" "github.com/goharbor/harbor/src/pkg/version" "github.com/goharbor/harbor/src/replication" @@ -155,8 +154,6 @@ func main() { log.Info("initializing notification...") notification.Init() - // Initialize the event handlers for handling artifact cascade deletion - event.Init() filter.Init() beego.InsertFilter("/api/*", beego.BeforeStatic, filter.SessionCheck) diff --git a/src/core/service/notifications/jobs/handler.go b/src/core/service/notifications/jobs/handler.go index a045ef8738..2e5cbac774 100755 --- a/src/core/service/notifications/jobs/handler.go +++ b/src/core/service/notifications/jobs/handler.go @@ -20,6 +20,7 @@ import ( "github.com/goharbor/harbor/src/core/service/notifications" + "github.com/goharbor/harbor/src/api/event/metadata" "github.com/goharbor/harbor/src/api/scan" "github.com/goharbor/harbor/src/common/job" "github.com/goharbor/harbor/src/common/models" @@ -116,7 +117,7 @@ func (h *Handler) HandleScan() { log.Debugf("Scan %s for artifact: %#v", h.status, req.Artifact) e := &event.Event{} - metaData := &event.ScanImageMetaData{ + metaData := &metadata.ScanImageMetaData{ Artifact: req.Artifact, Status: h.status, } diff --git a/src/pkg/notification/notification.go b/src/pkg/notification/notification.go index 342c870754..5811ba556f 100755 --- a/src/pkg/notification/notification.go +++ b/src/pkg/notification/notification.go @@ -39,26 +39,12 @@ func Init() { // init notification job manager JobMgr = jobMgr.NewDefaultManager() - SupportedEventTypes = make(map[string]struct{}) SupportedNotifyTypes = make(map[string]struct{}) - - initSupportedEventType( - model.EventTypePushImage, model.EventTypePullImage, model.EventTypeDeleteImage, - model.EventTypeUploadChart, model.EventTypeDeleteChart, model.EventTypeDownloadChart, - model.EventTypeScanningCompleted, model.EventTypeScanningFailed, model.EventTypeProjectQuota, - ) - initSupportedNotifyType(model.NotifyTypeHTTP, model.NotifyTypeSlack) log.Info("notification initialization completed") } -func initSupportedEventType(eventTypes ...string) { - for _, eventType := range eventTypes { - SupportedEventTypes[eventType] = struct{}{} - } -} - func initSupportedNotifyType(notifyTypes ...string) { for _, notifyType := range notifyTypes { SupportedNotifyTypes[notifyType] = struct{}{} diff --git a/src/pkg/notification/policy/manager/manager.go b/src/pkg/notification/policy/manager/manager.go index ff58d346bb..dc41c4ecd1 100755 --- a/src/pkg/notification/policy/manager/manager.go +++ b/src/pkg/notification/policy/manager/manager.go @@ -1,7 +1,6 @@ package manager import ( - "encoding/json" "fmt" "net/http" "time" @@ -10,8 +9,6 @@ import ( commonhttp "github.com/goharbor/harbor/src/common/http" "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/utils/log" - "github.com/goharbor/harbor/src/pkg/notifier/model" - notifierModel "github.com/goharbor/harbor/src/pkg/notifier/model" ) // DefaultManager ... @@ -95,17 +92,10 @@ func (m *DefaultManager) Delete(policyID int64) error { // Test the specified notification policy, just test for network connection without request body func (m *DefaultManager) Test(policy *models.NotificationPolicy) error { - p, err := json.Marshal(notifierModel.Payload{ - Type: model.EventTypeTestEndpoint, - }) - if err != nil { - return err - } - for _, target := range policy.Targets { switch target.Type { case "http": - return m.policyHTTPTest(target.Address, target.SkipCertVerify, p) + return m.policyHTTPTest(target.Address, target.SkipCertVerify) default: return fmt.Errorf("invalid policy target type: %s", target.Type) } @@ -113,7 +103,7 @@ func (m *DefaultManager) Test(policy *models.NotificationPolicy) error { return nil } -func (m *DefaultManager) policyHTTPTest(address string, skipCertVerify bool, p []byte) error { +func (m *DefaultManager) policyHTTPTest(address string, skipCertVerify bool) error { req, err := http.NewRequest(http.MethodPost, address, nil) if err != nil { return err diff --git a/src/pkg/notifier/event/event.go b/src/pkg/notifier/event/event.go index 604b9fad7b..04b9e2e947 100644 --- a/src/pkg/notifier/event/event.go +++ b/src/pkg/notifier/event/event.go @@ -1,21 +1,13 @@ package event import ( - "time" - "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/pkg/notifier" "github.com/goharbor/harbor/src/pkg/notifier/model" - notifyModel "github.com/goharbor/harbor/src/pkg/notifier/model" - v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" "github.com/pkg/errors" ) -const ( - autoTriggeredOperator = "auto" -) - // Event to publish type Event struct { Topic string @@ -44,243 +36,6 @@ type Metadata interface { Resolve(event *Event) error } -// ImageDelMetaData defines images deleting related event data -type ImageDelMetaData struct { - Project *models.Project - Tags []string - Digests map[string]string - OccurAt time.Time - Operator string - RepoName string -} - -// Resolve image deleting metadata into common image event -func (i *ImageDelMetaData) Resolve(evt *Event) error { - data := &model.ImageEvent{ - EventType: notifyModel.EventTypeDeleteImage, - Project: i.Project, - OccurAt: i.OccurAt, - Operator: i.Operator, - RepoName: i.RepoName, - } - for _, t := range i.Tags { - res := &model.ImgResource{ - Tag: t, - Digest: i.Digests[t], - } - data.Resource = append(data.Resource, res) - } - evt.Topic = model.DeleteImageTopic - evt.Data = data - return nil -} - -// ImagePushMetaData defines images pushing related event data -type ImagePushMetaData struct { - Project *models.Project - Tag string - Digest string - OccurAt time.Time - Operator string - RepoName string -} - -// Resolve image pushing metadata into common image event -func (i *ImagePushMetaData) Resolve(evt *Event) error { - data := &model.ImageEvent{ - EventType: notifyModel.EventTypePushImage, - Project: i.Project, - OccurAt: i.OccurAt, - Operator: i.Operator, - RepoName: i.RepoName, - Resource: []*model.ImgResource{ - { - Tag: i.Tag, - Digest: i.Digest, - }, - }, - } - - evt.Topic = model.PushImageTopic - evt.Data = data - return nil -} - -// ImagePullMetaData defines images pulling related event data -type ImagePullMetaData struct { - Project *models.Project - Tag string - Digest string - OccurAt time.Time - Operator string - RepoName string -} - -// Resolve image pulling metadata into common image event -func (i *ImagePullMetaData) Resolve(evt *Event) error { - data := &model.ImageEvent{ - EventType: notifyModel.EventTypePullImage, - Project: i.Project, - OccurAt: i.OccurAt, - Operator: i.Operator, - RepoName: i.RepoName, - Resource: []*model.ImgResource{ - { - Tag: i.Tag, - Digest: i.Digest, - }, - }, - } - - evt.Topic = model.PullImageTopic - evt.Data = data - return nil -} - -// ChartMetaData defines meta data of chart event -type ChartMetaData struct { - ProjectName string - ChartName string - Versions []string - OccurAt time.Time - Operator string -} - -func (cmd *ChartMetaData) convert(evt *model.ChartEvent) { - evt.ProjectName = cmd.ProjectName - evt.OccurAt = cmd.OccurAt - evt.Operator = cmd.Operator - evt.ChartName = cmd.ChartName - evt.Versions = cmd.Versions -} - -// ChartUploadMetaData defines meta data of chart upload event -type ChartUploadMetaData struct { - ChartMetaData -} - -// Resolve chart uploading metadata into common chart event -func (cu *ChartUploadMetaData) Resolve(evt *Event) error { - data := &model.ChartEvent{ - EventType: notifyModel.EventTypeUploadChart, - } - cu.convert(data) - - evt.Topic = model.UploadChartTopic - evt.Data = data - return nil -} - -// ChartDownloadMetaData defines meta data of chart download event -type ChartDownloadMetaData struct { - ChartMetaData -} - -// Resolve chart download metadata into common chart event -func (cd *ChartDownloadMetaData) Resolve(evt *Event) error { - data := &model.ChartEvent{ - EventType: notifyModel.EventTypeDownloadChart, - } - cd.convert(data) - - evt.Topic = model.DownloadChartTopic - evt.Data = data - return nil -} - -// ChartDeleteMetaData defines meta data of chart delete event -type ChartDeleteMetaData struct { - ChartMetaData -} - -// Resolve chart delete metadata into common chart event -func (cd *ChartDeleteMetaData) Resolve(evt *Event) error { - data := &model.ChartEvent{ - EventType: notifyModel.EventTypeDeleteChart, - } - cd.convert(data) - - evt.Topic = model.DeleteChartTopic - evt.Data = data - return nil -} - -// ScanImageMetaData defines meta data of image scanning event -type ScanImageMetaData struct { - Artifact *v1.Artifact - Status string -} - -// Resolve image scanning metadata into common chart event -func (si *ScanImageMetaData) Resolve(evt *Event) error { - var eventType string - var topic string - - switch si.Status { - case models.JobFinished: - eventType = notifyModel.EventTypeScanningCompleted - topic = model.ScanningCompletedTopic - case models.JobError, models.JobStopped: - eventType = notifyModel.EventTypeScanningFailed - topic = model.ScanningFailedTopic - default: - return errors.New("not supported scan hook status") - } - - data := &model.ScanImageEvent{ - EventType: eventType, - Artifact: si.Artifact, - OccurAt: time.Now(), - Operator: autoTriggeredOperator, - } - - evt.Topic = topic - evt.Data = data - return nil -} - -// QuotaMetaData defines quota related event data -type QuotaMetaData struct { - Project *models.Project - RepoName string - Tag string - Digest string - // used to define the event topic - Level int - // the msg contains the limitation and current usage of quota - Msg string - OccurAt time.Time -} - -// Resolve quota exceed into common image event -func (q *QuotaMetaData) Resolve(evt *Event) error { - var topic string - data := &model.QuotaEvent{ - EventType: notifyModel.EventTypeProjectQuota, - Project: q.Project, - Resource: &model.ImgResource{ - Tag: q.Tag, - Digest: q.Digest, - }, - OccurAt: q.OccurAt, - RepoName: q.RepoName, - Msg: q.Msg, - } - - switch q.Level { - case 1: - topic = model.QuotaExceedTopic - case 2: - topic = model.QuotaWarningTopic - default: - return errors.New("not supported quota status") - } - - evt.Topic = topic - evt.Data = data - return nil -} - // HookMetaData defines hook notification related event data type HookMetaData struct { PolicyID int64 diff --git a/src/pkg/notifier/event/event_test.go b/src/pkg/notifier/event/event_test.go index d9b19f8e60..dec5306aeb 100644 --- a/src/pkg/notifier/event/event_test.go +++ b/src/pkg/notifier/event/event_test.go @@ -1,141 +1,13 @@ package event import ( - "testing" - "time" - "github.com/goharbor/harbor/src/common/models" notifierModel "github.com/goharbor/harbor/src/pkg/notifier/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "testing" ) -func TestImagePushEvent_Build(t *testing.T) { - type args struct { - imgPushMetadata *ImagePushMetaData - hookMetadata *HookMetaData - } - - tests := []struct { - name string - args args - wantErr bool - want *Event - }{ - { - name: "Build Image Push Event", - args: args{ - imgPushMetadata: &ImagePushMetaData{ - Project: &models.Project{ProjectID: 1, Name: "library"}, - Tag: "v1.0", - Digest: "abcd", - OccurAt: time.Now(), - Operator: "admin", - RepoName: "library/alpine", - }, - }, - want: &Event{ - Topic: notifierModel.PushImageTopic, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - event := &Event{} - err := event.Build(tt.args.imgPushMetadata) - if tt.wantErr { - require.NotNil(t, err, "Error: %s", err) - return - } - assert.Equal(t, tt.want.Topic, event.Topic) - }) - } -} - -func TestImagePullEvent_Build(t *testing.T) { - type args struct { - imgPullMetadata *ImagePullMetaData - } - - tests := []struct { - name string - args args - wantErr bool - want *Event - }{ - { - name: "Build Image Pull Event", - args: args{ - imgPullMetadata: &ImagePullMetaData{ - Project: &models.Project{ProjectID: 1, Name: "library"}, - Tag: "v1.0", - Digest: "abcd", - OccurAt: time.Now(), - Operator: "admin", - RepoName: "library/alpine", - }, - }, - want: &Event{ - Topic: notifierModel.PullImageTopic, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - event := &Event{} - err := event.Build(tt.args.imgPullMetadata) - if tt.wantErr { - require.NotNil(t, err, "Error: %s", err) - return - } - assert.Equal(t, tt.want.Topic, event.Topic) - }) - } -} - -func TestImageDelEvent_Build(t *testing.T) { - type args struct { - imgDelMetadata *ImageDelMetaData - } - - tests := []struct { - name string - args args - wantErr bool - want *Event - }{ - { - name: "Build Image Delete Event", - args: args{ - imgDelMetadata: &ImageDelMetaData{ - Project: &models.Project{ProjectID: 1, Name: "library"}, - Tags: []string{"v1.0"}, - OccurAt: time.Now(), - Operator: "admin", - RepoName: "library/alpine", - }, - }, - want: &Event{ - Topic: notifierModel.DeleteImageTopic, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - event := &Event{} - err := event.Build(tt.args.imgDelMetadata) - if tt.wantErr { - require.NotNil(t, err, "Error: %s", err) - return - } - assert.Equal(t, tt.want.Topic, event.Topic) - }) - } -} - func TestHookEvent_Build(t *testing.T) { type args struct { hookMetadata *HookMetaData diff --git a/src/pkg/notifier/handler/auditlog/handler.go b/src/pkg/notifier/handler/auditlog/handler.go deleted file mode 100644 index be9c9e0f0d..0000000000 --- a/src/pkg/notifier/handler/auditlog/handler.go +++ /dev/null @@ -1,50 +0,0 @@ -package auditlog - -import ( - beegoorm "github.com/astaxie/beego/orm" - "github.com/goharbor/harbor/src/common/utils/log" - "github.com/goharbor/harbor/src/internal/orm" - "github.com/goharbor/harbor/src/pkg/audit" - am "github.com/goharbor/harbor/src/pkg/audit/model" - "github.com/goharbor/harbor/src/pkg/notifier/model" -) - -// Handler - audit log handler -type Handler struct { - AuditLogMgr audit.Manager -} - -// AuditResolver - interface to resolve to AuditLog -type AuditResolver interface { - ResolveToAuditLog() (*am.AuditLog, error) -} - -// AuditHandler ... -var AuditHandler = Handler{AuditLogMgr: audit.Mgr} - -// Handle ... -func (h *Handler) Handle(value interface{}) error { - ctx := orm.NewContext(nil, beegoorm.NewOrm()) - var auditLog *am.AuditLog - switch v := value.(type) { - case *model.ProjectEvent, *model.RepositoryEvent, *model.ArtifactEvent, *model.TagEvent: - resolver := value.(AuditResolver) - al, err := resolver.ResolveToAuditLog() - if err != nil { - log.Errorf("failed to handler event %v", err) - return err - } - auditLog = al - default: - log.Errorf("Can not handler this event type! %#v", v) - } - if auditLog != nil { - h.AuditLogMgr.Create(ctx, auditLog) - } - return nil -} - -// IsStateful ... -func (h *Handler) IsStateful() bool { - return false -} diff --git a/src/pkg/notifier/handler/notification/http_handler_test.go b/src/pkg/notifier/handler/notification/http_handler_test.go index 0017315d78..1bd5874b0c 100644 --- a/src/pkg/notifier/handler/notification/http_handler_test.go +++ b/src/pkg/notifier/handler/notification/http_handler_test.go @@ -53,7 +53,7 @@ func TestHTTPHandler_Handle(t *testing.T) { args: args{ event: &event.Event{ Topic: "http", - Data: &model.ImageEvent{}, + Data: &model.EventData{}, }, }, wantErr: true, diff --git a/src/pkg/notifier/handler/notification/image_handler.go b/src/pkg/notifier/handler/notification/image_handler.go deleted file mode 100644 index f9dc124683..0000000000 --- a/src/pkg/notifier/handler/notification/image_handler.go +++ /dev/null @@ -1,18 +0,0 @@ -package notification - -// ImagePreprocessHandler preprocess image event data -type ImagePreprocessHandler struct { -} - -// Handle preprocess image event data and then publish hook event -func (h *ImagePreprocessHandler) Handle(value interface{}) error { - if err := preprocessAndSendImageHook(value); err != nil { - return err - } - return nil -} - -// IsStateful ... -func (h *ImagePreprocessHandler) IsStateful() bool { - return false -} diff --git a/src/pkg/notifier/handler/notification/image_handler_test.go b/src/pkg/notifier/handler/notification/image_handler_test.go deleted file mode 100644 index 5725df1476..0000000000 --- a/src/pkg/notifier/handler/notification/image_handler_test.go +++ /dev/null @@ -1,192 +0,0 @@ -package notification - -import ( - "os" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/goharbor/harbor/src/common/dao" - "github.com/goharbor/harbor/src/common/models" - "github.com/goharbor/harbor/src/core/config" - "github.com/goharbor/harbor/src/pkg/notification" - "github.com/goharbor/harbor/src/pkg/notifier/model" - "github.com/pkg/errors" - "github.com/stretchr/testify/require" -) - -type fakedNotificationPlyMgr struct { -} - -func (f *fakedNotificationPlyMgr) Create(*models.NotificationPolicy) (int64, error) { - return 0, nil -} - -func (f *fakedNotificationPlyMgr) List(id int64) ([]*models.NotificationPolicy, error) { - return nil, nil -} - -func (f *fakedNotificationPlyMgr) Get(id int64) (*models.NotificationPolicy, error) { - return nil, nil -} - -func (f *fakedNotificationPlyMgr) GetByNameAndProjectID(string, int64) (*models.NotificationPolicy, error) { - return nil, nil -} - -func (f *fakedNotificationPlyMgr) Update(*models.NotificationPolicy) error { - return nil -} - -func (f *fakedNotificationPlyMgr) Delete(int64) error { - return nil -} - -func (f *fakedNotificationPlyMgr) Test(*models.NotificationPolicy) error { - return nil -} - -func (f *fakedNotificationPlyMgr) GetRelatedPolices(id int64, eventType string) ([]*models.NotificationPolicy, error) { - if id == 1 { - return []*models.NotificationPolicy{ - { - ID: 1, - EventTypes: []string{ - model.EventTypePullImage, - model.EventTypePushImage, - }, - Targets: []models.EventTarget{ - { - Type: "http", - Address: "http://127.0.0.1:8080", - }, - }, - }, - }, nil - } - if id == 2 { - return nil, nil - } - return nil, errors.New("") -} - -func TestMain(m *testing.M) { - dao.PrepareTestForPostgresSQL() - os.Exit(m.Run()) -} - -func TestImagePreprocessHandler_Handle(t *testing.T) { - PolicyMgr := notification.PolicyMgr - defer func() { - notification.PolicyMgr = PolicyMgr - }() - notification.PolicyMgr = &fakedNotificationPlyMgr{} - - handler := &ImagePreprocessHandler{} - config.Init() - - type args struct { - data interface{} - } - tests := []struct { - name string - args args - wantErr bool - }{ - { - name: "ImagePreprocessHandler Want Error 1", - args: args{ - data: nil, - }, - wantErr: true, - }, - { - name: "ImagePreprocessHandler Want Error 2", - args: args{ - data: &model.ImageEvent{}, - }, - wantErr: true, - }, - { - name: "ImagePreprocessHandler Want Error 3", - args: args{ - data: &model.ImageEvent{ - Resource: []*model.ImgResource{ - { - Tag: "v1.0", - }, - }, - Project: &models.Project{ - ProjectID: 3, - }, - }, - }, - wantErr: true, - }, - { - name: "ImagePreprocessHandler Want Error 4", - args: args{ - data: &model.ImageEvent{ - Resource: []*model.ImgResource{ - { - Tag: "v1.0", - }, - }, - Project: &models.Project{ - ProjectID: 1, - }, - }, - }, - wantErr: true, - }, - // No handlers registered for handling topic http - { - name: "ImagePreprocessHandler Want Error 5", - args: args{ - data: &model.ImageEvent{ - RepoName: "test/alpine", - Resource: []*model.ImgResource{ - { - Tag: "v1.0", - }, - }, - Project: &models.Project{ - ProjectID: 1, - }, - }, - }, - wantErr: true, - }, - { - name: "ImagePreprocessHandler 2", - args: args{ - data: &model.ImageEvent{ - Resource: []*model.ImgResource{ - { - Tag: "v1.0", - }, - }, - Project: &models.Project{ - ProjectID: 2, - }, - }, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := handler.Handle(tt.args.data) - if tt.wantErr { - require.NotNil(t, err, "Error: %s", err) - return - } - assert.Nil(t, err) - }) - } -} - -func TestImagePreprocessHandler_IsStateful(t *testing.T) { - handler := &ImagePreprocessHandler{} - assert.False(t, handler.IsStateful()) -} diff --git a/src/pkg/notifier/handler/notification/processor.go b/src/pkg/notifier/handler/notification/processor.go deleted file mode 100644 index 4237c02063..0000000000 --- a/src/pkg/notifier/handler/notification/processor.go +++ /dev/null @@ -1,196 +0,0 @@ -package notification - -import ( - "errors" - "fmt" - "strings" - - "github.com/goharbor/harbor/src/common/dao" - "github.com/goharbor/harbor/src/common/models" - "github.com/goharbor/harbor/src/common/utils/log" - "github.com/goharbor/harbor/src/core/config" - "github.com/goharbor/harbor/src/pkg/notification" - "github.com/goharbor/harbor/src/pkg/notifier/event" - notifyModel "github.com/goharbor/harbor/src/pkg/notifier/model" -) - -// getNameFromImgRepoFullName gets image name from repo full name with format `repoName/imageName` -func getNameFromImgRepoFullName(repo string) string { - idx := strings.Index(repo, "/") - return repo[idx+1:] -} - -func buildImageResourceURL(extURL, repoName, tag string) (string, error) { - resURL := fmt.Sprintf("%s/%s:%s", extURL, repoName, tag) - return resURL, nil -} - -func constructImagePayload(event *notifyModel.ImageEvent) (*notifyModel.Payload, error) { - repoName := event.RepoName - if repoName == "" { - return nil, fmt.Errorf("invalid %s event with empty repo name", event.EventType) - } - - repoType := models.ProjectPrivate - if event.Project.IsPublic() { - repoType = models.ProjectPublic - } - - imageName := getNameFromImgRepoFullName(repoName) - - payload := ¬ifyModel.Payload{ - Type: event.EventType, - OccurAt: event.OccurAt.Unix(), - EventData: ¬ifyModel.EventData{ - Repository: ¬ifyModel.Repository{ - Name: imageName, - Namespace: event.Project.Name, - RepoFullName: repoName, - RepoType: repoType, - }, - }, - Operator: event.Operator, - } - - repoRecord, err := dao.GetRepositoryByName(repoName) - if err != nil { - log.Errorf("failed to get repository with name %s: %v", repoName, err) - return nil, err - } - // once repo has been delete, cannot ensure to get repo record - if repoRecord == nil { - log.Debugf("cannot find repository info with repo %s", repoName) - } else { - payload.EventData.Repository.DateCreated = repoRecord.CreationTime.Unix() - } - - extURL, err := config.ExtURL() - if err != nil { - return nil, fmt.Errorf("get external endpoint failed: %v", err) - } - - for _, res := range event.Resource { - tag := res.Tag - digest := res.Digest - - if tag == "" { - log.Errorf("invalid notification event with empty tag: %v", event) - continue - } - - resURL, err := buildImageResourceURL(extURL, event.RepoName, tag) - if err != nil { - log.Errorf("get resource URL failed: %v", err) - continue - } - - resource := ¬ifyModel.Resource{ - Tag: tag, - Digest: digest, - ResourceURL: resURL, - } - payload.EventData.Resources = append(payload.EventData.Resources, resource) - } - - return payload, nil -} - -// send hook by publishing topic of specified target type(notify type) -func sendHookWithPolicies(policies []*models.NotificationPolicy, payload *notifyModel.Payload, eventType string) error { - errRet := false - for _, ply := range policies { - targets := ply.Targets - for _, target := range targets { - evt := &event.Event{} - hookMetadata := &event.HookMetaData{ - EventType: eventType, - PolicyID: ply.ID, - Payload: payload, - Target: &target, - } - // It should never affect evaluating other policies when one is failed, but error should return - if err := evt.Build(hookMetadata); err == nil { - if err := evt.Publish(); err != nil { - errRet = true - log.Errorf("failed to publish hook notify event: %v", err) - } - } else { - errRet = true - log.Errorf("failed to build hook notify event metadata: %v", err) - } - log.Debugf("published image event %s by topic %s", payload.Type, target.Type) - } - } - if errRet { - return errors.New("failed to trigger some of the events") - } - return nil -} - -func resolveImageEventData(value interface{}) (*notifyModel.ImageEvent, error) { - imgEvent, ok := value.(*notifyModel.ImageEvent) - if !ok || imgEvent == nil { - return nil, errors.New("invalid image event") - } - - if len(imgEvent.Resource) == 0 { - return nil, fmt.Errorf("empty event resouece data in image event: %v", imgEvent) - } - - return imgEvent, nil -} - -func resolveTagEventToImageEvent(value interface{}) (*notifyModel.ImageEvent, error) { - tagEvent, ok := value.(*notifyModel.TagEvent) - if !ok || tagEvent == nil { - return nil, errors.New("invalid image event") - } - imageEvent := notifyModel.ImageEvent{ - EventType: notifyModel.PushImageTopic, - Project: tagEvent.Project, - RepoName: tagEvent.RepoName, - Resource: []*notifyModel.ImgResource{ - {Tag: tagEvent.TagName}, - }, - OccurAt: tagEvent.OccurAt, - Operator: tagEvent.Operator, - } - return &imageEvent, nil -} - -// preprocessAndSendImageHook preprocess image event data and send hook by notification policy target -func preprocessAndSendImageHook(value interface{}) error { - // if global notification configured disabled, return directly - if !config.NotificationEnable() { - log.Debug("notification feature is not enabled") - return nil - } - - imgEvent, err := resolveImageEventData(value) - if err != nil { - return err - } - - policies, err := notification.PolicyMgr.GetRelatedPolices(imgEvent.Project.ProjectID, imgEvent.EventType) - if err != nil { - log.Errorf("failed to find policy for %s event: %v", imgEvent.EventType, err) - return err - } - // if cannot find policy including event type in project, return directly - if len(policies) == 0 { - log.Debugf("cannot find policy for %s event: %v", imgEvent.EventType, imgEvent) - return nil - } - - payload, err := constructImagePayload(imgEvent) - if err != nil { - return err - } - - err = sendHookWithPolicies(policies, payload, imgEvent.EventType) - if err != nil { - return err - } - - return nil -} diff --git a/src/pkg/notifier/handler/notification/slack_handler_test.go b/src/pkg/notifier/handler/notification/slack_handler_test.go index 6fc726c1df..d9316e32a7 100644 --- a/src/pkg/notifier/handler/notification/slack_handler_test.go +++ b/src/pkg/notifier/handler/notification/slack_handler_test.go @@ -45,7 +45,7 @@ func TestSlackHandler_Handle(t *testing.T) { args: args{ event: &event.Event{ Topic: "slack", - Data: &model.ImageEvent{}, + Data: &model.EventData{}, }, }, wantErr: true, diff --git a/src/pkg/notifier/model/const.go b/src/pkg/notifier/model/const.go index 386f9132fd..a4df057542 100644 --- a/src/pkg/notifier/model/const.go +++ b/src/pkg/notifier/model/const.go @@ -2,17 +2,6 @@ package model // const definitions const ( - EventTypePushImage = "pushImage" - EventTypePullImage = "pullImage" - EventTypeDeleteImage = "deleteImage" - EventTypeUploadChart = "uploadChart" - EventTypeDeleteChart = "deleteChart" - EventTypeDownloadChart = "downloadChart" - EventTypeScanningCompleted = "scanningCompleted" - EventTypeScanningFailed = "scanningFailed" - EventTypeTestEndpoint = "testEndpoint" - EventTypeProjectQuota = "projectQuota" - NotifyTypeHTTP = "http" NotifyTypeSlack = "slack" ) diff --git a/src/pkg/notifier/model/event.go b/src/pkg/notifier/model/event.go index c1973c0434..84f8a9a879 100755 --- a/src/pkg/notifier/model/event.go +++ b/src/pkg/notifier/model/event.go @@ -1,60 +1,9 @@ package model import ( - "time" - - "fmt" - "github.com/goharbor/harbor/src/common/models" - "github.com/goharbor/harbor/src/common/utils/log" - "github.com/goharbor/harbor/src/pkg/audit/model" - v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" ) -// ImageEvent is image related event data to publish -type ImageEvent struct { - EventType string - Project *models.Project - Resource []*ImgResource - OccurAt time.Time - Operator string - RepoName string -} - -// ImgResource include image digest and tag -type ImgResource struct { - Digest string - Tag string -} - -// ChartEvent is chart related event data to publish -type ChartEvent struct { - EventType string - ProjectName string - ChartName string - Versions []string - OccurAt time.Time - Operator string -} - -// ScanImageEvent is scanning image related event data to publish -type ScanImageEvent struct { - EventType string - Artifact *v1.Artifact - OccurAt time.Time - Operator string -} - -// QuotaEvent is project quota related event data to publish -type QuotaEvent struct { - EventType string - Project *models.Project - Resource *ImgResource - OccurAt time.Time - RepoName string - Msg string -} - // HookEvent is hook related event data to publish type HookEvent struct { PolicyID int64 @@ -63,144 +12,6 @@ type HookEvent struct { Payload *Payload } -// ProjectEvent info of Project related event -type ProjectEvent struct { - // EventType - create/delete event - TargetTopic string - Project *models.Project - OccurAt time.Time - Operator string - Operation string -} - -// ResolveToAuditLog ... -func (p *ProjectEvent) ResolveToAuditLog() (*model.AuditLog, error) { - auditLog := &model.AuditLog{ - ProjectID: p.Project.ProjectID, - OpTime: p.OccurAt, - Operation: p.Operation, - Username: p.Operator, - ResourceType: "project", - Resource: fmt.Sprintf("/api/project/%v", - p.Project.ProjectID)} - return auditLog, nil -} - -// Topic ... -func (p *ProjectEvent) Topic() string { - return p.TargetTopic -} - -// RepositoryEvent info of repository related event -type RepositoryEvent struct { - TargetTopic string - Project *models.Project - RepoName string - OccurAt time.Time - Operator string - Operation string -} - -// ResolveToAuditLog ... -func (r *RepositoryEvent) ResolveToAuditLog() (*model.AuditLog, error) { - auditLog := &model.AuditLog{ - ProjectID: r.Project.ProjectID, - OpTime: r.OccurAt, - Operation: r.Operation, - Username: r.Operator, - ResourceType: "repository", - Resource: fmt.Sprintf("/api/project/%v/repository/%v", - r.Project.ProjectID, r.RepoName)} - return auditLog, nil -} - -// Topic ... -func (r *RepositoryEvent) Topic() string { - return r.TargetTopic -} - -// ArtifactEvent info of artifact related event -type ArtifactEvent struct { - TargetTopic string - Project *models.Project - RepoName string - Digest string - OccurAt time.Time - Operator string - Operation string -} - -// ResolveToAuditLog ... -func (a *ArtifactEvent) ResolveToAuditLog() (*model.AuditLog, error) { - auditLog := &model.AuditLog{ - ProjectID: a.Project.ProjectID, - OpTime: a.OccurAt, - Operation: a.Operation, - Username: a.Operator, - ResourceType: "artifact", - Resource: fmt.Sprintf("/api/project/%v/repository/%v/artifact/%v", - a.Project.ProjectID, a.RepoName, a.Digest)} - return auditLog, nil -} - -// Topic ... -func (a *ArtifactEvent) Topic() string { - return a.TargetTopic -} - -// TagEvent info of tag related event -type TagEvent struct { - TargetTopic string - Project *models.Project - RepoName string - TagName string - Digest string - OccurAt time.Time - Operation string - Operator string -} - -// ResolveToAuditLog ... -func (t *TagEvent) ResolveToAuditLog() (*model.AuditLog, error) { - auditLog := &model.AuditLog{ - ProjectID: t.Project.ProjectID, - OpTime: t.OccurAt, - Operation: t.Operation, - Username: t.Operator, - ResourceType: "tag", - Resource: fmt.Sprintf("/api/project/%v/repository/%v/tag/%v", - t.Project.ProjectID, t.RepoName, t.TagName)} - log.Infof("create audit log %+v", auditLog) - return auditLog, nil -} - -// Topic ... -func (t *TagEvent) Topic() string { - return t.TargetTopic -} - -// ToImageEvent ... -func (t *TagEvent) ToImageEvent() *ImageEvent { - var eventType string - // convert tag operation to previous event type so that webhook can handle it - if t.Operation == "push" { - eventType = EventTypePushImage - } else if t.Operation == "pull" { - eventType = EventTypePullImage - } else if t.Operation == "delete" { - eventType = EventTypeDeleteImage - } - imgEvent := &ImageEvent{ - EventType: eventType, - Project: t.Project, - Resource: []*ImgResource{{Tag: t.TagName}}, - Operator: t.Operator, - OccurAt: t.OccurAt, - RepoName: t.RepoName, - } - return imgEvent -} - // Payload of notification event type Payload struct { Type string `json:"type"` diff --git a/src/pkg/notifier/model/topic.go b/src/pkg/notifier/model/topic.go index 0bab0e8f6c..54ea05773d 100644 --- a/src/pkg/notifier/model/topic.go +++ b/src/pkg/notifier/model/topic.go @@ -2,44 +2,6 @@ package model // Define global topic names const ( - // TagTopic - PushTagTopic = "PushTagTopic" - PullTagTopic = "PullTagTopic" - DeleteTagTopic = "DeleteTagTopic" - - // ProjectTopic ... - CreateProjectTopic = "CreateProjectTopic" - DeleteProjectTopic = "DeleteProjectTopic" - - // RepositoryTopic ... - CreateRepositoryTopic = "CreateRepositoryTopic" - DeleteRepositoryTopic = "DeleteRepositoryTopic" - - // ArtifactTopic - CreateArtifactTopic = "CreateArtifactTopic" - DeleteArtifactTopic = "DeleteArtifactTopic" - - // PushImageTopic is topic for push image event - PushImageTopic = "OnPushImage" - // PullImageTopic is topic for pull image event - PullImageTopic = "OnPullImage" - // DeleteImageTopic is topic for delete image event - DeleteImageTopic = "OnDeleteImage" - // UploadChartTopic is topic for upload chart event - UploadChartTopic = "OnUploadChart" - // DownloadChartTopic is topic for download chart event - DownloadChartTopic = "OnDownloadChart" - // DeleteChartTopic is topic for delete chart event - DeleteChartTopic = "OnDeleteChart" - // ScanningFailedTopic is topic for scanning failed event - ScanningFailedTopic = "OnScanningFailed" - // ScanningCompletedTopic is topic for scanning completed event - ScanningCompletedTopic = "OnScanningCompleted" - // QuotaExceedTopic is topic for quota warning event, the usage reaches the warning bar of limitation, like 85% - QuotaWarningTopic = "OnQuotaWarning" - // QuotaExceedTopic is topic for quota exceeded event - QuotaExceedTopic = "OnQuotaExceed" - // WebhookTopic is topic for sending webhook payload WebhookTopic = "http" // SlackTopic is topic for sending slack payload diff --git a/src/pkg/notifier/topic/topics.go b/src/pkg/notifier/topic/topics.go index c6b6430862..5b1725d8fc 100644 --- a/src/pkg/notifier/topic/topics.go +++ b/src/pkg/notifier/topic/topics.go @@ -3,7 +3,6 @@ package topic import ( "github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/pkg/notifier" - "github.com/goharbor/harbor/src/pkg/notifier/handler/auditlog" "github.com/goharbor/harbor/src/pkg/notifier/handler/notification" "github.com/goharbor/harbor/src/pkg/notifier/model" ) @@ -11,27 +10,8 @@ import ( // Subscribe topics func init() { handlersMap := map[string][]notifier.NotificationHandler{ - model.PushTagTopic: {&auditlog.AuditHandler}, - model.PullTagTopic: {&auditlog.AuditHandler}, - model.DeleteTagTopic: {&auditlog.AuditHandler}, - - model.CreateProjectTopic: {&auditlog.AuditHandler}, - model.DeleteProjectTopic: {&auditlog.AuditHandler}, - - model.CreateRepositoryTopic: {&auditlog.AuditHandler}, - model.DeleteRepositoryTopic: {&auditlog.AuditHandler}, - - model.CreateArtifactTopic: {&auditlog.AuditHandler}, - model.DeleteArtifactTopic: {&auditlog.AuditHandler}, - - model.WebhookTopic: {¬ification.HTTPHandler{}}, - model.UploadChartTopic: {¬ification.ChartPreprocessHandler{}}, - model.DownloadChartTopic: {¬ification.ChartPreprocessHandler{}}, - model.DeleteChartTopic: {¬ification.ChartPreprocessHandler{}}, - model.ScanningCompletedTopic: {¬ification.ScanImagePreprocessHandler{}}, - model.ScanningFailedTopic: {¬ification.ScanImagePreprocessHandler{}}, - model.QuotaExceedTopic: {¬ification.QuotaPreprocessHandler{}}, - model.SlackTopic: {¬ification.SlackHandler{}}, + model.WebhookTopic: {¬ification.HTTPHandler{}}, + model.SlackTopic: {¬ification.SlackHandler{}}, } for t, handlers := range handlersMap { diff --git a/src/pkg/scan/event/notification.go b/src/pkg/scan/event/notification.go deleted file mode 100644 index 9fa8f0c24a..0000000000 --- a/src/pkg/scan/event/notification.go +++ /dev/null @@ -1,101 +0,0 @@ -// 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 event - -import ( - "context" - bo "github.com/astaxie/beego/orm" - "github.com/goharbor/harbor/src/api/artifact" - "github.com/goharbor/harbor/src/api/scan" - "github.com/goharbor/harbor/src/common/utils/log" - "github.com/goharbor/harbor/src/internal/orm" - "github.com/goharbor/harbor/src/pkg/notifier" - "github.com/goharbor/harbor/src/pkg/notifier/model" - "github.com/goharbor/harbor/src/pkg/q" - "github.com/pkg/errors" -) - -// scanCtlGetter for getting a scan controller reference to avoid package importing order issue. -type scanCtlGetter func() scan.Controller - -// artCtlGetter for getting a artifact controller reference to avoid package importing order issue. -type artCtlGetter func() artifact.Controller - -// onDelImageHandler is a handler to listen to the internal delete image event. -type onDelImageHandler struct { - // scan controller - scanCtl scanCtlGetter - // artifact controller - artCtl artCtlGetter -} - -// NewOnDelImageHandler creates a new handler to handle on del event. -func NewOnDelImageHandler() notifier.NotificationHandler { - return &onDelImageHandler{ - scanCtl: func() scan.Controller { - return scan.DefaultController - }, - artCtl: func() artifact.Controller { - return artifact.Ctl - }, - } -} - -func (o *onDelImageHandler) Handle(value interface{}) error { - if value == nil { - return errors.New("delete image event handler: nil value ") - } - - evt, ok := value.(*model.ImageEvent) - if !ok { - return errors.New("delete image event handler: malformed image event model") - } - - log.Debugf("clear the scan reports as receiving event %s", evt.EventType) - - digests := make([]string, 0) - query := &q.Query{ - Keywords: make(map[string]interface{}), - } - - ctx := orm.NewContext(context.TODO(), bo.NewOrm()) - for _, res := range evt.Resource { - // Check if it is safe to delete the reports. - query.Keywords["digest"] = res.Digest - l, err := o.artCtl().List(ctx, query, nil) - - if err != nil { - // Just logged - log.Error(errors.Wrap(err, "delete image event handler")) - // Passed for safe consideration - continue - } - - if len(l) == 0 { - digests = append(digests, res.Digest) - log.Debugf("prepare to remove the scan report linked with artifact: %s", res.Digest) - } - } - - if err := o.scanCtl().DeleteReports(digests...); err != nil { - return errors.Wrap(err, "delete image event handler") - } - - return nil -} - -func (o *onDelImageHandler) IsStateful() bool { - return false -} diff --git a/src/server/middleware/notification/notification_test.go b/src/server/middleware/notification/notification_test.go index ee07931d6e..5af00a6a03 100644 --- a/src/server/middleware/notification/notification_test.go +++ b/src/server/middleware/notification/notification_test.go @@ -3,7 +3,7 @@ package notification import ( "context" "fmt" - "github.com/goharbor/harbor/src/api/event" + "github.com/goharbor/harbor/src/api/event/metadata" pkg_art "github.com/goharbor/harbor/src/pkg/artifact" "github.com/goharbor/harbor/src/pkg/notification" "github.com/stretchr/testify/suite" @@ -20,7 +20,7 @@ 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{ + notification.AddEvent(r.Context(), &metadata.DeleteArtifactEventMetadata{ Ctx: context.Background(), Artifact: &pkg_art.Artifact{ ProjectID: 1, @@ -42,7 +42,7 @@ 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{ + notification.AddEvent(r.Context(), &metadata.DeleteArtifactEventMetadata{ Ctx: context.Background(), Artifact: &pkg_art.Artifact{ ProjectID: 1, diff --git a/src/server/registry/manifest.go b/src/server/registry/manifest.go index 4896eafa0c..c67ae6b40b 100644 --- a/src/server/registry/manifest.go +++ b/src/server/registry/manifest.go @@ -16,12 +16,12 @@ package registry import ( "github.com/goharbor/harbor/src/api/artifact" - "github.com/goharbor/harbor/src/api/event" + "github.com/goharbor/harbor/src/api/event/metadata" "github.com/goharbor/harbor/src/api/repository" "github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/internal" ierror "github.com/goharbor/harbor/src/internal/error" - evt "github.com/goharbor/harbor/src/pkg/notifier/event" + "github.com/goharbor/harbor/src/pkg/notification" serror "github.com/goharbor/harbor/src/server/error" "github.com/goharbor/harbor/src/server/router" "github.com/opencontainers/go-digest" @@ -51,7 +51,7 @@ func getManifest(w http.ResponseWriter, req *http.Request) { // fire event if recorder.Success() { // TODO don't fire event for the pulling from replication - e := &event.PullArtifactEventMetadata{ + e := &metadata.PullArtifactEventMetadata{ Ctx: req.Context(), Artifact: &artifact.Artifact, } @@ -60,7 +60,7 @@ func getManifest(w http.ResponseWriter, req *http.Request) { if _, err = digest.Parse(reference); err != nil { e.Tag = reference } - evt.BuildAndPublish(e) + notification.AddEvent(req.Context(), e) } } diff --git a/src/server/v2.0/handler/artifact.go b/src/server/v2.0/handler/artifact.go index 4ac1614faf..8a4f2e373a 100644 --- a/src/server/v2.0/handler/artifact.go +++ b/src/server/v2.0/handler/artifact.go @@ -17,6 +17,8 @@ package handler import ( "context" "fmt" + "github.com/goharbor/harbor/src/api/event/metadata" + "github.com/goharbor/harbor/src/pkg/notification" "net/http" "strings" "time" @@ -26,14 +28,12 @@ import ( "github.com/go-openapi/runtime/middleware" "github.com/goharbor/harbor/src/api/artifact" "github.com/goharbor/harbor/src/api/artifact/processor" - "github.com/goharbor/harbor/src/api/event" "github.com/goharbor/harbor/src/api/repository" "github.com/goharbor/harbor/src/api/scan" "github.com/goharbor/harbor/src/api/tag" "github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/utils" ierror "github.com/goharbor/harbor/src/internal/error" - evt "github.com/goharbor/harbor/src/pkg/notifier/event" "github.com/goharbor/harbor/src/server/v2.0/handler/assembler" "github.com/goharbor/harbor/src/server/v2.0/handler/model" "github.com/goharbor/harbor/src/server/v2.0/models" @@ -218,7 +218,7 @@ func (a *artifactAPI) CreateTag(ctx context.Context, params operation.CreateTagP } // fire event - evt.BuildAndPublish(&event.CreateTagEventMetadata{ + notification.AddEvent(ctx, &metadata.CreateTagEventMetadata{ Ctx: ctx, Tag: tag.Name, AttachedArtifact: &art.Artifact, @@ -257,7 +257,7 @@ func (a *artifactAPI) DeleteTag(ctx context.Context, params operation.DeleteTagP } // fire event - evt.BuildAndPublish(&event.DeleteTagEventMetadata{ + notification.AddEvent(ctx, &metadata.DeleteTagEventMetadata{ Ctx: ctx, Tag: params.TagName, AttachedArtifact: &artifact.Artifact, diff --git a/src/server/v2.0/handler/repository.go b/src/server/v2.0/handler/repository.go index 2aec0bf142..cb7cd2b9c8 100644 --- a/src/server/v2.0/handler/repository.go +++ b/src/server/v2.0/handler/repository.go @@ -17,16 +17,16 @@ package handler import ( "context" "fmt" + "github.com/goharbor/harbor/src/api/event/metadata" + "github.com/goharbor/harbor/src/pkg/notification" "github.com/go-openapi/runtime/middleware" "github.com/goharbor/harbor/src/api/artifact" - "github.com/goharbor/harbor/src/api/event" "github.com/goharbor/harbor/src/api/project" "github.com/goharbor/harbor/src/api/repository" cmodels "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/utils/log" - evt "github.com/goharbor/harbor/src/pkg/notifier/event" "github.com/goharbor/harbor/src/pkg/q" "github.com/goharbor/harbor/src/server/v2.0/models" operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/repository" @@ -145,7 +145,7 @@ func (r *repositoryAPI) DeleteRepository(ctx context.Context, params operation.D } // fire event - evt.BuildAndPublish(&event.DeleteRepositoryEventMetadata{ + notification.AddEvent(ctx, &metadata.DeleteRepositoryEventMetadata{ Ctx: ctx, Repository: repository.Name, }) diff --git a/src/testing/pkg/notification/manager.go b/src/testing/pkg/notification/manager.go new file mode 100644 index 0000000000..0634032f2c --- /dev/null +++ b/src/testing/pkg/notification/manager.go @@ -0,0 +1,62 @@ +package notification + +import ( + "github.com/goharbor/harbor/src/api/event" + "github.com/goharbor/harbor/src/common/models" +) + +type FakedPolicyMgr struct { +} + +func (f *FakedPolicyMgr) Create(*models.NotificationPolicy) (int64, error) { + return 0, nil +} + +func (f *FakedPolicyMgr) List(id int64) ([]*models.NotificationPolicy, error) { + return nil, nil +} + +func (f *FakedPolicyMgr) Get(id int64) (*models.NotificationPolicy, error) { + return nil, nil +} + +func (f *FakedPolicyMgr) GetByNameAndProjectID(string, int64) (*models.NotificationPolicy, error) { + return nil, nil +} + +func (f *FakedPolicyMgr) Update(*models.NotificationPolicy) error { + return nil +} + +func (f *FakedPolicyMgr) Delete(int64) error { + return nil +} + +func (f *FakedPolicyMgr) Test(*models.NotificationPolicy) error { + return nil +} + +func (f *FakedPolicyMgr) GetRelatedPolices(id int64, eventType string) ([]*models.NotificationPolicy, error) { + return []*models.NotificationPolicy{ + { + ID: 1, + EventTypes: []string{ + event.TopicUploadChart, + event.TopicDownloadChart, + event.TopicDeleteChart, + event.TopicPushArtifact, + event.TopicPullArtifact, + event.TopicDeleteArtifact, + event.TopicScanningFailed, + event.TopicScanningCompleted, + event.TopicQuotaExceed, + }, + Targets: []models.EventTarget{ + { + Type: "http", + Address: "http://127.0.0.1:8080", + }, + }, + }, + }, nil +}