mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-24 01:27:49 +01:00
Merge pull request #9865 from wy65701436/quota-event
add quota exceed event imple
This commit is contained in:
commit
88773436c9
@ -59,6 +59,7 @@ func (h *countQuotaHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request)
|
|||||||
if err := interceptor.HandleRequest(req); err != nil {
|
if err := interceptor.HandleRequest(req); err != nil {
|
||||||
log.Warningf("Error occurred when to handle request in count quota handler: %v", err)
|
log.Warningf("Error occurred when to handle request in count quota handler: %v", err)
|
||||||
if _, ok := err.(quota.Errors); ok {
|
if _, ok := err.(quota.Errors); ok {
|
||||||
|
util.FireQuotaEvent(req, 1, err.Error())
|
||||||
http.Error(rw, util.MarshalError("DENIED", fmt.Sprintf("Quota exceeded when processing the request of %v", err)), http.StatusForbidden)
|
http.Error(rw, util.MarshalError("DENIED", fmt.Sprintf("Quota exceeded when processing the request of %v", err)), http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -59,6 +59,7 @@ func (h *sizeQuotaHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request)
|
|||||||
if err := interceptor.HandleRequest(req); err != nil {
|
if err := interceptor.HandleRequest(req); err != nil {
|
||||||
log.Warningf("Error occurred when to handle request in size quota handler: %v", err)
|
log.Warningf("Error occurred when to handle request in size quota handler: %v", err)
|
||||||
if _, ok := err.(quota.Errors); ok {
|
if _, ok := err.(quota.Errors); ok {
|
||||||
|
util.FireQuotaEvent(req, 1, err.Error())
|
||||||
http.Error(rw, util.MarshalError("DENIED", fmt.Sprintf("Quota exceeded when processing the request of %v", err)), http.StatusForbidden)
|
http.Error(rw, util.MarshalError("DENIED", fmt.Sprintf("Quota exceeded when processing the request of %v", err)), http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,8 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/common/utils"
|
"github.com/goharbor/harbor/src/common/utils"
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
"github.com/goharbor/harbor/src/core/config"
|
"github.com/goharbor/harbor/src/core/config"
|
||||||
|
"github.com/goharbor/harbor/src/core/filter"
|
||||||
|
notifierEvt "github.com/goharbor/harbor/src/core/notifier/event"
|
||||||
"github.com/goharbor/harbor/src/core/promgr"
|
"github.com/goharbor/harbor/src/core/promgr"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/vuln"
|
"github.com/goharbor/harbor/src/pkg/scan/vuln"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/whitelist"
|
"github.com/goharbor/harbor/src/pkg/scan/whitelist"
|
||||||
@ -563,6 +565,49 @@ func ParseManifestInfoFromPath(req *http.Request) (*ManifestInfo, error) {
|
|||||||
return info, nil
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FireQuotaEvent ...
|
||||||
|
func FireQuotaEvent(req *http.Request, level int, msg string) {
|
||||||
|
go func() {
|
||||||
|
info, err := ParseManifestInfoFromReq(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Quota exceed event: failed to get manifest from request: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pm, err := filter.GetProjectManager(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Quota exceed event: failed to get project manager: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
project, err := pm.Get(info.ProjectID)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(fmt.Sprintf("Quota exceed event: failed to get the project %d", info.ProjectID), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if project == nil {
|
||||||
|
log.Errorf(fmt.Sprintf("Quota exceed event: no project found %d", info.ProjectID), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
evt := ¬ifierEvt.Event{}
|
||||||
|
quotaMetadata := ¬ifierEvt.QuotaMetaData{
|
||||||
|
Project: project,
|
||||||
|
Tag: info.Tag,
|
||||||
|
Digest: info.Digest,
|
||||||
|
RepoName: info.Repository,
|
||||||
|
Level: level,
|
||||||
|
Msg: msg,
|
||||||
|
OccurAt: time.Now(),
|
||||||
|
}
|
||||||
|
if err := evt.Build(quotaMetadata); err == nil {
|
||||||
|
if err := evt.Publish(); err != nil {
|
||||||
|
log.Errorf("failed to publish quota event: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Errorf("failed to build quota event metadata: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
func getProjectVulnSeverity(project *models.Project) vuln.Severity {
|
func getProjectVulnSeverity(project *models.Project) vuln.Severity {
|
||||||
mp := map[string]vuln.Severity{
|
mp := map[string]vuln.Severity{
|
||||||
models.SeverityNegligible: vuln.Negligible,
|
models.SeverityNegligible: vuln.Negligible,
|
||||||
|
@ -222,6 +222,48 @@ func (si *ScanImageMetaData) Resolve(evt *Event) error {
|
|||||||
return nil
|
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
|
// HookMetaData defines hook notification related event data
|
||||||
type HookMetaData struct {
|
type HookMetaData struct {
|
||||||
PolicyID int64
|
PolicyID int64
|
||||||
|
103
src/core/notifier/handler/notification/quota_handler.go
Normal file
103
src/core/notifier/handler/notification/quota_handler.go
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
package notification
|
||||||
|
|
||||||
|
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/core/notifier/model"
|
||||||
|
notifyModel "github.com/goharbor/harbor/src/core/notifier/model"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/notification"
|
||||||
|
)
|
||||||
|
|
||||||
|
// QuotaPreprocessHandler preprocess image event data
|
||||||
|
type QuotaPreprocessHandler 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)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("invalid quota event type")
|
||||||
|
}
|
||||||
|
if quotaEvent == nil {
|
||||||
|
return fmt.Errorf("nil quota event")
|
||||||
|
}
|
||||||
|
|
||||||
|
project, err := config.GlobalProjectMgr.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)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(policies) == 0 {
|
||||||
|
log.Debugf("cannot find policy for %s event: %v", quotaEvent.EventType, quotaEvent)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := constructQuotaPayload(quotaEvent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = sendHookWithPolicies(policies, payload, quotaEvent.EventType)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsStateful ...
|
||||||
|
func (qp *QuotaPreprocessHandler) IsStateful() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func constructQuotaPayload(event *model.QuotaEvent) (*model.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)
|
||||||
|
quotaCustom := make(map[string]string)
|
||||||
|
quotaCustom["Details"] = event.Msg
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
Custom: quotaCustom,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resource := ¬ifyModel.Resource{
|
||||||
|
Tag: event.Resource.Tag,
|
||||||
|
Digest: event.Resource.Digest,
|
||||||
|
}
|
||||||
|
payload.EventData.Resources = append(payload.EventData.Resources, resource)
|
||||||
|
|
||||||
|
return payload, nil
|
||||||
|
}
|
87
src/core/notifier/handler/notification/quota_handler_test.go
Normal file
87
src/core/notifier/handler/notification/quota_handler_test.go
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
package notification
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/common"
|
||||||
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
|
"github.com/goharbor/harbor/src/core/config"
|
||||||
|
"github.com/goharbor/harbor/src/core/notifier"
|
||||||
|
"github.com/goharbor/harbor/src/core/notifier/model"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/notification"
|
||||||
|
nm "github.com/goharbor/harbor/src/pkg/notification/model"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/notification/policy"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// QuotaPreprocessHandlerSuite ...
|
||||||
|
type QuotaPreprocessHandlerSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
om policy.Manager
|
||||||
|
evt *model.QuotaEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestQuotaPreprocessHandler ...
|
||||||
|
func TestQuotaPreprocessHandler(t *testing.T) {
|
||||||
|
suite.Run(t, &QuotaPreprocessHandlerSuite{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupSuite prepares env for test suite.
|
||||||
|
func (suite *QuotaPreprocessHandlerSuite) SetupSuite() {
|
||||||
|
cfg := map[string]interface{}{
|
||||||
|
common.NotificationEnable: true,
|
||||||
|
}
|
||||||
|
config.InitWithSettings(cfg)
|
||||||
|
|
||||||
|
res := &model.ImgResource{
|
||||||
|
Digest: "sha256:abcd",
|
||||||
|
Tag: "latest",
|
||||||
|
}
|
||||||
|
suite.evt = &model.QuotaEvent{
|
||||||
|
EventType: nm.EventTypeProjectQuota,
|
||||||
|
OccurAt: time.Now().UTC(),
|
||||||
|
RepoName: "hello-world",
|
||||||
|
Resource: res,
|
||||||
|
Project: &models.Project{
|
||||||
|
ProjectID: 1,
|
||||||
|
Name: "library",
|
||||||
|
},
|
||||||
|
Msg: "this is a testing quota event",
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.om = notification.PolicyMgr
|
||||||
|
mp := &fakedPolicyMgr{}
|
||||||
|
notification.PolicyMgr = mp
|
||||||
|
|
||||||
|
h := &MockHandler{}
|
||||||
|
|
||||||
|
err := notifier.Subscribe(model.WebhookTopic, h)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TearDownSuite ...
|
||||||
|
func (suite *QuotaPreprocessHandlerSuite) TearDownSuite() {
|
||||||
|
notification.PolicyMgr = suite.om
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandle ...
|
||||||
|
func (suite *QuotaPreprocessHandlerSuite) TestHandle() {
|
||||||
|
handler := &QuotaPreprocessHandler{}
|
||||||
|
err := handler.Handle(suite.evt)
|
||||||
|
suite.NoError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockHandler ...
|
||||||
|
type MockHandler struct{}
|
||||||
|
|
||||||
|
// Handle ...
|
||||||
|
func (m *MockHandler) Handle(value interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsStateful ...
|
||||||
|
func (m *MockHandler) IsStateful() bool {
|
||||||
|
return false
|
||||||
|
}
|
@ -41,6 +41,16 @@ type ScanImageEvent struct {
|
|||||||
Operator string
|
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
|
// HookEvent is hook related event data to publish
|
||||||
type HookEvent struct {
|
type HookEvent struct {
|
||||||
PolicyID int64
|
PolicyID int64
|
||||||
@ -53,14 +63,15 @@ type HookEvent struct {
|
|||||||
type Payload struct {
|
type Payload struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
OccurAt int64 `json:"occur_at"`
|
OccurAt int64 `json:"occur_at"`
|
||||||
EventData *EventData `json:"event_data,omitempty"`
|
|
||||||
Operator string `json:"operator"`
|
Operator string `json:"operator"`
|
||||||
|
EventData *EventData `json:"event_data,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// EventData of notification event payload
|
// EventData of notification event payload
|
||||||
type EventData struct {
|
type EventData struct {
|
||||||
Resources []*Resource `json:"resources"`
|
Resources []*Resource `json:"resources"`
|
||||||
Repository *Repository `json:"repository"`
|
Repository *Repository `json:"repository"`
|
||||||
|
Custom map[string]string `json:"custom_attributes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resource describe infos of resource triggered notification
|
// Resource describe infos of resource triggered notification
|
||||||
|
@ -18,6 +18,10 @@ const (
|
|||||||
ScanningFailedTopic = "OnScanningFailed"
|
ScanningFailedTopic = "OnScanningFailed"
|
||||||
// ScanningCompletedTopic is topic for scanning completed event
|
// ScanningCompletedTopic is topic for scanning completed event
|
||||||
ScanningCompletedTopic = "OnScanningCompleted"
|
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 is topic for sending webhook payload
|
||||||
WebhookTopic = "http"
|
WebhookTopic = "http"
|
||||||
|
@ -19,6 +19,7 @@ func init() {
|
|||||||
model.DeleteChartTopic: {¬ification.ChartPreprocessHandler{}},
|
model.DeleteChartTopic: {¬ification.ChartPreprocessHandler{}},
|
||||||
model.ScanningCompletedTopic: {¬ification.ScanImagePreprocessHandler{}},
|
model.ScanningCompletedTopic: {¬ification.ScanImagePreprocessHandler{}},
|
||||||
model.ScanningFailedTopic: {¬ification.ScanImagePreprocessHandler{}},
|
model.ScanningFailedTopic: {¬ification.ScanImagePreprocessHandler{}},
|
||||||
|
model.QuotaExceedTopic: {¬ification.QuotaPreprocessHandler{}},
|
||||||
}
|
}
|
||||||
|
|
||||||
for t, handlers := range handlersMap {
|
for t, handlers := range handlersMap {
|
||||||
|
@ -11,6 +11,7 @@ const (
|
|||||||
EventTypeScanningCompleted = "scanningCompleted"
|
EventTypeScanningCompleted = "scanningCompleted"
|
||||||
EventTypeScanningFailed = "scanningFailed"
|
EventTypeScanningFailed = "scanningFailed"
|
||||||
EventTypeTestEndpoint = "testEndpoint"
|
EventTypeTestEndpoint = "testEndpoint"
|
||||||
|
EventTypeProjectQuota = "projectQuota"
|
||||||
|
|
||||||
NotifyTypeHTTP = "http"
|
NotifyTypeHTTP = "http"
|
||||||
)
|
)
|
||||||
|
@ -42,7 +42,7 @@ func Init() {
|
|||||||
initSupportedEventType(
|
initSupportedEventType(
|
||||||
model.EventTypePushImage, model.EventTypePullImage, model.EventTypeDeleteImage,
|
model.EventTypePushImage, model.EventTypePullImage, model.EventTypeDeleteImage,
|
||||||
model.EventTypeUploadChart, model.EventTypeDeleteChart, model.EventTypeDownloadChart,
|
model.EventTypeUploadChart, model.EventTypeDeleteChart, model.EventTypeDownloadChart,
|
||||||
model.EventTypeScanningCompleted, model.EventTypeScanningFailed,
|
model.EventTypeScanningCompleted, model.EventTypeScanningFailed, model.EventTypeProjectQuota,
|
||||||
)
|
)
|
||||||
|
|
||||||
initSupportedNotifyType(model.NotifyTypeHTTP)
|
initSupportedNotifyType(model.NotifyTypeHTTP)
|
||||||
|
@ -109,4 +109,5 @@ export enum WebhookEventTypes {
|
|||||||
PUSH_IMAGE = "pushImage",
|
PUSH_IMAGE = "pushImage",
|
||||||
SCANNING_FAILED = "scanningFailed",
|
SCANNING_FAILED = "scanningFailed",
|
||||||
SCANNING_COMPLETED = "scanningCompleted",
|
SCANNING_COMPLETED = "scanningCompleted",
|
||||||
|
PROJECT_QUOTA = "projectQuota",
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user