mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-24 01:27:49 +01:00
Add chart and scanning event for webhook
Signed-off-by: peimingming <peimingming@corp.netease.com>
This commit is contained in:
parent
d663796b3d
commit
222c47142a
@ -15,9 +15,11 @@ import (
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
hlog "github.com/goharbor/harbor/src/common/utils/log"
|
||||
n_event "github.com/goharbor/harbor/src/core/notifier/event"
|
||||
"github.com/goharbor/harbor/src/replication"
|
||||
rep_event "github.com/goharbor/harbor/src/replication/event"
|
||||
"github.com/justinas/alice"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -110,8 +112,44 @@ func modifyResponse(res *http.Response) error {
|
||||
hlog.Errorf("failed to handle event: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Trigger harbor webhook
|
||||
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{
|
||||
ProjectName: e.Resource.ExtendedInfo["projectName"].(string),
|
||||
ChartName: e.Resource.ExtendedInfo["chartName"].(string),
|
||||
Versions: e.Resource.Metadata.Vtags,
|
||||
OccurAt: time.Now(),
|
||||
Operator: e.Resource.ExtendedInfo["operator"].(string),
|
||||
},
|
||||
}
|
||||
if err := event.Build(metaData); err != nil {
|
||||
hlog.Errorf("failed to build chart upload event metadata: %v", err)
|
||||
}
|
||||
if err := event.Publish(); err != nil {
|
||||
hlog.Errorf("failed to publish chart upload event: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process downloading chart success webhook event
|
||||
if res.StatusCode == http.StatusOK {
|
||||
chartDownloadEvent := res.Request.Context().Value(common.ChartDownloadCtxKey)
|
||||
eventMetaData, ok := chartDownloadEvent.(*n_event.ChartDownloadMetaData)
|
||||
if ok && eventMetaData != nil {
|
||||
// Trigger harbor webhook
|
||||
event := &n_event.Event{}
|
||||
if err := event.Build(eventMetaData); err != nil {
|
||||
hlog.Errorf("failed to build chart download event metadata: %v", err)
|
||||
}
|
||||
if err := event.Publish(); err != nil {
|
||||
hlog.Errorf("failed to publish chart download event: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Accept cases
|
||||
|
@ -143,6 +143,7 @@ const (
|
||||
OIDCLoginPath = "/c/oidc/login"
|
||||
|
||||
ChartUploadCtxKey = contextKey("chart_upload_event")
|
||||
ChartDownloadCtxKey = contextKey("chart_download_event")
|
||||
|
||||
// Global notification enable configuration
|
||||
NotificationEnable = "notification_enable"
|
||||
|
@ -20,13 +20,18 @@ import (
|
||||
"github.com/goharbor/harbor/src/core/label"
|
||||
|
||||
"github.com/goharbor/harbor/src/core/middlewares"
|
||||
n_event "github.com/goharbor/harbor/src/core/notifier/event"
|
||||
rep_event "github.com/goharbor/harbor/src/replication/event"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
namespaceParam = ":repo"
|
||||
nameParam = ":name"
|
||||
filenameParam = ":filename"
|
||||
defaultRepo = "library"
|
||||
rootUploadingEndpoint = "/api/chartrepo/charts"
|
||||
rootIndexEndpoint = "/chartrepo/index.yaml"
|
||||
@ -42,6 +47,8 @@ const (
|
||||
formFiledNameForProv = "prov"
|
||||
headerContentType = "Content-Type"
|
||||
contentTypeMultipart = "multipart/form-data"
|
||||
// chartPackageFileExtension is the file extension used for chart packages
|
||||
chartPackageFileExtension = "tgz"
|
||||
)
|
||||
|
||||
// chartController is a singleton instance
|
||||
@ -181,6 +188,11 @@ func (cra *ChartRepositoryAPI) DownloadChart() {
|
||||
return
|
||||
}
|
||||
|
||||
namespace := cra.GetStringFromPath(namespaceParam)
|
||||
fileName := cra.GetStringFromPath(filenameParam)
|
||||
// Add hook event to request context
|
||||
cra.addDownloadChartEventContext(fileName, namespace, cra.Ctx.Request)
|
||||
|
||||
// Directly proxy to the backend
|
||||
chartController.ProxyTraffic(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
||||
}
|
||||
@ -278,6 +290,23 @@ func (cra *ChartRepositoryAPI) DeleteChartVersion() {
|
||||
cra.ParseAndHandleError("fail to delete chart version", err)
|
||||
return
|
||||
}
|
||||
|
||||
event := &n_event.Event{}
|
||||
metaData := &n_event.ChartDeleteMetaData{
|
||||
ChartMetaData: n_event.ChartMetaData{
|
||||
ProjectName: cra.namespace,
|
||||
ChartName: chartName,
|
||||
Versions: []string{version},
|
||||
OccurAt: time.Now(),
|
||||
Operator: cra.SecurityCtx.GetUsername(),
|
||||
},
|
||||
}
|
||||
if err := event.Build(metaData); err != nil {
|
||||
hlog.Errorf("failed to build chart delete event metadata: %v", err)
|
||||
}
|
||||
if err := event.Publish(); err != nil {
|
||||
hlog.Errorf("failed to publish chart delete event: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// UploadChartVersion handles POST /api/:repo/charts
|
||||
@ -355,13 +384,32 @@ func (cra *ChartRepositoryAPI) DeleteChart() {
|
||||
return
|
||||
}
|
||||
|
||||
versions := []string{}
|
||||
for _, chartVersion := range chartVersions {
|
||||
versions = append(versions, chartVersion.GetVersion())
|
||||
if err := cra.removeLabelsFromChart(chartName, chartVersion.GetVersion()); err != nil {
|
||||
cra.SendInternalServerError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
event := &n_event.Event{}
|
||||
metaData := &n_event.ChartDeleteMetaData{
|
||||
ChartMetaData: n_event.ChartMetaData{
|
||||
ProjectName: cra.namespace,
|
||||
ChartName: chartName,
|
||||
Versions: versions,
|
||||
OccurAt: time.Now(),
|
||||
Operator: cra.SecurityCtx.GetUsername(),
|
||||
},
|
||||
}
|
||||
if err := event.Build(metaData); err != nil {
|
||||
hlog.Errorf("failed to build chart delete event metadata: %v", err)
|
||||
}
|
||||
if err := event.Publish(); err != nil {
|
||||
hlog.Errorf("failed to publish chart delete event: %v", err)
|
||||
}
|
||||
|
||||
if err := chartController.DeleteChart(cra.namespace, chartName); err != nil {
|
||||
cra.SendInternalServerError(err)
|
||||
return
|
||||
@ -446,6 +494,10 @@ func (cra *ChartRepositoryAPI) addEventContext(files []formFile, request *http.R
|
||||
return err
|
||||
}
|
||||
|
||||
extInfo := make(map[string]interface{})
|
||||
extInfo["operator"] = cra.SecurityCtx.GetUsername()
|
||||
extInfo["projectName"] = cra.namespace
|
||||
extInfo["chartName"] = chartDetails.Metadata.Name
|
||||
e := &rep_event.Event{
|
||||
Type: rep_event.EventTypeChartUpload,
|
||||
Resource: &model.Resource{
|
||||
@ -456,6 +508,7 @@ func (cra *ChartRepositoryAPI) addEventContext(files []formFile, request *http.R
|
||||
},
|
||||
Vtags: []string{chartDetails.Metadata.Version},
|
||||
},
|
||||
ExtendedInfo: extInfo,
|
||||
},
|
||||
}
|
||||
*request = *(request.WithContext(context.WithValue(request.Context(), common.ChartUploadCtxKey, e)))
|
||||
@ -466,6 +519,20 @@ func (cra *ChartRepositoryAPI) addEventContext(files []formFile, request *http.R
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cra *ChartRepositoryAPI) addDownloadChartEventContext(fileName, namespace string, request *http.Request) {
|
||||
chartName, version := parseChartVersionFromFilename(fileName)
|
||||
event := &n_event.ChartDownloadMetaData{
|
||||
ChartMetaData: n_event.ChartMetaData{
|
||||
ProjectName: namespace,
|
||||
ChartName: chartName,
|
||||
Versions: []string{version},
|
||||
OccurAt: time.Now(),
|
||||
Operator: cra.SecurityCtx.GetUsername(),
|
||||
},
|
||||
}
|
||||
*request = *(request.WithContext(context.WithValue(request.Context(), common.ChartDownloadCtxKey, event)))
|
||||
}
|
||||
|
||||
// If the files are uploaded with multipart/form-data mimetype, beego will extract the data
|
||||
// from the request automatically. Then the request passed to the backend server with proxying
|
||||
// way will have empty content.
|
||||
@ -555,3 +622,24 @@ func chartFullName(namespace, chartName, version string) string {
|
||||
}
|
||||
return fmt.Sprintf("%s/%s:%s", namespace, chartName, version)
|
||||
}
|
||||
|
||||
// parseChartVersionFromFilename parse chart and version from file name
|
||||
func parseChartVersionFromFilename(filename string) (string, string) {
|
||||
noExt := strings.TrimSuffix(path.Base(filename), fmt.Sprintf(".%s", chartPackageFileExtension))
|
||||
parts := strings.Split(noExt, "-")
|
||||
name := parts[0]
|
||||
version := ""
|
||||
for idx, part := range parts[1:] {
|
||||
if _, err := strconv.Atoi(string(part[0])); err == nil { // see if this part looks like a version (starts w int)
|
||||
version = strings.Join(parts[idx+1:], "-")
|
||||
break
|
||||
}
|
||||
name = fmt.Sprintf("%s-%s", name, part)
|
||||
}
|
||||
if version == "" { // no parts looked like a real version, just take everything after last hyphen
|
||||
lastIndex := len(parts) - 1
|
||||
name = strings.Join(parts[:lastIndex], "-")
|
||||
version = parts[lastIndex]
|
||||
}
|
||||
return name, version
|
||||
}
|
||||
|
@ -11,6 +11,10 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
autoTriggeredOperator = "auto"
|
||||
)
|
||||
|
||||
// Event to publish
|
||||
type Event struct {
|
||||
Topic string
|
||||
@ -111,6 +115,105 @@ func (i *ImagePullMetaData) Resolve(evt *Event) error {
|
||||
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 {
|
||||
JobID int64
|
||||
Status string
|
||||
}
|
||||
|
||||
// Resolve image scanning metadata into common chart event
|
||||
func (si *ScanImageMetaData) Resolve(evt *Event) error {
|
||||
var eventType string
|
||||
var topic string
|
||||
if si.Status == models.JobFinished {
|
||||
eventType = notifyModel.EventTypeScanningCompleted
|
||||
topic = model.ScanningCompletedTopic
|
||||
} else if si.Status == models.JobError {
|
||||
eventType = notifyModel.EventTypeScanningFailed
|
||||
topic = model.ScanningFailedTopic
|
||||
} else {
|
||||
return errors.New("not supported scan hook status")
|
||||
}
|
||||
data := &model.ScanImageEvent{
|
||||
EventType: eventType,
|
||||
JobID: si.JobID,
|
||||
OccurAt: time.Now(),
|
||||
Operator: autoTriggeredOperator,
|
||||
}
|
||||
|
||||
evt.Topic = topic
|
||||
evt.Data = data
|
||||
return nil
|
||||
}
|
||||
|
||||
// HookMetaData defines hook notification related event data
|
||||
type HookMetaData struct {
|
||||
PolicyID int64
|
||||
|
107
src/core/notifier/handler/notification/chart_handler.go
Normal file
107
src/core/notifier/handler/notification/chart_handler.go
Normal file
@ -0,0 +1,107 @@
|
||||
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"
|
||||
"github.com/goharbor/harbor/src/pkg/notification"
|
||||
)
|
||||
|
||||
// ChartPreprocessHandler preprocess chart event data
|
||||
type ChartPreprocessHandler 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)
|
||||
if !ok {
|
||||
return errors.New("invalid chart event type")
|
||||
}
|
||||
|
||||
if chartEvent == nil || len(chartEvent.Versions) == 0 || len(chartEvent.ProjectName) == 0 || len(chartEvent.ChartName) == 0 {
|
||||
return fmt.Errorf("data miss in chart event: %v", chartEvent)
|
||||
}
|
||||
|
||||
project, err := config.GlobalProjectMgr.Get(chartEvent.ProjectName)
|
||||
if err != nil {
|
||||
log.Errorf("failed to find project[%s] for chart event: %v", chartEvent.ProjectName, err)
|
||||
return err
|
||||
}
|
||||
if project == nil {
|
||||
return fmt.Errorf("project not found for chart event: %s", chartEvent.ProjectName)
|
||||
}
|
||||
policies, err := notification.PolicyMgr.GetRelatedPolices(project.ProjectID, chartEvent.EventType)
|
||||
if err != nil {
|
||||
log.Errorf("failed to find policy for %s event: %v", chartEvent.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", chartEvent.EventType, chartEvent)
|
||||
return nil
|
||||
}
|
||||
|
||||
payload, err := constructChartPayload(chartEvent, project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = sendHookWithPolicies(policies, payload, chartEvent.EventType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsStateful ...
|
||||
func (cph *ChartPreprocessHandler) IsStateful() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func constructChartPayload(event *model.ChartEvent, project *models.Project) (*model.Payload, error) {
|
||||
repoType := models.ProjectPrivate
|
||||
if project.IsPublic() {
|
||||
repoType = models.ProjectPublic
|
||||
}
|
||||
|
||||
payload := &model.Payload{
|
||||
Type: event.EventType,
|
||||
OccurAt: event.OccurAt.Unix(),
|
||||
EventData: &model.EventData{
|
||||
Repository: &model.Repository{
|
||||
Name: event.ChartName,
|
||||
Namespace: event.ProjectName,
|
||||
RepoFullName: fmt.Sprintf("%s/%s", event.ProjectName, event.ChartName),
|
||||
RepoType: repoType,
|
||||
},
|
||||
},
|
||||
Operator: event.Operator,
|
||||
}
|
||||
|
||||
extURL, err := config.ExtEndpoint()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get external endpoint failed: %v", err)
|
||||
}
|
||||
|
||||
resourcePrefix := fmt.Sprintf("%s/chartrepo/%s/charts/%s", extURL, event.ProjectName, event.ChartName)
|
||||
for _, v := range event.Versions {
|
||||
resURL := fmt.Sprintf("%s-%s.tgz", resourcePrefix, v)
|
||||
|
||||
resource := &model.Resource{
|
||||
Tag: v,
|
||||
ResourceURL: resURL,
|
||||
}
|
||||
payload.EventData.Resources = append(payload.EventData.Resources, resource)
|
||||
}
|
||||
return payload, nil
|
||||
}
|
171
src/core/notifier/handler/notification/chart_handler_test.go
Normal file
171
src/core/notifier/handler/notification/chart_handler_test.go
Normal file
@ -0,0 +1,171 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/core/notifier/model"
|
||||
"github.com/goharbor/harbor/src/pkg/notification"
|
||||
notificationModel "github.com/goharbor/harbor/src/pkg/notification/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
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{
|
||||
notificationModel.EventTypeUploadChart,
|
||||
notificationModel.EventTypeDownloadChart,
|
||||
notificationModel.EventTypeDeleteChart,
|
||||
notificationModel.EventTypeScanningCompleted,
|
||||
notificationModel.EventTypeScanningFailed,
|
||||
},
|
||||
Targets: []models.EventTarget{
|
||||
{
|
||||
Type: "http",
|
||||
Address: "http://127.0.0.1:8080",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestChartPreprocessHandler_Handle(t *testing.T) {
|
||||
PolicyMgr := notification.PolicyMgr
|
||||
defer func() {
|
||||
notification.PolicyMgr = PolicyMgr
|
||||
}()
|
||||
notification.PolicyMgr = &fakedPolicyMgr{}
|
||||
|
||||
handler := &ChartPreprocessHandler{}
|
||||
config.Init()
|
||||
|
||||
name := "project_for_test_chart_event_preprocess"
|
||||
id, _ := config.GlobalProjectMgr.Create(&models.Project{
|
||||
Name: name,
|
||||
OwnerID: 1,
|
||||
Metadata: map[string]string{
|
||||
models.ProMetaEnableContentTrust: "true",
|
||||
models.ProMetaPreventVul: "true",
|
||||
models.ProMetaSeverity: "low",
|
||||
models.ProMetaReuseSysCVEWhitelist: "false",
|
||||
},
|
||||
})
|
||||
defer func(id int64) {
|
||||
if err := config.GlobalProjectMgr.Delete(id); err != nil {
|
||||
t.Logf("failed to delete project %d: %v", id, err)
|
||||
}
|
||||
}(id)
|
||||
|
||||
type args struct {
|
||||
data interface{}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "ChartPreprocessHandler Want Error 1",
|
||||
args: args{
|
||||
data: nil,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "ChartPreprocessHandler Want Error 2",
|
||||
args: args{
|
||||
data: &model.ChartEvent{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "ChartPreprocessHandler Want Error 3",
|
||||
args: args{
|
||||
data: &model.ChartEvent{
|
||||
Versions: []string{
|
||||
"v1.2.1",
|
||||
},
|
||||
ProjectName: "project_for_test_chart_event_preprocess",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "ChartPreprocessHandler Want Error 4",
|
||||
args: args{
|
||||
data: &model.ChartEvent{
|
||||
Versions: []string{
|
||||
"v1.2.1",
|
||||
},
|
||||
ProjectName: "project_for_test_chart_event_preprocess_not_exists",
|
||||
ChartName: "testChart",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "ChartPreprocessHandler Want Error 5",
|
||||
args: args{
|
||||
data: &model.ChartEvent{
|
||||
Versions: []string{
|
||||
"v1.2.1",
|
||||
},
|
||||
ProjectName: "project_for_test_chart_event_preprocess",
|
||||
ChartName: "testChart",
|
||||
EventType: "uploadChart",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
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 TestChartPreprocessHandler_IsStateful(t *testing.T) {
|
||||
handler := &ChartPreprocessHandler{}
|
||||
assert.False(t, handler.IsStateful())
|
||||
}
|
@ -12,6 +12,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/core/notifier/event"
|
||||
notifyModel "github.com/goharbor/harbor/src/core/notifier/model"
|
||||
"github.com/goharbor/harbor/src/pkg/notification"
|
||||
pkgNotifyModel "github.com/goharbor/harbor/src/pkg/notification/model"
|
||||
)
|
||||
|
||||
// getNameFromImgRepoFullName gets image name from repo full name with format `repoName/imageName`
|
||||
@ -172,3 +173,30 @@ func preprocessAndSendImageHook(value interface{}) error {
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// will return nil when it failed to get data
|
||||
func getScanOverview(digest string, tag string, eventType string) *models.ImgScanOverview {
|
||||
if len(digest) == 0 {
|
||||
log.Debug("digest is nil")
|
||||
return nil
|
||||
}
|
||||
data, err := dao.GetImgScanOverview(digest)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get scan result for tag:%s, digest: %s, error: %v", tag, digest, err)
|
||||
}
|
||||
if data == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Status should set by the eventType but the status from jobData in DB
|
||||
if eventType == pkgNotifyModel.EventTypeScanningCompleted {
|
||||
data.Status = models.JobFinished
|
||||
} else {
|
||||
log.Debugf("Unsetting vulnerable related historical values, job status: %s", data.Status)
|
||||
data.Status = models.JobError
|
||||
data.Sev = 0
|
||||
data.CompOverview = nil
|
||||
data.DetailsKey = ""
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
127
src/core/notifier/handler/notification/scan_image_handler.go
Normal file
127
src/core/notifier/handler/notification/scan_image_handler.go
Normal file
@ -0,0 +1,127 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"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/core/notifier/model"
|
||||
"github.com/goharbor/harbor/src/pkg/notification"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ScanImagePreprocessHandler preprocess chart event data
|
||||
type ScanImagePreprocessHandler 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
|
||||
}
|
||||
|
||||
e, ok := value.(*model.ScanImageEvent)
|
||||
if !ok {
|
||||
return errors.New("invalid scan image event type")
|
||||
}
|
||||
|
||||
if e == nil {
|
||||
return errors.New("empty scan image event")
|
||||
}
|
||||
|
||||
job, err := dao.GetScanJob(e.JobID)
|
||||
if err != nil {
|
||||
log.Errorf("failed to find scan job[%d] for scanning webhook: %v", e.JobID, err)
|
||||
return err
|
||||
}
|
||||
if job == nil {
|
||||
log.Errorf("can't find scan job[%d] for scanning webhook", e.JobID)
|
||||
return fmt.Errorf("scan job for scanning webhook not found: %d", e.JobID)
|
||||
}
|
||||
|
||||
rs := strings.SplitN(job.Repository, "/", 2)
|
||||
projectName := rs[0]
|
||||
repoName := rs[1]
|
||||
|
||||
project, err := config.GlobalProjectMgr.Get(projectName)
|
||||
if err != nil {
|
||||
log.Errorf("failed to find project[%s] for scan image event: %v", projectName, err)
|
||||
return err
|
||||
}
|
||||
policies, err := notification.PolicyMgr.GetRelatedPolices(project.ProjectID, e.EventType)
|
||||
if err != nil {
|
||||
log.Errorf("failed to find policy for %s event: %v", e.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", e.EventType, e)
|
||||
return nil
|
||||
}
|
||||
|
||||
payload, err := constructScanImagePayload(e, job, project, projectName, repoName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = sendHookWithPolicies(policies, payload, e.EventType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsStateful ...
|
||||
func (si *ScanImagePreprocessHandler) IsStateful() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func constructScanImagePayload(event *model.ScanImageEvent, job *models.ScanJob, project *models.Project, projectName, repoName string) (*model.Payload, error) {
|
||||
repoType := models.ProjectPrivate
|
||||
if project.IsPublic() {
|
||||
repoType = models.ProjectPublic
|
||||
}
|
||||
|
||||
payload := &model.Payload{
|
||||
Type: event.EventType,
|
||||
OccurAt: event.OccurAt.Unix(),
|
||||
EventData: &model.EventData{
|
||||
Repository: &model.Repository{
|
||||
Name: repoName,
|
||||
Namespace: projectName,
|
||||
RepoFullName: job.Repository,
|
||||
RepoType: repoType,
|
||||
},
|
||||
},
|
||||
Operator: event.Operator,
|
||||
}
|
||||
|
||||
extURL, err := config.ExtURL()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get external endpoint failed: %v", err)
|
||||
}
|
||||
resURL, _ := buildImageResourceURL(extURL, job.Repository, job.Tag)
|
||||
|
||||
// Add scan overview
|
||||
scanOverview := getScanOverview(job.Digest, job.Tag, event.EventType)
|
||||
if scanOverview == nil {
|
||||
scanOverview = &models.ImgScanOverview{
|
||||
JobID: job.ID,
|
||||
Status: job.Status,
|
||||
CreationTime: job.CreationTime,
|
||||
}
|
||||
}
|
||||
resource := &model.Resource{
|
||||
Tag: job.Tag,
|
||||
Digest: job.Digest,
|
||||
ResourceURL: resURL,
|
||||
ScanOverview: scanOverview,
|
||||
}
|
||||
payload.EventData.Resources = append(payload.EventData.Resources, resource)
|
||||
return payload, nil
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"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/core/notifier/model"
|
||||
"github.com/goharbor/harbor/src/pkg/notification"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestScanImagePreprocessHandler_Handle(t *testing.T) {
|
||||
PolicyMgr := notification.PolicyMgr
|
||||
defer func() {
|
||||
notification.PolicyMgr = PolicyMgr
|
||||
}()
|
||||
notification.PolicyMgr = &fakedPolicyMgr{}
|
||||
|
||||
handler := &ScanImagePreprocessHandler{}
|
||||
config.Init()
|
||||
|
||||
name := "project_for_test_scanning_event_preprocess"
|
||||
id, _ := config.GlobalProjectMgr.Create(&models.Project{
|
||||
Name: name,
|
||||
OwnerID: 1,
|
||||
Metadata: map[string]string{
|
||||
models.ProMetaEnableContentTrust: "true",
|
||||
models.ProMetaPreventVul: "true",
|
||||
models.ProMetaSeverity: "low",
|
||||
models.ProMetaReuseSysCVEWhitelist: "false",
|
||||
},
|
||||
})
|
||||
defer func(id int64) {
|
||||
if err := config.GlobalProjectMgr.Delete(id); err != nil {
|
||||
t.Logf("failed to delete project %d: %v", id, err)
|
||||
}
|
||||
}(id)
|
||||
|
||||
jID, _ := dao.AddScanJob(models.ScanJob{
|
||||
Status: "finished",
|
||||
Repository: "project_for_test_scanning_event_preprocess/testrepo",
|
||||
Tag: "v1.0.0",
|
||||
Digest: "sha256:5a539a2c733ca9efcd62d4561b36ea93d55436c5a86825b8e43ce8303a7a0752",
|
||||
CreationTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
})
|
||||
|
||||
type args struct {
|
||||
data interface{}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "ScanImagePreprocessHandler Want Error 1",
|
||||
args: args{
|
||||
data: nil,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "ScanImagePreprocessHandler Want Error 2",
|
||||
args: args{
|
||||
data: &model.ScanImageEvent{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "ScanImagePreprocessHandler Want Error 3",
|
||||
args: args{
|
||||
data: &model.ScanImageEvent{
|
||||
JobID: jID + 1000,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "ScanImagePreprocessHandler Want Error 4",
|
||||
args: args{
|
||||
data: &model.ScanImageEvent{
|
||||
JobID: jID,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
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 TestScanImagePreprocessHandler_IsStateful(t *testing.T) {
|
||||
handler := &ScanImagePreprocessHandler{}
|
||||
assert.False(t, handler.IsStateful())
|
||||
}
|
@ -22,6 +22,24 @@ type ImgResource struct {
|
||||
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
|
||||
JobID int64
|
||||
OccurAt time.Time
|
||||
Operator string
|
||||
}
|
||||
|
||||
// HookEvent is hook related event data to publish
|
||||
type HookEvent struct {
|
||||
PolicyID int64
|
||||
@ -49,6 +67,7 @@ type Resource struct {
|
||||
Digest string `json:"digest,omitempty"`
|
||||
Tag string `json:"tag"`
|
||||
ResourceURL string `json:"resource_url,omitempty"`
|
||||
ScanOverview *models.ImgScanOverview `json:"scan_overview,omitempty"`
|
||||
}
|
||||
|
||||
// Repository info of notification event
|
||||
|
@ -14,6 +14,11 @@ func init() {
|
||||
model.PullImageTopic: {¬ification.ImagePreprocessHandler{}},
|
||||
model.DeleteImageTopic: {¬ification.ImagePreprocessHandler{}},
|
||||
model.WebhookTopic: {¬ification.HTTPHandler{}},
|
||||
model.UploadChartTopic: {¬ification.ChartPreprocessHandler{}},
|
||||
model.DownloadChartTopic: {¬ification.ChartPreprocessHandler{}},
|
||||
model.DeleteChartTopic: {¬ification.ChartPreprocessHandler{}},
|
||||
model.ScanningCompletedTopic: {¬ification.ScanImagePreprocessHandler{}},
|
||||
model.ScanningFailedTopic: {¬ification.ScanImagePreprocessHandler{}},
|
||||
}
|
||||
|
||||
for t, handlers := range handlersMap {
|
||||
|
@ -24,6 +24,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/api"
|
||||
"github.com/goharbor/harbor/src/core/notifier/event"
|
||||
"github.com/goharbor/harbor/src/pkg/notification"
|
||||
"github.com/goharbor/harbor/src/pkg/retention"
|
||||
"github.com/goharbor/harbor/src/replication"
|
||||
@ -81,6 +82,21 @@ func (h *Handler) Prepare() {
|
||||
// HandleScan handles the webhook of scan job
|
||||
func (h *Handler) HandleScan() {
|
||||
log.Debugf("received san job status update event: job-%d, status-%s", h.id, h.status)
|
||||
// Trigger image scan webhook event only for JobFinished and JobError status
|
||||
if h.status == models.JobFinished || h.status == models.JobError {
|
||||
e := &event.Event{}
|
||||
metaData := &event.ScanImageMetaData{
|
||||
JobID: h.id,
|
||||
Status: h.status,
|
||||
}
|
||||
if err := e.Build(metaData); err != nil {
|
||||
log.Errorf("failed to build image scanning event metadata: %v", err)
|
||||
}
|
||||
if err := e.Publish(); err != nil {
|
||||
log.Errorf("failed to publish image scanning event: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := dao.UpdateScanJobStatus(h.id, h.status); err != nil {
|
||||
log.Errorf("Failed to update job status, id: %d, status: %s", h.id, h.status)
|
||||
h.SendInternalServerError(err)
|
||||
|
Loading…
Reference in New Issue
Block a user