mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-19 23:28:20 +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"
|
"github.com/goharbor/harbor/src/common"
|
||||||
hlog "github.com/goharbor/harbor/src/common/utils/log"
|
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"
|
"github.com/goharbor/harbor/src/replication"
|
||||||
rep_event "github.com/goharbor/harbor/src/replication/event"
|
rep_event "github.com/goharbor/harbor/src/replication/event"
|
||||||
"github.com/justinas/alice"
|
"github.com/justinas/alice"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -110,8 +112,44 @@ func modifyResponse(res *http.Response) error {
|
|||||||
hlog.Errorf("failed to handle event: %v", err)
|
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
|
// Accept cases
|
||||||
|
@ -143,6 +143,7 @@ const (
|
|||||||
OIDCLoginPath = "/c/oidc/login"
|
OIDCLoginPath = "/c/oidc/login"
|
||||||
|
|
||||||
ChartUploadCtxKey = contextKey("chart_upload_event")
|
ChartUploadCtxKey = contextKey("chart_upload_event")
|
||||||
|
ChartDownloadCtxKey = contextKey("chart_download_event")
|
||||||
|
|
||||||
// Global notification enable configuration
|
// Global notification enable configuration
|
||||||
NotificationEnable = "notification_enable"
|
NotificationEnable = "notification_enable"
|
||||||
|
@ -20,13 +20,18 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/core/label"
|
"github.com/goharbor/harbor/src/core/label"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/core/middlewares"
|
"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"
|
rep_event "github.com/goharbor/harbor/src/replication/event"
|
||||||
"github.com/goharbor/harbor/src/replication/model"
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
namespaceParam = ":repo"
|
namespaceParam = ":repo"
|
||||||
nameParam = ":name"
|
nameParam = ":name"
|
||||||
|
filenameParam = ":filename"
|
||||||
defaultRepo = "library"
|
defaultRepo = "library"
|
||||||
rootUploadingEndpoint = "/api/chartrepo/charts"
|
rootUploadingEndpoint = "/api/chartrepo/charts"
|
||||||
rootIndexEndpoint = "/chartrepo/index.yaml"
|
rootIndexEndpoint = "/chartrepo/index.yaml"
|
||||||
@ -42,6 +47,8 @@ const (
|
|||||||
formFiledNameForProv = "prov"
|
formFiledNameForProv = "prov"
|
||||||
headerContentType = "Content-Type"
|
headerContentType = "Content-Type"
|
||||||
contentTypeMultipart = "multipart/form-data"
|
contentTypeMultipart = "multipart/form-data"
|
||||||
|
// chartPackageFileExtension is the file extension used for chart packages
|
||||||
|
chartPackageFileExtension = "tgz"
|
||||||
)
|
)
|
||||||
|
|
||||||
// chartController is a singleton instance
|
// chartController is a singleton instance
|
||||||
@ -181,6 +188,11 @@ func (cra *ChartRepositoryAPI) DownloadChart() {
|
|||||||
return
|
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
|
// Directly proxy to the backend
|
||||||
chartController.ProxyTraffic(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
chartController.ProxyTraffic(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
||||||
}
|
}
|
||||||
@ -278,6 +290,23 @@ func (cra *ChartRepositoryAPI) DeleteChartVersion() {
|
|||||||
cra.ParseAndHandleError("fail to delete chart version", err)
|
cra.ParseAndHandleError("fail to delete chart version", err)
|
||||||
return
|
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
|
// UploadChartVersion handles POST /api/:repo/charts
|
||||||
@ -355,13 +384,32 @@ func (cra *ChartRepositoryAPI) DeleteChart() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
versions := []string{}
|
||||||
for _, chartVersion := range chartVersions {
|
for _, chartVersion := range chartVersions {
|
||||||
|
versions = append(versions, chartVersion.GetVersion())
|
||||||
if err := cra.removeLabelsFromChart(chartName, chartVersion.GetVersion()); err != nil {
|
if err := cra.removeLabelsFromChart(chartName, chartVersion.GetVersion()); err != nil {
|
||||||
cra.SendInternalServerError(err)
|
cra.SendInternalServerError(err)
|
||||||
return
|
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 {
|
if err := chartController.DeleteChart(cra.namespace, chartName); err != nil {
|
||||||
cra.SendInternalServerError(err)
|
cra.SendInternalServerError(err)
|
||||||
return
|
return
|
||||||
@ -446,6 +494,10 @@ func (cra *ChartRepositoryAPI) addEventContext(files []formFile, request *http.R
|
|||||||
return err
|
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{
|
e := &rep_event.Event{
|
||||||
Type: rep_event.EventTypeChartUpload,
|
Type: rep_event.EventTypeChartUpload,
|
||||||
Resource: &model.Resource{
|
Resource: &model.Resource{
|
||||||
@ -456,6 +508,7 @@ func (cra *ChartRepositoryAPI) addEventContext(files []formFile, request *http.R
|
|||||||
},
|
},
|
||||||
Vtags: []string{chartDetails.Metadata.Version},
|
Vtags: []string{chartDetails.Metadata.Version},
|
||||||
},
|
},
|
||||||
|
ExtendedInfo: extInfo,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
*request = *(request.WithContext(context.WithValue(request.Context(), common.ChartUploadCtxKey, e)))
|
*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
|
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
|
// 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
|
// from the request automatically. Then the request passed to the backend server with proxying
|
||||||
// way will have empty content.
|
// 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)
|
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"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
autoTriggeredOperator = "auto"
|
||||||
|
)
|
||||||
|
|
||||||
// Event to publish
|
// Event to publish
|
||||||
type Event struct {
|
type Event struct {
|
||||||
Topic string
|
Topic string
|
||||||
@ -111,6 +115,105 @@ func (i *ImagePullMetaData) Resolve(evt *Event) error {
|
|||||||
return nil
|
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
|
// HookMetaData defines hook notification related event data
|
||||||
type HookMetaData struct {
|
type HookMetaData struct {
|
||||||
PolicyID int64
|
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"
|
"github.com/goharbor/harbor/src/core/notifier/event"
|
||||||
notifyModel "github.com/goharbor/harbor/src/core/notifier/model"
|
notifyModel "github.com/goharbor/harbor/src/core/notifier/model"
|
||||||
"github.com/goharbor/harbor/src/pkg/notification"
|
"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`
|
// getNameFromImgRepoFullName gets image name from repo full name with format `repoName/imageName`
|
||||||
@ -172,3 +173,30 @@ func preprocessAndSendImageHook(value interface{}) error {
|
|||||||
return nil
|
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
|
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
|
// HookEvent is hook related event data to publish
|
||||||
type HookEvent struct {
|
type HookEvent struct {
|
||||||
PolicyID int64
|
PolicyID int64
|
||||||
@ -49,6 +67,7 @@ type Resource struct {
|
|||||||
Digest string `json:"digest,omitempty"`
|
Digest string `json:"digest,omitempty"`
|
||||||
Tag string `json:"tag"`
|
Tag string `json:"tag"`
|
||||||
ResourceURL string `json:"resource_url,omitempty"`
|
ResourceURL string `json:"resource_url,omitempty"`
|
||||||
|
ScanOverview *models.ImgScanOverview `json:"scan_overview,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Repository info of notification event
|
// Repository info of notification event
|
||||||
|
@ -14,6 +14,11 @@ func init() {
|
|||||||
model.PullImageTopic: {¬ification.ImagePreprocessHandler{}},
|
model.PullImageTopic: {¬ification.ImagePreprocessHandler{}},
|
||||||
model.DeleteImageTopic: {¬ification.ImagePreprocessHandler{}},
|
model.DeleteImageTopic: {¬ification.ImagePreprocessHandler{}},
|
||||||
model.WebhookTopic: {¬ification.HTTPHandler{}},
|
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 {
|
for t, handlers := range handlersMap {
|
||||||
|
@ -24,6 +24,7 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/common/models"
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
"github.com/goharbor/harbor/src/core/api"
|
"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/notification"
|
||||||
"github.com/goharbor/harbor/src/pkg/retention"
|
"github.com/goharbor/harbor/src/pkg/retention"
|
||||||
"github.com/goharbor/harbor/src/replication"
|
"github.com/goharbor/harbor/src/replication"
|
||||||
@ -81,6 +82,21 @@ func (h *Handler) Prepare() {
|
|||||||
// HandleScan handles the webhook of scan job
|
// HandleScan handles the webhook of scan job
|
||||||
func (h *Handler) HandleScan() {
|
func (h *Handler) HandleScan() {
|
||||||
log.Debugf("received san job status update event: job-%d, status-%s", h.id, h.status)
|
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 {
|
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)
|
log.Errorf("Failed to update job status, id: %d, status: %s", h.id, h.status)
|
||||||
h.SendInternalServerError(err)
|
h.SendInternalServerError(err)
|
||||||
|
Loading…
Reference in New Issue
Block a user