mirror of
https://github.com/goharbor/harbor.git
synced 2025-02-27 01:02:34 +01: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'
|
||||
'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
|
||||
|
@ -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
|
||||
|
@ -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},
|
||||
|
||||
|
@ -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},
|
||||
|
||||
|
@ -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{})
|
||||
|
@ -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:
|
||||
|
@ -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{})
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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:
|
||||
|
@ -58,6 +58,7 @@ func initSupportedNotifyType() {
|
||||
event.TopicQuotaExceed,
|
||||
event.TopicQuotaWarning,
|
||||
event.TopicScanningFailed,
|
||||
event.TopicScanningStopped,
|
||||
event.TopicScanningCompleted,
|
||||
event.TopicReplication,
|
||||
event.TopicTagRetention,
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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',
|
||||
};
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
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
|
||||
}
|
||||
|
||||
// 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