mirror of
https://github.com/goharbor/harbor.git
synced 2024-09-28 21:37:31 +02:00
feat: add stop scan & stop scan-all feature
Signed-off-by: Shengwen Yu <yshengwen@vmware.com>
This commit is contained in:
parent
cdb13f5191
commit
e2e3bcca1c
@ -1166,6 +1166,31 @@ paths:
|
|||||||
$ref: '#/responses/404'
|
$ref: '#/responses/404'
|
||||||
'500':
|
'500':
|
||||||
$ref: '#/responses/500'
|
$ref: '#/responses/500'
|
||||||
|
/projects/{project_name}/repositories/{repository_name}/artifacts/{reference}/scan/stop:
|
||||||
|
post:
|
||||||
|
summary: Cancelling a scan job for a particular artifact
|
||||||
|
description: Cancelling a scan job for a particular artifact
|
||||||
|
tags:
|
||||||
|
- scan
|
||||||
|
operationId: stopScanArtifact
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/parameters/requestId'
|
||||||
|
- $ref: '#/parameters/projectName'
|
||||||
|
- $ref: '#/parameters/repositoryName'
|
||||||
|
- $ref: '#/parameters/reference'
|
||||||
|
responses:
|
||||||
|
'202':
|
||||||
|
$ref: '#/responses/202'
|
||||||
|
'400':
|
||||||
|
$ref: '#/responses/400'
|
||||||
|
'401':
|
||||||
|
$ref: '#/responses/401'
|
||||||
|
'403':
|
||||||
|
$ref: '#/responses/403'
|
||||||
|
'404':
|
||||||
|
$ref: '#/responses/404'
|
||||||
|
'500':
|
||||||
|
$ref: '#/responses/500'
|
||||||
/projects/{project_name}/repositories/{repository_name}/artifacts/{reference}/scan/{report_id}/log:
|
/projects/{project_name}/repositories/{repository_name}/artifacts/{reference}/scan/{report_id}/log:
|
||||||
get:
|
get:
|
||||||
summary: Get the log of the scan report
|
summary: Get the log of the scan report
|
||||||
@ -4167,6 +4192,26 @@ paths:
|
|||||||
$ref: '#/responses/412'
|
$ref: '#/responses/412'
|
||||||
'500':
|
'500':
|
||||||
$ref: '#/responses/500'
|
$ref: '#/responses/500'
|
||||||
|
/system/scanAll/stop:
|
||||||
|
post:
|
||||||
|
summary: Stop scanAll job execution
|
||||||
|
description: Stop scanAll job execution
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/parameters/requestId'
|
||||||
|
tags:
|
||||||
|
- scanAll
|
||||||
|
operationId: stopScanAll
|
||||||
|
responses:
|
||||||
|
'202':
|
||||||
|
$ref: '#/responses/202'
|
||||||
|
'400':
|
||||||
|
$ref: '#/responses/400'
|
||||||
|
'401':
|
||||||
|
$ref: '#/responses/401'
|
||||||
|
'403':
|
||||||
|
$ref: '#/responses/403'
|
||||||
|
'500':
|
||||||
|
$ref: '#/responses/500'
|
||||||
/ping:
|
/ping:
|
||||||
get:
|
get:
|
||||||
operationId: getPing
|
operationId: getPing
|
||||||
|
@ -30,6 +30,7 @@ const (
|
|||||||
|
|
||||||
ActionOperate = Action("operate")
|
ActionOperate = Action("operate")
|
||||||
ActionScannerPull = Action("scanner-pull") // for robot account created by scanner to pull image, bypass the policy check
|
ActionScannerPull = Action("scanner-pull") // for robot account created by scanner to pull image, bypass the policy check
|
||||||
|
ActionStop = Action("stop") // for stop scan/scan-all execution
|
||||||
)
|
)
|
||||||
|
|
||||||
// const resource variables
|
// const resource variables
|
||||||
|
@ -98,6 +98,7 @@ var (
|
|||||||
|
|
||||||
{Resource: rbac.ResourceScan, Action: rbac.ActionCreate},
|
{Resource: rbac.ResourceScan, Action: rbac.ActionCreate},
|
||||||
{Resource: rbac.ResourceScan, Action: rbac.ActionRead},
|
{Resource: rbac.ResourceScan, Action: rbac.ActionRead},
|
||||||
|
{Resource: rbac.ResourceScan, Action: rbac.ActionStop},
|
||||||
|
|
||||||
{Resource: rbac.ResourceScanner, Action: rbac.ActionRead},
|
{Resource: rbac.ResourceScanner, Action: rbac.ActionRead},
|
||||||
{Resource: rbac.ResourceScanner, Action: rbac.ActionCreate},
|
{Resource: rbac.ResourceScanner, Action: rbac.ActionCreate},
|
||||||
@ -185,6 +186,7 @@ var (
|
|||||||
|
|
||||||
{Resource: rbac.ResourceScan, Action: rbac.ActionCreate},
|
{Resource: rbac.ResourceScan, Action: rbac.ActionCreate},
|
||||||
{Resource: rbac.ResourceScan, Action: rbac.ActionRead},
|
{Resource: rbac.ResourceScan, Action: rbac.ActionRead},
|
||||||
|
{Resource: rbac.ResourceScan, Action: rbac.ActionStop},
|
||||||
|
|
||||||
{Resource: rbac.ResourceScanner, Action: rbac.ActionRead},
|
{Resource: rbac.ResourceScanner, Action: rbac.ActionRead},
|
||||||
|
|
||||||
|
@ -58,6 +58,7 @@ var (
|
|||||||
{Resource: rbac.ResourceScanAll, Action: rbac.ActionUpdate},
|
{Resource: rbac.ResourceScanAll, Action: rbac.ActionUpdate},
|
||||||
{Resource: rbac.ResourceScanAll, Action: rbac.ActionDelete},
|
{Resource: rbac.ResourceScanAll, Action: rbac.ActionDelete},
|
||||||
{Resource: rbac.ResourceScanAll, Action: rbac.ActionList},
|
{Resource: rbac.ResourceScanAll, Action: rbac.ActionList},
|
||||||
|
{Resource: rbac.ResourceScanAll, Action: rbac.ActionStop},
|
||||||
|
|
||||||
{Resource: rbac.ResourceSystemVolumes, Action: rbac.ActionRead},
|
{Resource: rbac.ResourceSystemVolumes, Action: rbac.ActionRead},
|
||||||
|
|
||||||
|
@ -30,6 +30,7 @@ func init() {
|
|||||||
notifier.Subscribe(event.TopicQuotaExceed, "a.Handler{})
|
notifier.Subscribe(event.TopicQuotaExceed, "a.Handler{})
|
||||||
notifier.Subscribe(event.TopicQuotaWarning, "a.Handler{})
|
notifier.Subscribe(event.TopicQuotaWarning, "a.Handler{})
|
||||||
notifier.Subscribe(event.TopicScanningFailed, &scan.Handler{})
|
notifier.Subscribe(event.TopicScanningFailed, &scan.Handler{})
|
||||||
|
notifier.Subscribe(event.TopicScanningStopped, &scan.Handler{})
|
||||||
notifier.Subscribe(event.TopicScanningCompleted, &scan.Handler{})
|
notifier.Subscribe(event.TopicScanningCompleted, &scan.Handler{})
|
||||||
notifier.Subscribe(event.TopicDeleteArtifact, &scan.DelArtHandler{})
|
notifier.Subscribe(event.TopicDeleteArtifact, &scan.DelArtHandler{})
|
||||||
notifier.Subscribe(event.TopicReplication, &artifact.ReplicationHandler{})
|
notifier.Subscribe(event.TopicReplication, &artifact.ReplicationHandler{})
|
||||||
|
@ -29,7 +29,10 @@ func (si *ScanImageMetaData) Resolve(evt *event.Event) error {
|
|||||||
case job.SuccessStatus:
|
case job.SuccessStatus:
|
||||||
eventType = event2.TopicScanningCompleted
|
eventType = event2.TopicScanningCompleted
|
||||||
topic = event2.TopicScanningCompleted
|
topic = event2.TopicScanningCompleted
|
||||||
case job.ErrorStatus, job.StoppedStatus:
|
case job.StoppedStatus:
|
||||||
|
eventType = event2.TopicScanningStopped
|
||||||
|
topic = event2.TopicScanningStopped
|
||||||
|
case job.ErrorStatus:
|
||||||
eventType = event2.TopicScanningFailed
|
eventType = event2.TopicScanningFailed
|
||||||
topic = event2.TopicScanningFailed
|
topic = event2.TopicScanningFailed
|
||||||
default:
|
default:
|
||||||
|
@ -49,6 +49,48 @@ func (r *scanEventTestSuite) TestResolveOfScanImageEventMetadata() {
|
|||||||
r.Equal("library/hello-world", data.Artifact.Repository)
|
r.Equal("library/hello-world", data.Artifact.Repository)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *scanEventTestSuite) TestResolveOfStopScanImageEventMetadata() {
|
||||||
|
e := &event.Event{}
|
||||||
|
metadata := &ScanImageMetaData{
|
||||||
|
Artifact: &v1.Artifact{
|
||||||
|
NamespaceID: 0,
|
||||||
|
Repository: "library/hello-world",
|
||||||
|
Tag: "latest",
|
||||||
|
Digest: "sha256:absdfd87123",
|
||||||
|
MimeType: "docker.chart",
|
||||||
|
},
|
||||||
|
Status: job.StoppedStatus.String(),
|
||||||
|
}
|
||||||
|
err := metadata.Resolve(e)
|
||||||
|
r.Require().Nil(err)
|
||||||
|
r.Equal(event2.TopicScanningStopped, e.Topic)
|
||||||
|
r.Require().NotNil(e.Data)
|
||||||
|
data, ok := e.Data.(*event2.ScanImageEvent)
|
||||||
|
r.Require().True(ok)
|
||||||
|
r.Equal("library/hello-world", data.Artifact.Repository)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *scanEventTestSuite) TestResolveOfFailedScanImageEventMetadata() {
|
||||||
|
e := &event.Event{}
|
||||||
|
metadata := &ScanImageMetaData{
|
||||||
|
Artifact: &v1.Artifact{
|
||||||
|
NamespaceID: 0,
|
||||||
|
Repository: "library/hello-world",
|
||||||
|
Tag: "latest",
|
||||||
|
Digest: "sha256:absdfd87123",
|
||||||
|
MimeType: "docker.chart",
|
||||||
|
},
|
||||||
|
Status: job.ErrorStatus.String(),
|
||||||
|
}
|
||||||
|
err := metadata.Resolve(e)
|
||||||
|
r.Require().Nil(err)
|
||||||
|
r.Equal(event2.TopicScanningFailed, e.Topic)
|
||||||
|
r.Require().NotNil(e.Data)
|
||||||
|
data, ok := e.Data.(*event2.ScanImageEvent)
|
||||||
|
r.Require().True(ok)
|
||||||
|
r.Equal("library/hello-world", data.Artifact.Repository)
|
||||||
|
}
|
||||||
|
|
||||||
func TestScanEventTestSuite(t *testing.T) {
|
func TestScanEventTestSuite(t *testing.T) {
|
||||||
suite.Run(t, &scanEventTestSuite{})
|
suite.Run(t, &scanEventTestSuite{})
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ const (
|
|||||||
TopicCreateTag = "CREATE_TAG"
|
TopicCreateTag = "CREATE_TAG"
|
||||||
TopicDeleteTag = "DELETE_TAG"
|
TopicDeleteTag = "DELETE_TAG"
|
||||||
TopicScanningFailed = "SCANNING_FAILED"
|
TopicScanningFailed = "SCANNING_FAILED"
|
||||||
|
TopicScanningStopped = "SCANNING_STOPPED"
|
||||||
TopicScanningCompleted = "SCANNING_COMPLETED"
|
TopicScanningCompleted = "SCANNING_COMPLETED"
|
||||||
// QuotaExceedTopic is topic for quota warning event, the usage reaches the warning bar of limitation, like 85%
|
// QuotaExceedTopic is topic for quota warning event, the usage reaches the warning bar of limitation, like 85%
|
||||||
TopicQuotaWarning = "QUOTA_WARNING"
|
TopicQuotaWarning = "QUOTA_WARNING"
|
||||||
|
@ -313,6 +313,24 @@ func (bc *basicController) Scan(ctx context.Context, artifact *ar.Artifact, opti
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop scan job of a given artifact
|
||||||
|
func (bc *basicController) Stop(ctx context.Context, artifact *ar.Artifact) error {
|
||||||
|
if artifact == nil {
|
||||||
|
return errors.New("nil artifact to stop scan")
|
||||||
|
}
|
||||||
|
query := q.New(q.KeyWords{"extra_attrs.artifact.digest": artifact.Digest})
|
||||||
|
executions, err := bc.execMgr.List(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(executions) == 0 {
|
||||||
|
message := fmt.Sprintf("no scan job for artifact digest=%v", artifact.Digest)
|
||||||
|
return errors.BadRequestError(nil).WithMessage(message)
|
||||||
|
}
|
||||||
|
execution := executions[0]
|
||||||
|
return bc.execMgr.Stop(ctx, execution.ID)
|
||||||
|
}
|
||||||
|
|
||||||
func (bc *basicController) ScanAll(ctx context.Context, trigger string, async bool) (int64, error) {
|
func (bc *basicController) ScanAll(ctx context.Context, trigger string, async bool) (int64, error) {
|
||||||
executionID, err := bc.execMgr.Create(ctx, VendorTypeScanAll, 0, trigger)
|
executionID, err := bc.execMgr.Create(ctx, VendorTypeScanAll, 0, trigger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -358,6 +358,45 @@ func (suite *ControllerTestSuite) TestScanControllerScan() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestScanControllerStop ...
|
||||||
|
func (suite *ControllerTestSuite) TestScanControllerStop() {
|
||||||
|
{
|
||||||
|
// artifact not provieded
|
||||||
|
suite.Require().Error(suite.c.Stop(context.TODO(), nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// success
|
||||||
|
mock.OnAnything(suite.execMgr, "List").Return([]*task.Execution{
|
||||||
|
{ExtraAttrs: suite.makeExtraAttrs("rp-uuid-001"), Status: "Running"},
|
||||||
|
}, nil).Once()
|
||||||
|
mock.OnAnything(suite.execMgr, "Stop").Return(nil).Once()
|
||||||
|
|
||||||
|
ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{})
|
||||||
|
|
||||||
|
suite.Require().NoError(suite.c.Stop(ctx, suite.artifact))
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// failed due to no execution returned by List
|
||||||
|
mock.OnAnything(suite.execMgr, "List").Return([]*task.Execution{}, nil).Once()
|
||||||
|
mock.OnAnything(suite.execMgr, "Stop").Return(nil).Once()
|
||||||
|
|
||||||
|
ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{})
|
||||||
|
|
||||||
|
suite.Require().Error(suite.c.Stop(ctx, suite.artifact))
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// failed due to execMgr.List() errored out
|
||||||
|
mock.OnAnything(suite.execMgr, "List").Return([]*task.Execution{}, fmt.Errorf("failed to call execMgr.List()")).Once()
|
||||||
|
|
||||||
|
ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{})
|
||||||
|
|
||||||
|
suite.Require().Error(suite.c.Stop(ctx, suite.artifact))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestScanControllerGetReport ...
|
// TestScanControllerGetReport ...
|
||||||
func (suite *ControllerTestSuite) TestScanControllerGetReport() {
|
func (suite *ControllerTestSuite) TestScanControllerGetReport() {
|
||||||
mock.OnAnything(suite.ar, "Walk").Return(nil).Run(func(args mock.Arguments) {
|
mock.OnAnything(suite.ar, "Walk").Return(nil).Run(func(args mock.Arguments) {
|
||||||
|
@ -50,6 +50,16 @@ type Controller interface {
|
|||||||
// error : non nil error if any errors occurred
|
// error : non nil error if any errors occurred
|
||||||
Scan(ctx context.Context, artifact *artifact.Artifact, options ...Option) error
|
Scan(ctx context.Context, artifact *artifact.Artifact, options ...Option) error
|
||||||
|
|
||||||
|
// Stop scan job of the given artifact
|
||||||
|
//
|
||||||
|
// Arguments:
|
||||||
|
// ctx context.Context : the context for this method
|
||||||
|
// artifact *artifact.Artifact : the artifact whose scan job to be stopped
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// error : non nil error if any errors occurred
|
||||||
|
Stop(ctx context.Context, artifact *artifact.Artifact) error
|
||||||
|
|
||||||
// GetReport gets the reports for the given artifact identified by the digest
|
// GetReport gets the reports for the given artifact identified by the digest
|
||||||
//
|
//
|
||||||
// Arguments:
|
// Arguments:
|
||||||
|
@ -58,6 +58,7 @@ func initSupportedNotifyType() {
|
|||||||
event.TopicQuotaExceed,
|
event.TopicQuotaExceed,
|
||||||
event.TopicQuotaWarning,
|
event.TopicQuotaWarning,
|
||||||
event.TopicScanningFailed,
|
event.TopicScanningFailed,
|
||||||
|
event.TopicScanningStopped,
|
||||||
event.TopicScanningCompleted,
|
event.TopicScanningCompleted,
|
||||||
event.TopicReplication,
|
event.TopicReplication,
|
||||||
event.TopicTagRetention,
|
event.TopicTagRetention,
|
||||||
|
@ -146,6 +146,16 @@ func (j *Job) Run(ctx job.Context, params job.Parameters) error {
|
|||||||
// Get logger
|
// Get logger
|
||||||
myLogger := ctx.GetLogger()
|
myLogger := ctx.GetLogger()
|
||||||
|
|
||||||
|
// shouldStop checks if the job should be stopped
|
||||||
|
shouldStop := func() bool {
|
||||||
|
if cmd, ok := ctx.OPCommand(); ok && cmd == job.StopCommand {
|
||||||
|
myLogger.Info("scan job being stopped")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Ignore errors as they have been validated already
|
// Ignore errors as they have been validated already
|
||||||
r, _ := extractRegistration(params)
|
r, _ := extractRegistration(params)
|
||||||
req, _ := ExtractScanReq(params)
|
req, _ := ExtractScanReq(params)
|
||||||
@ -156,6 +166,10 @@ func (j *Job) Run(ctx job.Context, params job.Parameters) error {
|
|||||||
printJSONParameter(JobParameterRequest, removeAuthInfo(req), myLogger)
|
printJSONParameter(JobParameterRequest, removeAuthInfo(req), myLogger)
|
||||||
myLogger.Infof("Report mime types: %v\n", mimeTypes)
|
myLogger.Infof("Report mime types: %v\n", mimeTypes)
|
||||||
|
|
||||||
|
if shouldStop() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Submit scan request to the scanner adapter
|
// Submit scan request to the scanner adapter
|
||||||
client, err := r.Client(v1.DefaultClientPool)
|
client, err := r.Client(v1.DefaultClientPool)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -182,6 +196,10 @@ func (j *Job) Run(ctx job.Context, params job.Parameters) error {
|
|||||||
logAndWrapError(myLogger, err, "scan job: make authorization")
|
logAndWrapError(myLogger, err, "scan job: make authorization")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if shouldStop() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
req.Registry.Authorization = authorization
|
req.Registry.Authorization = authorization
|
||||||
resp, err := client.SubmitScan(req)
|
resp, err := client.SubmitScan(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -210,6 +228,10 @@ func (j *Job) Run(ctx job.Context, params job.Parameters) error {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case t := <-tm.C:
|
case t := <-tm.C:
|
||||||
|
if shouldStop() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
myLogger.Debugf("check scan report for mime %s at %s", m, t.Format("2006/01/02 15:04:05"))
|
myLogger.Debugf("check scan report for mime %s at %s", m, t.Format("2006/01/02 15:04:05"))
|
||||||
|
|
||||||
rawReport, err := client.GetScanReport(resp.ID, m)
|
rawReport, err := client.GetScanReport(resp.ID, m)
|
||||||
@ -250,6 +272,10 @@ func (j *Job) Run(ctx job.Context, params job.Parameters) error {
|
|||||||
// Wait for all the retrieving routines are completed
|
// Wait for all the retrieving routines are completed
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
|
if shouldStop() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Merge errors
|
// Merge errors
|
||||||
for _, e := range errs {
|
for _, e := range errs {
|
||||||
if e != nil {
|
if e != nil {
|
||||||
|
@ -65,6 +65,7 @@ func (suite *JobTestSuite) TestJob() {
|
|||||||
lg := &mockjobservice.MockJobLogger{}
|
lg := &mockjobservice.MockJobLogger{}
|
||||||
|
|
||||||
ctx.On("GetLogger").Return(lg)
|
ctx.On("GetLogger").Return(lg)
|
||||||
|
ctx.On("OPCommand").Return(job.NilCommand, false)
|
||||||
|
|
||||||
r := &scanner.Registration{
|
r := &scanner.Registration{
|
||||||
ID: 0,
|
ID: 0,
|
||||||
|
@ -113,9 +113,8 @@ func GenerateNativeSummary(r *scan.Report, options ...Option) (interface{}, erro
|
|||||||
|
|
||||||
sum.TotalCount = 1
|
sum.TotalCount = 1
|
||||||
|
|
||||||
// If the status is not success/stopped, there will not be any report.
|
// If the status is not success, there will not be any report.
|
||||||
if r.Status != job.SuccessStatus.String() &&
|
if r.Status != job.SuccessStatus.String() {
|
||||||
r.Status != job.StoppedStatus.String() {
|
|
||||||
return sum, nil
|
return sum, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,6 +85,8 @@ func MergeScanStatus(s1, s2 string) string {
|
|||||||
|
|
||||||
if j1 == job.RunningStatus || j2 == job.RunningStatus {
|
if j1 == job.RunningStatus || j2 == job.RunningStatus {
|
||||||
return job.RunningStatus.String()
|
return job.RunningStatus.String()
|
||||||
|
} else if j1 == job.StoppedStatus || j2 == job.StoppedStatus {
|
||||||
|
return job.StoppedStatus.String()
|
||||||
} else if j1 == job.SuccessStatus || j2 == job.SuccessStatus {
|
} else if j1 == job.SuccessStatus || j2 == job.SuccessStatus {
|
||||||
// the scan status of the image index will be treated as a success when one of its children is success
|
// the scan status of the image index will be treated as a success when one of its children is success
|
||||||
return job.SuccessStatus.String()
|
return job.SuccessStatus.String()
|
||||||
|
@ -71,6 +71,7 @@ func Test_mergeScanStatus(t *testing.T) {
|
|||||||
errorStatus := job.ErrorStatus.String()
|
errorStatus := job.ErrorStatus.String()
|
||||||
runningStatus := job.RunningStatus.String()
|
runningStatus := job.RunningStatus.String()
|
||||||
successStatus := job.SuccessStatus.String()
|
successStatus := job.SuccessStatus.String()
|
||||||
|
stoppedStatus := job.StoppedStatus.String()
|
||||||
|
|
||||||
type args struct {
|
type args struct {
|
||||||
s1 string
|
s1 string
|
||||||
@ -88,6 +89,9 @@ func Test_mergeScanStatus(t *testing.T) {
|
|||||||
{"success and success", args{successStatus, successStatus}, successStatus},
|
{"success and success", args{successStatus, successStatus}, successStatus},
|
||||||
{"error and error", args{errorStatus, errorStatus}, errorStatus},
|
{"error and error", args{errorStatus, errorStatus}, errorStatus},
|
||||||
{"error and empty string", args{errorStatus, ""}, errorStatus},
|
{"error and empty string", args{errorStatus, ""}, errorStatus},
|
||||||
|
{"running and stopped", args{runningStatus, stoppedStatus}, runningStatus},
|
||||||
|
{"success and stopped", args{successStatus, stoppedStatus}, stoppedStatus},
|
||||||
|
{"stopped and stopped", args{stoppedStatus, stoppedStatus}, stoppedStatus},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
@ -329,11 +329,25 @@ func (e *executionDAO) querySetter(ctx context.Context, query *q.Query) (orm.Que
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(keyPrefix) == 0 {
|
if len(keyPrefix) == 0 || keyPrefix == key {
|
||||||
return qs, nil
|
return qs, nil
|
||||||
}
|
}
|
||||||
inClause, err := orm.CreateInClause(ctx, "select id from execution where extra_attrs->>?=?",
|
|
||||||
strings.TrimPrefix(key, keyPrefix), value)
|
// key with keyPrefix supports multi-level query operator on PostgreSQL JSON data
|
||||||
|
// examples:
|
||||||
|
// key = extra_attrs.id,
|
||||||
|
// ==> sql = "select id from execution where extra_attrs->>?=?", args = {id, value}
|
||||||
|
// key = extra_attrs.artifact.digest
|
||||||
|
// ==> sql = "select id from execution where extra_attrs->?->>?=?", args = {artifact, id, value}
|
||||||
|
// key = extra_attrs.a.b.c
|
||||||
|
// ==> sql = "select id from execution where extra_attrs->?->?->>?=?", args = {a, b, c, value}
|
||||||
|
keys := strings.Split(strings.TrimPrefix(key, keyPrefix), ".")
|
||||||
|
var args []interface{}
|
||||||
|
for _, item := range keys {
|
||||||
|
args = append(args, item)
|
||||||
|
}
|
||||||
|
args = append(args, value)
|
||||||
|
inClause, err := orm.CreateInClause(ctx, buildInClauseSqlForExtraAttrs(keys), args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -342,3 +356,23 @@ func (e *executionDAO) querySetter(ctx context.Context, query *q.Query) (orm.Que
|
|||||||
|
|
||||||
return qs, nil
|
return qs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Param keys is strings.Split() after trim "extra_attrs."/"ExtraAttrs." prefix
|
||||||
|
func buildInClauseSqlForExtraAttrs(keys []string) string {
|
||||||
|
switch len(keys) {
|
||||||
|
case 0:
|
||||||
|
// won't fall into this case, as the if condition on "keyPrefix == key"
|
||||||
|
// act as a place holder to ensure "default" is equivalent to "len(keys) >= 2"
|
||||||
|
return ""
|
||||||
|
case 1:
|
||||||
|
return fmt.Sprintf("select id from execution where extra_attrs->>?=?")
|
||||||
|
default:
|
||||||
|
// len(keys) >= 2
|
||||||
|
elements := make([]string, len(keys)-1)
|
||||||
|
for i := range elements {
|
||||||
|
elements[i] = "?"
|
||||||
|
}
|
||||||
|
s := strings.Join(elements, "->")
|
||||||
|
return fmt.Sprintf("select id from execution where extra_attrs->%s->>?=?", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -329,3 +329,26 @@ func (e *executionDAOTestSuite) TestRefreshStatus() {
|
|||||||
func TestExecutionDAOSuite(t *testing.T) {
|
func TestExecutionDAOSuite(t *testing.T) {
|
||||||
suite.Run(t, &executionDAOTestSuite{})
|
suite.Run(t, &executionDAOTestSuite{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_buildInClauseSqlForExtraAttrs(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
keys []string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"extra_attrs.", args{[]string{}}, ""},
|
||||||
|
{"extra_attrs.id", args{[]string{"id"}}, "select id from execution where extra_attrs->>?=?"},
|
||||||
|
{"extra_attrs.artifact.digest", args{[]string{"artifact", "digest"}}, "select id from execution where extra_attrs->?->>?=?"},
|
||||||
|
{"extra_attrs.a.b.c", args{[]string{"a", "b", "c"}}, "select id from execution where extra_attrs->?->?->>?=?"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := buildInClauseSqlForExtraAttrs(tt.args.keys); got != tt.want {
|
||||||
|
t.Errorf("buildInClauseSqlForExtraAttrs() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -24,6 +24,7 @@ const EVENT_TYPES_TEXT_MAP = {
|
|||||||
'QUOTA_EXCEED': 'Quota exceed',
|
'QUOTA_EXCEED': 'Quota exceed',
|
||||||
'QUOTA_WARNING': 'Quota near threshold',
|
'QUOTA_WARNING': 'Quota near threshold',
|
||||||
'SCANNING_FAILED': 'Scanning failed',
|
'SCANNING_FAILED': 'Scanning failed',
|
||||||
|
'SCANNING_STOPPED': 'Scanning stopped',
|
||||||
'SCANNING_COMPLETED': 'Scanning finished',
|
'SCANNING_COMPLETED': 'Scanning finished',
|
||||||
'TAG_RETENTION': 'Tag retention finished',
|
'TAG_RETENTION': 'Tag retention finished',
|
||||||
};
|
};
|
||||||
|
@ -48,6 +48,24 @@ func (s *scanAPI) Prepare(ctx context.Context, operation string, params interfac
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *scanAPI) StopScanArtifact(ctx context.Context, params operation.StopScanArtifactParams) middleware.Responder {
|
||||||
|
if err := s.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionStop, rbac.ResourceScan); err != nil {
|
||||||
|
return s.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the artifact
|
||||||
|
curArtifact, err := s.artCtl.GetByReference(ctx, fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName), params.Reference, nil)
|
||||||
|
if err != nil {
|
||||||
|
return s.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.scanCtl.Stop(ctx, curArtifact); err != nil {
|
||||||
|
return s.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return operation.NewStopScanArtifactAccepted()
|
||||||
|
}
|
||||||
|
|
||||||
func (s *scanAPI) ScanArtifact(ctx context.Context, params operation.ScanArtifactParams) middleware.Responder {
|
func (s *scanAPI) ScanArtifact(ctx context.Context, params operation.ScanArtifactParams) middleware.Responder {
|
||||||
if err := s.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionCreate, rbac.ResourceScan); err != nil {
|
if err := s.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionCreate, rbac.ResourceScan); err != nil {
|
||||||
return s.SendError(ctx, err)
|
return s.SendError(ctx, err)
|
||||||
|
@ -25,6 +25,8 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/controller/scanner"
|
"github.com/goharbor/harbor/src/controller/scanner"
|
||||||
"github.com/goharbor/harbor/src/jobservice/job"
|
"github.com/goharbor/harbor/src/jobservice/job"
|
||||||
"github.com/goharbor/harbor/src/lib/errors"
|
"github.com/goharbor/harbor/src/lib/errors"
|
||||||
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
|
"github.com/goharbor/harbor/src/lib/orm"
|
||||||
"github.com/goharbor/harbor/src/lib/q"
|
"github.com/goharbor/harbor/src/lib/q"
|
||||||
"github.com/goharbor/harbor/src/pkg/scheduler"
|
"github.com/goharbor/harbor/src/pkg/scheduler"
|
||||||
"github.com/goharbor/harbor/src/pkg/task"
|
"github.com/goharbor/harbor/src/pkg/task"
|
||||||
@ -54,6 +56,30 @@ func (s *scanAllAPI) Prepare(ctx context.Context, operation string, params inter
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StopScanAll stops the execution of scan all artifacts.
|
||||||
|
func (s *scanAllAPI) StopScanAll(ctx context.Context, params operation.StopScanAllParams) middleware.Responder {
|
||||||
|
if err := s.requireAccess(ctx, rbac.ActionStop); err != nil {
|
||||||
|
return s.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
execution, err := s.getLatestScanAllExecution(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return s.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
if execution == nil {
|
||||||
|
message := fmt.Sprintf("no scan all job is found currently")
|
||||||
|
return s.SendError(ctx, errors.BadRequestError(nil).WithMessage(message))
|
||||||
|
}
|
||||||
|
go func(ctx context.Context, eid int64) {
|
||||||
|
err := s.execMgr.Stop(ctx, eid)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to stop the execution of executionID=%+v", execution.ID)
|
||||||
|
}
|
||||||
|
}(orm.Context(), execution.ID)
|
||||||
|
|
||||||
|
return operation.NewStopScanAllAccepted()
|
||||||
|
}
|
||||||
|
|
||||||
func (s *scanAllAPI) CreateScanAllSchedule(ctx context.Context, params operation.CreateScanAllScheduleParams) middleware.Responder {
|
func (s *scanAllAPI) CreateScanAllSchedule(ctx context.Context, params operation.CreateScanAllScheduleParams) middleware.Responder {
|
||||||
if err := s.requireAccess(ctx, rbac.ActionCreate); err != nil {
|
if err := s.requireAccess(ctx, rbac.ActionCreate); err != nil {
|
||||||
return s.SendError(ctx, err)
|
return s.SendError(ctx, err)
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/common/dao"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
||||||
"github.com/goharbor/harbor/src/pkg/scheduler"
|
"github.com/goharbor/harbor/src/pkg/scheduler"
|
||||||
"github.com/goharbor/harbor/src/pkg/task"
|
"github.com/goharbor/harbor/src/pkg/task"
|
||||||
@ -51,6 +52,9 @@ type ScanAllTestSuite struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ScanAllTestSuite) SetupSuite() {
|
func (suite *ScanAllTestSuite) SetupSuite() {
|
||||||
|
// this is because orm.Context()
|
||||||
|
dao.PrepareTestForPostgresSQL()
|
||||||
|
|
||||||
suite.execution = &task.Execution{
|
suite.execution = &task.Execution{
|
||||||
Status: "Running",
|
Status: "Running",
|
||||||
Metrics: &taskdao.Metrics{
|
Metrics: &taskdao.Metrics{
|
||||||
@ -238,6 +242,41 @@ func (suite *ScanAllTestSuite) TestGetLatestScheduledScanAllMetrics() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *ScanAllTestSuite) TestStopScanAll() {
|
||||||
|
times := 3
|
||||||
|
suite.Security.On("IsAuthenticated").Return(true).Times(times)
|
||||||
|
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Times(times)
|
||||||
|
mock.OnAnything(suite.scannerCtl, "ListRegistrations").Return([]*scanner.Registration{{ID: int64(1)}}, nil).Times(times)
|
||||||
|
|
||||||
|
{
|
||||||
|
// create stop scan all but get latest scan all execution failed
|
||||||
|
mock.OnAnything(suite.execMgr, "List").Return(nil, fmt.Errorf("list executions failed")).Once()
|
||||||
|
|
||||||
|
res, err := suite.Post("/system/scanAll/stop", nil)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(500, res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// create stop scan all but no latest scan all execution
|
||||||
|
mock.OnAnything(suite.execMgr, "List").Return([]*task.Execution{}, nil).Once()
|
||||||
|
|
||||||
|
res, err := suite.Post("/system/scanAll/stop", nil)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(400, res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// successfully stop scan all
|
||||||
|
mock.OnAnything(suite.execMgr, "List").Return([]*task.Execution{suite.execution}, nil).Once()
|
||||||
|
mock.OnAnything(suite.execMgr, "Stop").Return(nil).Once()
|
||||||
|
|
||||||
|
res, err := suite.Post("/system/scanAll/stop", nil)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(202, res.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (suite *ScanAllTestSuite) TestCreateScanAllSchedule() {
|
func (suite *ScanAllTestSuite) TestCreateScanAllSchedule() {
|
||||||
times := 11
|
times := 11
|
||||||
suite.Security.On("IsAuthenticated").Return(true).Times(times)
|
suite.Security.On("IsAuthenticated").Return(true).Times(times)
|
||||||
|
102
src/server/v2.0/handler/scan_test.go
Normal file
102
src/server/v2.0/handler/scan_test.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/controller/artifact"
|
||||||
|
"github.com/goharbor/harbor/src/controller/project"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/task"
|
||||||
|
"github.com/goharbor/harbor/src/server/v2.0/restapi"
|
||||||
|
artifacttesting "github.com/goharbor/harbor/src/testing/controller/artifact"
|
||||||
|
projecttesting "github.com/goharbor/harbor/src/testing/controller/project"
|
||||||
|
scantesting "github.com/goharbor/harbor/src/testing/controller/scan"
|
||||||
|
"github.com/goharbor/harbor/src/testing/mock"
|
||||||
|
htesting "github.com/goharbor/harbor/src/testing/server/v2.0/handler"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ScanTestSuite struct {
|
||||||
|
htesting.Suite
|
||||||
|
|
||||||
|
artifactCtl *artifacttesting.Controller
|
||||||
|
scanCtl *scantesting.Controller
|
||||||
|
|
||||||
|
execution *task.Execution
|
||||||
|
projectCtlMock *projecttesting.Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ScanTestSuite) SetupSuite() {
|
||||||
|
suite.execution = &task.Execution{
|
||||||
|
Status: "Running",
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.scanCtl = &scantesting.Controller{}
|
||||||
|
suite.artifactCtl = &artifacttesting.Controller{}
|
||||||
|
|
||||||
|
suite.Config = &restapi.Config{
|
||||||
|
ScanAPI: &scanAPI{
|
||||||
|
artCtl: suite.artifactCtl,
|
||||||
|
scanCtl: suite.scanCtl,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.Suite.SetupSuite()
|
||||||
|
|
||||||
|
mock.OnAnything(projectCtlMock, "GetByName").Return(&project.Project{ProjectID: 1}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ScanTestSuite) TestStopScan() {
|
||||||
|
times := 3
|
||||||
|
suite.Security.On("IsAuthenticated").Return(true).Times(times)
|
||||||
|
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Times(times)
|
||||||
|
|
||||||
|
url := "/projects/library/repositories/nginx/artifacts/sha256:e4f0474a75c510f40b37b6b7dc2516241ffa8bde5a442bde3d372c9519c84d90/scan/stop"
|
||||||
|
|
||||||
|
{
|
||||||
|
// failed to get artifact by reference
|
||||||
|
mock.OnAnything(suite.artifactCtl, "GetByReference").Return(&artifact.Artifact{}, fmt.Errorf("failed to get artifact by reference")).Once()
|
||||||
|
|
||||||
|
res, err := suite.Post(url, nil)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(500, res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// get nil artifact by reference
|
||||||
|
mock.OnAnything(suite.artifactCtl, "GetByReference").Return(nil, nil).Once()
|
||||||
|
mock.OnAnything(suite.scanCtl, "Stop").Return(fmt.Errorf("nil artifact to stop scan")).Once()
|
||||||
|
|
||||||
|
res, err := suite.Post(url, nil)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(500, res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// successfully stop scan artifact
|
||||||
|
mock.OnAnything(suite.artifactCtl, "GetByReference").Return(&artifact.Artifact{}, nil).Once()
|
||||||
|
mock.OnAnything(suite.scanCtl, "Stop").Return(nil).Once()
|
||||||
|
|
||||||
|
res, err := suite.Post(url, nil)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(202, res.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, &ScanTestSuite{})
|
||||||
|
}
|
@ -175,3 +175,17 @@ func (_m *Controller) ScanAll(ctx context.Context, trigger string, async bool) (
|
|||||||
|
|
||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop provides a mock function with given fields: ctx, _a1
|
||||||
|
func (_m *Controller) Stop(ctx context.Context, _a1 *artifact.Artifact) error {
|
||||||
|
ret := _m.Called(ctx, _a1)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact) error); ok {
|
||||||
|
r0 = rf(ctx, _a1)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user