From e2e3bcca1c38e5041ea6f1d22409309818f100b7 Mon Sep 17 00:00:00 2001 From: Shengwen Yu Date: Sun, 15 Aug 2021 17:21:39 +0800 Subject: [PATCH] feat: add stop scan & stop scan-all feature Signed-off-by: Shengwen Yu --- api/v2.0/swagger.yaml | 45 ++++++++ src/common/rbac/const.go | 1 + src/common/rbac/project/rbac_role.go | 2 + src/common/rbac/system/policies.go | 1 + src/controller/event/handler/init.go | 1 + src/controller/event/metadata/scan.go | 5 +- src/controller/event/metadata/scan_test.go | 42 ++++++++ src/controller/event/topic.go | 1 + src/controller/scan/base_controller.go | 18 ++++ src/controller/scan/base_controller_test.go | 39 +++++++ src/controller/scan/controller.go | 10 ++ src/pkg/notification/notification.go | 1 + src/pkg/scan/job.go | 26 +++++ src/pkg/scan/job_test.go | 1 + src/pkg/scan/report/summary.go | 5 +- src/pkg/scan/vuln/util.go | 2 + src/pkg/scan/vuln/util_test.go | 4 + src/pkg/task/dao/execution.go | 40 ++++++- src/pkg/task/dao/execution_test.go | 23 ++++ .../base/project/webhook/webhook.service.ts | 1 + src/server/v2.0/handler/scan.go | 18 ++++ src/server/v2.0/handler/scan_all.go | 26 +++++ src/server/v2.0/handler/scan_all_test.go | 39 +++++++ src/server/v2.0/handler/scan_test.go | 102 ++++++++++++++++++ src/testing/controller/scan/controller.go | 14 +++ 25 files changed, 460 insertions(+), 7 deletions(-) create mode 100644 src/server/v2.0/handler/scan_test.go diff --git a/api/v2.0/swagger.yaml b/api/v2.0/swagger.yaml index 049e9222a..461011f9c 100644 --- a/api/v2.0/swagger.yaml +++ b/api/v2.0/swagger.yaml @@ -1166,6 +1166,31 @@ paths: $ref: '#/responses/404' '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: get: summary: Get the log of the scan report @@ -4167,6 +4192,26 @@ paths: $ref: '#/responses/412' '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: get: operationId: getPing diff --git a/src/common/rbac/const.go b/src/common/rbac/const.go index 3e45f5350..256c7c67f 100755 --- a/src/common/rbac/const.go +++ b/src/common/rbac/const.go @@ -30,6 +30,7 @@ const ( ActionOperate = Action("operate") 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 diff --git a/src/common/rbac/project/rbac_role.go b/src/common/rbac/project/rbac_role.go index dd4bfa27d..8d289f665 100644 --- a/src/common/rbac/project/rbac_role.go +++ b/src/common/rbac/project/rbac_role.go @@ -98,6 +98,7 @@ var ( {Resource: rbac.ResourceScan, Action: rbac.ActionCreate}, {Resource: rbac.ResourceScan, Action: rbac.ActionRead}, + {Resource: rbac.ResourceScan, Action: rbac.ActionStop}, {Resource: rbac.ResourceScanner, Action: rbac.ActionRead}, {Resource: rbac.ResourceScanner, Action: rbac.ActionCreate}, @@ -185,6 +186,7 @@ var ( {Resource: rbac.ResourceScan, Action: rbac.ActionCreate}, {Resource: rbac.ResourceScan, Action: rbac.ActionRead}, + {Resource: rbac.ResourceScan, Action: rbac.ActionStop}, {Resource: rbac.ResourceScanner, Action: rbac.ActionRead}, diff --git a/src/common/rbac/system/policies.go b/src/common/rbac/system/policies.go index 6bb28e440..611ccf5e2 100644 --- a/src/common/rbac/system/policies.go +++ b/src/common/rbac/system/policies.go @@ -58,6 +58,7 @@ var ( {Resource: rbac.ResourceScanAll, Action: rbac.ActionUpdate}, {Resource: rbac.ResourceScanAll, Action: rbac.ActionDelete}, {Resource: rbac.ResourceScanAll, Action: rbac.ActionList}, + {Resource: rbac.ResourceScanAll, Action: rbac.ActionStop}, {Resource: rbac.ResourceSystemVolumes, Action: rbac.ActionRead}, diff --git a/src/controller/event/handler/init.go b/src/controller/event/handler/init.go index e81405d2c..0df44d765 100644 --- a/src/controller/event/handler/init.go +++ b/src/controller/event/handler/init.go @@ -30,6 +30,7 @@ func init() { notifier.Subscribe(event.TopicQuotaExceed, "a.Handler{}) notifier.Subscribe(event.TopicQuotaWarning, "a.Handler{}) notifier.Subscribe(event.TopicScanningFailed, &scan.Handler{}) + notifier.Subscribe(event.TopicScanningStopped, &scan.Handler{}) notifier.Subscribe(event.TopicScanningCompleted, &scan.Handler{}) notifier.Subscribe(event.TopicDeleteArtifact, &scan.DelArtHandler{}) notifier.Subscribe(event.TopicReplication, &artifact.ReplicationHandler{}) diff --git a/src/controller/event/metadata/scan.go b/src/controller/event/metadata/scan.go index 5f7f85c2d..9ec6f0c24 100644 --- a/src/controller/event/metadata/scan.go +++ b/src/controller/event/metadata/scan.go @@ -29,7 +29,10 @@ func (si *ScanImageMetaData) Resolve(evt *event.Event) error { case job.SuccessStatus: eventType = 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 topic = event2.TopicScanningFailed default: diff --git a/src/controller/event/metadata/scan_test.go b/src/controller/event/metadata/scan_test.go index 13f795460..816e9338b 100644 --- a/src/controller/event/metadata/scan_test.go +++ b/src/controller/event/metadata/scan_test.go @@ -49,6 +49,48 @@ func (r *scanEventTestSuite) TestResolveOfScanImageEventMetadata() { 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) { suite.Run(t, &scanEventTestSuite{}) } diff --git a/src/controller/event/topic.go b/src/controller/event/topic.go index 26e616323..e4f40d074 100644 --- a/src/controller/event/topic.go +++ b/src/controller/event/topic.go @@ -38,6 +38,7 @@ const ( TopicCreateTag = "CREATE_TAG" TopicDeleteTag = "DELETE_TAG" TopicScanningFailed = "SCANNING_FAILED" + TopicScanningStopped = "SCANNING_STOPPED" TopicScanningCompleted = "SCANNING_COMPLETED" // QuotaExceedTopic is topic for quota warning event, the usage reaches the warning bar of limitation, like 85% TopicQuotaWarning = "QUOTA_WARNING" diff --git a/src/controller/scan/base_controller.go b/src/controller/scan/base_controller.go index 7d2b04067..951991bd6 100644 --- a/src/controller/scan/base_controller.go +++ b/src/controller/scan/base_controller.go @@ -313,6 +313,24 @@ func (bc *basicController) Scan(ctx context.Context, artifact *ar.Artifact, opti 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) { executionID, err := bc.execMgr.Create(ctx, VendorTypeScanAll, 0, trigger) if err != nil { diff --git a/src/controller/scan/base_controller_test.go b/src/controller/scan/base_controller_test.go index d8da439a3..ac56105d0 100644 --- a/src/controller/scan/base_controller_test.go +++ b/src/controller/scan/base_controller_test.go @@ -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 ... func (suite *ControllerTestSuite) TestScanControllerGetReport() { mock.OnAnything(suite.ar, "Walk").Return(nil).Run(func(args mock.Arguments) { diff --git a/src/controller/scan/controller.go b/src/controller/scan/controller.go index 191047d04..773e83688 100644 --- a/src/controller/scan/controller.go +++ b/src/controller/scan/controller.go @@ -50,6 +50,16 @@ type Controller interface { // error : non nil error if any errors occurred 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 // // Arguments: diff --git a/src/pkg/notification/notification.go b/src/pkg/notification/notification.go index a244c8d97..774f8658c 100755 --- a/src/pkg/notification/notification.go +++ b/src/pkg/notification/notification.go @@ -58,6 +58,7 @@ func initSupportedNotifyType() { event.TopicQuotaExceed, event.TopicQuotaWarning, event.TopicScanningFailed, + event.TopicScanningStopped, event.TopicScanningCompleted, event.TopicReplication, event.TopicTagRetention, diff --git a/src/pkg/scan/job.go b/src/pkg/scan/job.go index 1d2a116a4..63978887b 100644 --- a/src/pkg/scan/job.go +++ b/src/pkg/scan/job.go @@ -146,6 +146,16 @@ func (j *Job) Run(ctx job.Context, params job.Parameters) error { // Get logger 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 r, _ := extractRegistration(params) req, _ := ExtractScanReq(params) @@ -156,6 +166,10 @@ func (j *Job) Run(ctx job.Context, params job.Parameters) error { printJSONParameter(JobParameterRequest, removeAuthInfo(req), myLogger) myLogger.Infof("Report mime types: %v\n", mimeTypes) + if shouldStop() { + return nil + } + // Submit scan request to the scanner adapter client, err := r.Client(v1.DefaultClientPool) 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") } + if shouldStop() { + return nil + } + req.Registry.Authorization = authorization resp, err := client.SubmitScan(req) if err != nil { @@ -210,6 +228,10 @@ func (j *Job) Run(ctx job.Context, params job.Parameters) error { for { select { 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")) 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 wg.Wait() + if shouldStop() { + return nil + } + // Merge errors for _, e := range errs { if e != nil { diff --git a/src/pkg/scan/job_test.go b/src/pkg/scan/job_test.go index 8a8bb2836..785e8ea4b 100644 --- a/src/pkg/scan/job_test.go +++ b/src/pkg/scan/job_test.go @@ -65,6 +65,7 @@ func (suite *JobTestSuite) TestJob() { lg := &mockjobservice.MockJobLogger{} ctx.On("GetLogger").Return(lg) + ctx.On("OPCommand").Return(job.NilCommand, false) r := &scanner.Registration{ ID: 0, diff --git a/src/pkg/scan/report/summary.go b/src/pkg/scan/report/summary.go index 1a8d47617..1181f9509 100644 --- a/src/pkg/scan/report/summary.go +++ b/src/pkg/scan/report/summary.go @@ -113,9 +113,8 @@ func GenerateNativeSummary(r *scan.Report, options ...Option) (interface{}, erro sum.TotalCount = 1 - // If the status is not success/stopped, there will not be any report. - if r.Status != job.SuccessStatus.String() && - r.Status != job.StoppedStatus.String() { + // If the status is not success, there will not be any report. + if r.Status != job.SuccessStatus.String() { return sum, nil } diff --git a/src/pkg/scan/vuln/util.go b/src/pkg/scan/vuln/util.go index 7e5fb5cb0..c8a80e8cb 100644 --- a/src/pkg/scan/vuln/util.go +++ b/src/pkg/scan/vuln/util.go @@ -85,6 +85,8 @@ func MergeScanStatus(s1, s2 string) string { if j1 == job.RunningStatus || j2 == job.RunningStatus { return job.RunningStatus.String() + } else if j1 == job.StoppedStatus || j2 == job.StoppedStatus { + return job.StoppedStatus.String() } 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 return job.SuccessStatus.String() diff --git a/src/pkg/scan/vuln/util_test.go b/src/pkg/scan/vuln/util_test.go index 988415b4e..796c7287b 100644 --- a/src/pkg/scan/vuln/util_test.go +++ b/src/pkg/scan/vuln/util_test.go @@ -71,6 +71,7 @@ func Test_mergeScanStatus(t *testing.T) { errorStatus := job.ErrorStatus.String() runningStatus := job.RunningStatus.String() successStatus := job.SuccessStatus.String() + stoppedStatus := job.StoppedStatus.String() type args struct { s1 string @@ -88,6 +89,9 @@ func Test_mergeScanStatus(t *testing.T) { {"success and success", args{successStatus, successStatus}, successStatus}, {"error and error", args{errorStatus, 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 { t.Run(tt.name, func(t *testing.T) { diff --git a/src/pkg/task/dao/execution.go b/src/pkg/task/dao/execution.go index 8923fc432..6911876f1 100644 --- a/src/pkg/task/dao/execution.go +++ b/src/pkg/task/dao/execution.go @@ -329,11 +329,25 @@ func (e *executionDAO) querySetter(ctx context.Context, query *q.Query) (orm.Que break } } - if len(keyPrefix) == 0 { + if len(keyPrefix) == 0 || keyPrefix == key { 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 { return nil, err } @@ -342,3 +356,23 @@ func (e *executionDAO) querySetter(ctx context.Context, query *q.Query) (orm.Que 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) + } +} diff --git a/src/pkg/task/dao/execution_test.go b/src/pkg/task/dao/execution_test.go index 1e54eed02..92fefb638 100644 --- a/src/pkg/task/dao/execution_test.go +++ b/src/pkg/task/dao/execution_test.go @@ -329,3 +329,26 @@ func (e *executionDAOTestSuite) TestRefreshStatus() { func TestExecutionDAOSuite(t *testing.T) { 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) + } + }) + } +} diff --git a/src/portal/src/app/base/project/webhook/webhook.service.ts b/src/portal/src/app/base/project/webhook/webhook.service.ts index acf2f234c..d171456c9 100644 --- a/src/portal/src/app/base/project/webhook/webhook.service.ts +++ b/src/portal/src/app/base/project/webhook/webhook.service.ts @@ -24,6 +24,7 @@ const EVENT_TYPES_TEXT_MAP = { 'QUOTA_EXCEED': 'Quota exceed', 'QUOTA_WARNING': 'Quota near threshold', 'SCANNING_FAILED': 'Scanning failed', + 'SCANNING_STOPPED': 'Scanning stopped', 'SCANNING_COMPLETED': 'Scanning finished', 'TAG_RETENTION': 'Tag retention finished', }; diff --git a/src/server/v2.0/handler/scan.go b/src/server/v2.0/handler/scan.go index c5ffc85da..e1f566550 100644 --- a/src/server/v2.0/handler/scan.go +++ b/src/server/v2.0/handler/scan.go @@ -48,6 +48,24 @@ func (s *scanAPI) Prepare(ctx context.Context, operation string, params interfac 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 { if err := s.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionCreate, rbac.ResourceScan); err != nil { return s.SendError(ctx, err) diff --git a/src/server/v2.0/handler/scan_all.go b/src/server/v2.0/handler/scan_all.go index c0ffddd4d..8bc807e1d 100644 --- a/src/server/v2.0/handler/scan_all.go +++ b/src/server/v2.0/handler/scan_all.go @@ -25,6 +25,8 @@ import ( "github.com/goharbor/harbor/src/controller/scanner" "github.com/goharbor/harbor/src/jobservice/job" "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/pkg/scheduler" "github.com/goharbor/harbor/src/pkg/task" @@ -54,6 +56,30 @@ func (s *scanAllAPI) Prepare(ctx context.Context, operation string, params inter 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 { if err := s.requireAccess(ctx, rbac.ActionCreate); err != nil { return s.SendError(ctx, err) diff --git a/src/server/v2.0/handler/scan_all_test.go b/src/server/v2.0/handler/scan_all_test.go index e13e5049f..59c79c62d 100644 --- a/src/server/v2.0/handler/scan_all_test.go +++ b/src/server/v2.0/handler/scan_all_test.go @@ -23,6 +23,7 @@ import ( "testing" "time" + "github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/pkg/scan/dao/scanner" "github.com/goharbor/harbor/src/pkg/scheduler" "github.com/goharbor/harbor/src/pkg/task" @@ -51,6 +52,9 @@ type ScanAllTestSuite struct { } func (suite *ScanAllTestSuite) SetupSuite() { + // this is because orm.Context() + dao.PrepareTestForPostgresSQL() + suite.execution = &task.Execution{ Status: "Running", 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() { times := 11 suite.Security.On("IsAuthenticated").Return(true).Times(times) diff --git a/src/server/v2.0/handler/scan_test.go b/src/server/v2.0/handler/scan_test.go new file mode 100644 index 000000000..47cd0bae6 --- /dev/null +++ b/src/server/v2.0/handler/scan_test.go @@ -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{}) +} diff --git a/src/testing/controller/scan/controller.go b/src/testing/controller/scan/controller.go index b70e6fc3c..1805e10f8 100644 --- a/src/testing/controller/scan/controller.go +++ b/src/testing/controller/scan/controller.go @@ -175,3 +175,17 @@ func (_m *Controller) ScanAll(ctx context.Context, trigger string, async bool) ( 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 +}