Add chart and scanning event for webhook

Signed-off-by: peimingming <peimingming@corp.netease.com>
This commit is contained in:
peimingming 2019-08-11 00:27:07 +08:00
parent d663796b3d
commit 222c47142a
12 changed files with 820 additions and 9 deletions

View File

@ -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

View File

@ -142,7 +142,8 @@ const (
OIDCCallbackPath = "/c/oidc/callback"
OIDCLoginPath = "/c/oidc/login"
ChartUploadCtxKey = contextKey("chart_upload_event")
ChartUploadCtxKey = contextKey("chart_upload_event")
ChartDownloadCtxKey = contextKey("chart_download_event")
// Global notification enable configuration
NotificationEnable = "notification_enable"

View File

@ -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
}

View File

@ -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

View 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
}

View 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())
}

View File

@ -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
}

View 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
}

View File

@ -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())
}

View File

@ -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
@ -46,9 +64,10 @@ type EventData struct {
// Resource describe infos of resource triggered notification
type Resource struct {
Digest string `json:"digest,omitempty"`
Tag string `json:"tag"`
ResourceURL string `json:"resource_url,omitempty"`
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

View File

@ -10,10 +10,15 @@ import (
// Subscribe topics
func init() {
handlersMap := map[string][]notifier.NotificationHandler{
model.PushImageTopic: {&notification.ImagePreprocessHandler{}},
model.PullImageTopic: {&notification.ImagePreprocessHandler{}},
model.DeleteImageTopic: {&notification.ImagePreprocessHandler{}},
model.WebhookTopic: {&notification.HTTPHandler{}},
model.PushImageTopic: {&notification.ImagePreprocessHandler{}},
model.PullImageTopic: {&notification.ImagePreprocessHandler{}},
model.DeleteImageTopic: {&notification.ImagePreprocessHandler{}},
model.WebhookTopic: {&notification.HTTPHandler{}},
model.UploadChartTopic: {&notification.ChartPreprocessHandler{}},
model.DownloadChartTopic: {&notification.ChartPreprocessHandler{}},
model.DeleteChartTopic: {&notification.ChartPreprocessHandler{}},
model.ScanningCompletedTopic: {&notification.ScanImagePreprocessHandler{}},
model.ScanningFailedTopic: {&notification.ScanImagePreprocessHandler{}},
}
for t, handlers := range handlersMap {

View File

@ -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)