mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-22 16:48:30 +01:00
Merge pull request #9733 from steven-zou/fix/scan_all_enhancements
do improvements to the scan all job
This commit is contained in:
commit
240f771006
@ -27,6 +27,7 @@ CREATE TABLE scan_report
|
|||||||
mime_type VARCHAR(256) NOT NULL,
|
mime_type VARCHAR(256) NOT NULL,
|
||||||
job_id VARCHAR(64),
|
job_id VARCHAR(64),
|
||||||
track_id VARCHAR(64),
|
track_id VARCHAR(64),
|
||||||
|
requester VARCHAR(64),
|
||||||
status VARCHAR(1024) NOT NULL,
|
status VARCHAR(1024) NOT NULL,
|
||||||
status_code INTEGER DEFAULT 0,
|
status_code INTEGER DEFAULT 0,
|
||||||
status_rev BIGINT DEFAULT 0,
|
status_rev BIGINT DEFAULT 0,
|
||||||
@ -63,3 +64,7 @@ DROP TABLE IF EXISTS clair_vuln_timestamp;
|
|||||||
|
|
||||||
/* Add limited guest role */
|
/* Add limited guest role */
|
||||||
INSERT INTO role (role_code, name) VALUES ('LRS', 'limitedGuest');
|
INSERT INTO role (role_code, name) VALUES ('LRS', 'limitedGuest');
|
||||||
|
|
||||||
|
/* Add revision and status code columns for admin job table */
|
||||||
|
ALTER TABLE admin_job ADD COLUMN revision BIGINT DEFAULT 0;
|
||||||
|
ALTER TABLE admin_job ADD COLUMN status_code INTEGER DEFAULT 0;
|
@ -59,17 +59,35 @@ func DeleteAdminJob(id int64) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAdminJobStatus ...
|
// UpdateAdminJobStatus ...
|
||||||
func UpdateAdminJobStatus(id int64, status string) error {
|
func UpdateAdminJobStatus(id int64, status string, statusCode uint16, revision int64) error {
|
||||||
o := GetOrmer()
|
o := GetOrmer()
|
||||||
j := models.AdminJob{
|
qt := o.QueryTable(&models.AdminJob{})
|
||||||
ID: id,
|
|
||||||
Status: status,
|
// The generated sql statement example:{
|
||||||
UpdateTime: time.Now(),
|
//
|
||||||
}
|
// UPDATE "admin_job" SET "update_time" = $1, "status" = $2, "status_code" = $3, "revision" = $4
|
||||||
n, err := o.Update(&j, "Status", "UpdateTime")
|
// WHERE "id" IN ( SELECT T0."id" FROM "admin_job" T0 WHERE
|
||||||
|
// ( T0."revision" = $5 AND T0."status_code" < $6 ) OR ( T0."revision" < $7 )
|
||||||
|
// AND T0."id" = $8 )
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
cond := orm.NewCondition()
|
||||||
|
c1 := cond.And("revision", revision).And("status_code__lt", statusCode)
|
||||||
|
c2 := cond.And("revision__lt", revision)
|
||||||
|
c := cond.AndCond(c1).OrCond(c2)
|
||||||
|
|
||||||
|
data := make(orm.Params)
|
||||||
|
data["status"] = status
|
||||||
|
data["status_code"] = statusCode
|
||||||
|
data["revision"] = revision
|
||||||
|
data["update_time"] = time.Now()
|
||||||
|
|
||||||
|
n, err := qt.SetCond(c).Filter("id", id).Update(data)
|
||||||
|
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
log.Warningf("no records are updated when updating admin job %d", id)
|
log.Warningf("no records are updated when updating admin job %d", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,7 +110,7 @@ func GetTop10AdminJobsOfName(name string) ([]*models.AdminJob, error) {
|
|||||||
o := GetOrmer()
|
o := GetOrmer()
|
||||||
jobs := []*models.AdminJob{}
|
jobs := []*models.AdminJob{}
|
||||||
n, err := o.Raw(`select * from admin_job
|
n, err := o.Raw(`select * from admin_job
|
||||||
where deleted = false and job_name = ? order by update_time desc limit 10`, name).QueryRows(&jobs)
|
where deleted = false and job_name = ? order by id desc limit 10`, name).QueryRows(&jobs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -137,6 +155,6 @@ func adminQueryConditions(query *models.AdminJobQuery) orm.QuerySeter {
|
|||||||
qs = qs.Filter("UUID", query.UUID)
|
qs = qs.Filter("UUID", query.UUID)
|
||||||
}
|
}
|
||||||
qs = qs.Filter("Deleted", false)
|
qs = qs.Filter("Deleted", false)
|
||||||
return qs
|
return qs.OrderBy("-ID")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -15,14 +15,29 @@
|
|||||||
package dao
|
package dao
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAddAdminJob(t *testing.T) {
|
// AdminJobSuite is a test suite for testing admin job
|
||||||
|
type AdminJobSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
job0 *models.AdminJob
|
||||||
|
ids []int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAdminJob is the entry point of AdminJobSuite
|
||||||
|
func TestAdminJob(t *testing.T) {
|
||||||
|
suite.Run(t, &AdminJobSuite{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupSuite prepares testing env for the suite
|
||||||
|
func (suite *AdminJobSuite) SetupSuite() {
|
||||||
job := &models.AdminJob{
|
job := &models.AdminJob{
|
||||||
Name: "job",
|
Name: "job",
|
||||||
Kind: "jobKind",
|
Kind: "jobKind",
|
||||||
@ -33,43 +48,96 @@ func TestAddAdminJob(t *testing.T) {
|
|||||||
Kind: "testKind",
|
Kind: "testKind",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suite.ids = make([]int64, 0)
|
||||||
|
|
||||||
// add
|
// add
|
||||||
id, err := AddAdminJob(job0)
|
id, err := AddAdminJob(job0)
|
||||||
require.Nil(t, err)
|
require.NoError(suite.T(), err)
|
||||||
job0.ID = id
|
job0.ID = id
|
||||||
|
suite.job0 = job0
|
||||||
|
suite.ids = append(suite.ids, id)
|
||||||
|
|
||||||
|
id1, err := AddAdminJob(job)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
suite.ids = append(suite.ids, id1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TearDownSuite cleans testing env
|
||||||
|
func (suite *AdminJobSuite) TearDownSuite() {
|
||||||
|
for _, id := range suite.ids {
|
||||||
|
err := DeleteAdminJob(id)
|
||||||
|
suite.NoError(err, fmt.Sprintf("clear admin job: %d", id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAdminJobBase ...
|
||||||
|
func (suite *AdminJobSuite) TestAdminJobBase() {
|
||||||
// get
|
// get
|
||||||
job1, err := GetAdminJob(id)
|
job1, err := GetAdminJob(suite.job0.ID)
|
||||||
require.Nil(t, err)
|
require.Nil(suite.T(), err)
|
||||||
assert.Equal(t, job1.ID, job0.ID)
|
suite.Equal(job1.ID, suite.job0.ID)
|
||||||
assert.Equal(t, job1.Name, job0.Name)
|
suite.Equal(job1.Name, suite.job0.Name)
|
||||||
|
|
||||||
// update status
|
|
||||||
err = UpdateAdminJobStatus(id, "testStatus")
|
|
||||||
require.Nil(t, err)
|
|
||||||
job2, err := GetAdminJob(id)
|
|
||||||
assert.Equal(t, job2.Status, "testStatus")
|
|
||||||
|
|
||||||
// set uuid
|
// set uuid
|
||||||
err = SetAdminJobUUID(id, "f5ef34f4cb3588d663176132")
|
err = SetAdminJobUUID(suite.job0.ID, "f5ef34f4cb3588d663176132")
|
||||||
require.Nil(t, err)
|
require.Nil(suite.T(), err)
|
||||||
job3, err := GetAdminJob(id)
|
job3, err := GetAdminJob(suite.job0.ID)
|
||||||
require.Nil(t, err)
|
require.Nil(suite.T(), err)
|
||||||
assert.Equal(t, job3.UUID, "f5ef34f4cb3588d663176132")
|
suite.Equal(job3.UUID, "f5ef34f4cb3588d663176132")
|
||||||
|
|
||||||
// get admin jobs
|
// get admin jobs
|
||||||
_, err = AddAdminJob(job)
|
|
||||||
require.Nil(t, err)
|
|
||||||
query := &models.AdminJobQuery{
|
query := &models.AdminJobQuery{
|
||||||
Name: "job",
|
Name: "job",
|
||||||
}
|
}
|
||||||
jobs, err := GetAdminJobs(query)
|
jobs, err := GetAdminJobs(query)
|
||||||
assert.Equal(t, len(jobs), 1)
|
suite.Equal(len(jobs), 1)
|
||||||
|
|
||||||
// get top 10
|
// get top 10
|
||||||
_, err = AddAdminJob(job)
|
|
||||||
require.Nil(t, err)
|
|
||||||
|
|
||||||
jobs, _ = GetTop10AdminJobsOfName("job")
|
jobs, _ = GetTop10AdminJobsOfName("job")
|
||||||
assert.Equal(t, len(jobs), 2)
|
suite.Equal(len(jobs), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAdminJobUpdateStatus ...
|
||||||
|
func (suite *AdminJobSuite) TestAdminJobUpdateStatus() {
|
||||||
|
// update status
|
||||||
|
err := UpdateAdminJobStatus(suite.job0.ID, "testStatus", 1, 10000)
|
||||||
|
require.Nil(suite.T(), err)
|
||||||
|
|
||||||
|
job2, err := GetAdminJob(suite.job0.ID)
|
||||||
|
require.Nil(suite.T(), err)
|
||||||
|
suite.Equal(job2.Status, "testStatus")
|
||||||
|
|
||||||
|
// Update status with same rev
|
||||||
|
err = UpdateAdminJobStatus(suite.job0.ID, "testStatus3", 3, 10000)
|
||||||
|
require.Nil(suite.T(), err)
|
||||||
|
|
||||||
|
job3, err := GetAdminJob(suite.job0.ID)
|
||||||
|
require.Nil(suite.T(), err)
|
||||||
|
suite.Equal(job3.Status, "testStatus3")
|
||||||
|
|
||||||
|
// Update status with same rev, previous status
|
||||||
|
err = UpdateAdminJobStatus(suite.job0.ID, "testStatus2", 2, 10000)
|
||||||
|
require.Nil(suite.T(), err)
|
||||||
|
|
||||||
|
job4, err := GetAdminJob(suite.job0.ID)
|
||||||
|
require.Nil(suite.T(), err)
|
||||||
|
// No status change
|
||||||
|
suite.Equal(job4.Status, "testStatus3")
|
||||||
|
|
||||||
|
// Update status with previous rev
|
||||||
|
err = UpdateAdminJobStatus(suite.job0.ID, "testStatus4", 4, 9999)
|
||||||
|
require.Nil(suite.T(), err)
|
||||||
|
|
||||||
|
job5, err := GetAdminJob(suite.job0.ID)
|
||||||
|
require.Nil(suite.T(), err)
|
||||||
|
// No status change
|
||||||
|
suite.Equal(job5.Status, "testStatus3")
|
||||||
|
|
||||||
|
// Update status with latest rev
|
||||||
|
err = UpdateAdminJobStatus(suite.job0.ID, "testStatus", 1, 10001)
|
||||||
|
require.Nil(suite.T(), err)
|
||||||
|
|
||||||
|
job6, err := GetAdminJob(suite.job0.ID)
|
||||||
|
require.Nil(suite.T(), err)
|
||||||
|
suite.Equal(job6.Status, "testStatus")
|
||||||
}
|
}
|
||||||
|
@ -159,8 +159,8 @@ func testForAll(m *testing.M) int {
|
|||||||
func clearAll() {
|
func clearAll() {
|
||||||
tables := []string{"project_member",
|
tables := []string{"project_member",
|
||||||
"project_metadata", "access_log", "repository", "replication_policy",
|
"project_metadata", "access_log", "repository", "replication_policy",
|
||||||
"registry", "replication_execution", "replication_task", "img_scan_job",
|
"registry", "replication_execution", "replication_task",
|
||||||
"replication_schedule_job", "img_scan_overview", "clair_vuln_timestamp", "project", "harbor_user"}
|
"replication_schedule_job", "project", "harbor_user"}
|
||||||
for _, t := range tables {
|
for _, t := range tables {
|
||||||
if err := ClearTable(t); err != nil {
|
if err := ClearTable(t); err != nil {
|
||||||
log.Errorf("Failed to clear table: %s,error: %v", t, err)
|
log.Errorf("Failed to clear table: %s,error: %v", t, err)
|
||||||
|
@ -50,6 +50,7 @@ type StatsInfo struct {
|
|||||||
UpstreamJobID string `json:"upstream_job_id,omitempty"` // Ref the upstream job if existing
|
UpstreamJobID string `json:"upstream_job_id,omitempty"` // Ref the upstream job if existing
|
||||||
NumericPID int64 `json:"numeric_policy_id,omitempty"` // The numeric policy ID of the periodic job
|
NumericPID int64 `json:"numeric_policy_id,omitempty"` // The numeric policy ID of the periodic job
|
||||||
Parameters Parameters `json:"parameters,omitempty"`
|
Parameters Parameters `json:"parameters,omitempty"`
|
||||||
|
Revision int64 `json:"revision,omitempty"` // For differentiating the each retry of the same job
|
||||||
}
|
}
|
||||||
|
|
||||||
// JobPoolStats represents the healthy and status of all the running worker pools.
|
// JobPoolStats represents the healthy and status of all the running worker pools.
|
||||||
|
@ -32,6 +32,8 @@ type AdminJob struct {
|
|||||||
Cron string `orm:"column(cron_str)" json:"cron_str"`
|
Cron string `orm:"column(cron_str)" json:"cron_str"`
|
||||||
Status string `orm:"column(status)" json:"job_status"`
|
Status string `orm:"column(status)" json:"job_status"`
|
||||||
UUID string `orm:"column(job_uuid)" json:"-"`
|
UUID string `orm:"column(job_uuid)" json:"-"`
|
||||||
|
Revision int64 `orm:"column(revision)" json:"-"`
|
||||||
|
StatusCode uint16 `orm:"column(status_code)" json:"-"`
|
||||||
Deleted bool `orm:"column(deleted)" json:"deleted"`
|
Deleted bool `orm:"column(deleted)" json:"deleted"`
|
||||||
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
|
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
|
||||||
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
|
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
|
||||||
|
@ -16,8 +16,10 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common/dao"
|
"github.com/goharbor/harbor/src/common/dao"
|
||||||
common_http "github.com/goharbor/harbor/src/common/http"
|
common_http "github.com/goharbor/harbor/src/common/http"
|
||||||
@ -26,6 +28,7 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
"github.com/goharbor/harbor/src/core/api/models"
|
"github.com/goharbor/harbor/src/core/api/models"
|
||||||
utils_core "github.com/goharbor/harbor/src/core/utils"
|
utils_core "github.com/goharbor/harbor/src/core/utils"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/scan/api/scan"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -241,6 +244,54 @@ func (aj *AJAPI) submit(ajr *models.AdminJobReq) {
|
|||||||
aj.SendPreconditionFailedError(errors.New("fail to set schedule for admin job as always had one, please delete it firstly then to re-schedule"))
|
aj.SendPreconditionFailedError(errors.New("fail to set schedule for admin job as always had one, please delete it firstly then to re-schedule"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// So far, it should be a generic job for the manually trigger case.
|
||||||
|
// Only needs to care the 1st generic job.
|
||||||
|
// Check if there are still ongoing scan jobs triggered by the previous admin job.
|
||||||
|
// TODO: REPLACE WITH TASK MANAGER METHODS IN FUTURE
|
||||||
|
query := &common_models.AdminJobQuery{
|
||||||
|
Name: ajr.Name,
|
||||||
|
Kind: common_job.JobKindGeneric,
|
||||||
|
}
|
||||||
|
query.Size = 1
|
||||||
|
query.Page = 1
|
||||||
|
|
||||||
|
ajbs, err := dao.GetAdminJobs(query)
|
||||||
|
if err != nil {
|
||||||
|
aj.SendInternalServerError(errors.Wrap(err, "AJAPI"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ajbs) > 0 {
|
||||||
|
jb := ajbs[0]
|
||||||
|
|
||||||
|
// With a reasonable timeout duration
|
||||||
|
if jb.UpdateTime.Add(2 * time.Hour).After(time.Now()) {
|
||||||
|
if isOnGoing(jb.Status) {
|
||||||
|
err := errors.Errorf("reject job submitting: job %s with ID %d is %s", jb.Name, jb.ID, jb.Status)
|
||||||
|
aj.SendInternalServerError(errors.Wrap(err, "submit : AJAPI"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// For scan all job, check more
|
||||||
|
if jb.Name == common_job.ImageScanAllJob {
|
||||||
|
// Get the overall stats with the ID of the previous job
|
||||||
|
stats, err := scan.DefaultController.GetStats(fmt.Sprintf("%d", jb.ID))
|
||||||
|
if err != nil {
|
||||||
|
aj.SendInternalServerError(errors.Wrap(err, "submit : AJAPI"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if stats.Total != stats.Completed {
|
||||||
|
// Not all scan processes are completed
|
||||||
|
// In case status is hang, add outdated timeout
|
||||||
|
err := errors.Errorf("scan processes started by %s job with ID %d is in progress: %s", jb.Name, jb.ID, progress(stats.Completed, stats.Total))
|
||||||
|
aj.SendPreconditionFailedError(errors.Wrap(err, "submit : AJAPI"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := dao.AddAdminJob(&common_models.AdminJob{
|
id, err := dao.AddAdminJob(&common_models.AdminJob{
|
||||||
@ -271,6 +322,29 @@ func (aj *AJAPI) submit(ajr *models.AdminJobReq) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (aj *AJAPI) getLatestScanAllJobIDByKind(kind string) (int64, error) {
|
||||||
|
query := &common_models.AdminJobQuery{
|
||||||
|
Name: common_job.ImageScanAllJob,
|
||||||
|
Kind: kind,
|
||||||
|
}
|
||||||
|
query.Size = 1
|
||||||
|
query.Page = 1
|
||||||
|
|
||||||
|
jbs, err := dao.GetAdminJobs(query)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(jbs) == 0 {
|
||||||
|
// Not exist
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the latest one (with biggest ID)
|
||||||
|
return jbs[0].ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
func convertToAdminJobRep(job *common_models.AdminJob) (models.AdminJobRep, error) {
|
func convertToAdminJobRep(job *common_models.AdminJob) (models.AdminJobRep, error) {
|
||||||
if job == nil {
|
if job == nil {
|
||||||
return models.AdminJobRep{}, nil
|
return models.AdminJobRep{}, nil
|
||||||
@ -294,3 +368,22 @@ func convertToAdminJobRep(job *common_models.AdminJob) (models.AdminJobRep, erro
|
|||||||
}
|
}
|
||||||
return AdminJobRep, nil
|
return AdminJobRep, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func progress(completed, total uint) string {
|
||||||
|
if total == 0 {
|
||||||
|
return fmt.Sprintf("0%s", "%")
|
||||||
|
}
|
||||||
|
|
||||||
|
v := float64(completed)
|
||||||
|
vv := float64(total)
|
||||||
|
|
||||||
|
p := (int)(math.Round((v / vv) * 100))
|
||||||
|
|
||||||
|
return fmt.Sprintf("%d%s", p, "%")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isOnGoing(status string) bool {
|
||||||
|
return status == common_models.JobRunning ||
|
||||||
|
status == common_models.JobScheduled ||
|
||||||
|
status == common_models.JobPending
|
||||||
|
}
|
||||||
|
@ -113,6 +113,14 @@ func (ar *AdminJobReq) ToJob() *models.JobData {
|
|||||||
StatusHook: fmt.Sprintf("%s/service/notifications/jobs/adminjob/%d",
|
StatusHook: fmt.Sprintf("%s/service/notifications/jobs/adminjob/%d",
|
||||||
config.InternalCoreURL(), ar.ID),
|
config.InternalCoreURL(), ar.ID),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Append admin job ID as job parameter
|
||||||
|
if jobData.Parameters == nil {
|
||||||
|
jobData.Parameters = make(models.Parameters)
|
||||||
|
}
|
||||||
|
// As string
|
||||||
|
jobData.Parameters["admin_job_id"] = fmt.Sprintf("%d", ar.ID)
|
||||||
|
|
||||||
return jobData
|
return jobData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/pkg/q"
|
|
||||||
|
|
||||||
common_job "github.com/goharbor/harbor/src/common/job"
|
common_job "github.com/goharbor/harbor/src/common/job"
|
||||||
"github.com/goharbor/harbor/src/core/api/models"
|
"github.com/goharbor/harbor/src/core/api/models"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/q"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/scan/all"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/scan/api/scan"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/api/scanner"
|
"github.com/goharbor/harbor/src/pkg/scan/api/scanner"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
@ -97,6 +99,41 @@ func (sc *ScanAllAPI) List() {
|
|||||||
sc.list(common_job.ImageScanAllJob)
|
sc.list(common_job.ImageScanAllJob)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetScheduleMetrics returns the progress metrics for the latest scheduled scan all job
|
||||||
|
func (sc *ScanAllAPI) GetScheduleMetrics() {
|
||||||
|
sc.getMetrics(common_job.JobKindPeriodic)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScanAllMetrics returns the progress metrics for the latest manually triggered scan all job
|
||||||
|
func (sc *ScanAllAPI) GetScanAllMetrics() {
|
||||||
|
sc.getMetrics(common_job.JobKindGeneric)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *ScanAllAPI) getMetrics(kind string) {
|
||||||
|
id, err := sc.getLatestScanAllJobIDByKind(kind)
|
||||||
|
if err != nil {
|
||||||
|
sc.SendInternalServerError(errors.Wrap(err, "get metrics: scan all API"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var sts *all.Stats
|
||||||
|
if id > 0 {
|
||||||
|
sts, err = scan.DefaultController.GetStats(fmt.Sprintf("%d", id))
|
||||||
|
if err != nil {
|
||||||
|
sc.SendInternalServerError(errors.Wrap(err, "get metrics: scan all API"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return empty
|
||||||
|
if sts == nil {
|
||||||
|
sts = &all.Stats{}
|
||||||
|
}
|
||||||
|
|
||||||
|
sc.Data["json"] = sts
|
||||||
|
sc.ServeJSON()
|
||||||
|
}
|
||||||
|
|
||||||
func isScanEnabled() (bool, error) {
|
func isScanEnabled() (bool, error) {
|
||||||
kws := make(map[string]interface{})
|
kws := make(map[string]interface{})
|
||||||
kws["is_default"] = true
|
kws["is_default"] = true
|
||||||
|
@ -19,6 +19,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/pkg/scan/all"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/jobservice/job"
|
"github.com/goharbor/harbor/src/jobservice/job"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/api/scan"
|
"github.com/goharbor/harbor/src/pkg/scan/api/scan"
|
||||||
dscan "github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
dscan "github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
||||||
@ -170,7 +172,7 @@ type MockScanAPIController struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan ...
|
// Scan ...
|
||||||
func (msc *MockScanAPIController) Scan(artifact *v1.Artifact) error {
|
func (msc *MockScanAPIController) Scan(artifact *v1.Artifact, option ...scan.Option) error {
|
||||||
args := msc.Called(artifact)
|
args := msc.Called(artifact)
|
||||||
|
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
@ -215,3 +217,13 @@ func (msc *MockScanAPIController) HandleJobHooks(trackID string, change *job.Sta
|
|||||||
func (msc *MockScanAPIController) DeleteReports(digests ...string) error {
|
func (msc *MockScanAPIController) DeleteReports(digests ...string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (msc *MockScanAPIController) GetStats(requester string) (*all.Stats, error) {
|
||||||
|
args := msc.Called(requester)
|
||||||
|
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return args.Get(0).(*all.Stats), args.Error(1)
|
||||||
|
}
|
||||||
|
@ -221,7 +221,7 @@ func (sa *ScannerAPI) Delete() {
|
|||||||
|
|
||||||
// Immutable registration is not allowed
|
// Immutable registration is not allowed
|
||||||
if r.Immutable {
|
if r.Immutable {
|
||||||
sa.SendForbiddenError(errors.Errorf("registration %s is not allowed to delete as it is immutable: scanner API: update", r.Name))
|
sa.SendForbiddenError(errors.Errorf("registration %s is not allowed to delete as it is immutable: scanner API: delete", r.Name))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/pkg/scan/all"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common"
|
"github.com/goharbor/harbor/src/common"
|
||||||
"github.com/goharbor/harbor/src/core/config"
|
"github.com/goharbor/harbor/src/core/config"
|
||||||
"github.com/goharbor/harbor/src/core/notifier"
|
"github.com/goharbor/harbor/src/core/notifier"
|
||||||
@ -105,7 +107,7 @@ type MockScanAPIController struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan ...
|
// Scan ...
|
||||||
func (msc *MockScanAPIController) Scan(artifact *v1.Artifact) error {
|
func (msc *MockScanAPIController) Scan(artifact *v1.Artifact, option ...sc.Option) error {
|
||||||
args := msc.Called(artifact)
|
args := msc.Called(artifact)
|
||||||
|
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
@ -157,6 +159,15 @@ func (msc *MockScanAPIController) DeleteReports(digests ...string) error {
|
|||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (msc *MockScanAPIController) GetStats(requester string) (*all.Stats, error) {
|
||||||
|
args := msc.Called(requester)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return args.Get(0).(*all.Stats), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
// MockHTTPHandler ...
|
// MockHTTPHandler ...
|
||||||
type MockHTTPHandler struct{}
|
type MockHTTPHandler struct{}
|
||||||
|
|
||||||
|
@ -214,6 +214,11 @@ func initRouters() {
|
|||||||
// Handle scan hook
|
// Handle scan hook
|
||||||
beego.Router("/service/notifications/jobs/scan/:uuid", &jobs.Handler{}, "post:HandleScan")
|
beego.Router("/service/notifications/jobs/scan/:uuid", &jobs.Handler{}, "post:HandleScan")
|
||||||
|
|
||||||
|
// Add routes for scan all metrics
|
||||||
|
scanAllAPI := &api.ScanAllAPI{}
|
||||||
|
beego.Router("/api/scans/all/metrics", scanAllAPI, "get:GetScanAllMetrics")
|
||||||
|
beego.Router("/api/scans/schedule/metrics", scanAllAPI, "get:GetScheduleMetrics")
|
||||||
|
|
||||||
// Error pages
|
// Error pages
|
||||||
beego.ErrorController(&controllers.ErrorController{})
|
beego.ErrorController(&controllers.ErrorController{})
|
||||||
|
|
||||||
|
@ -23,6 +23,8 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/common/models"
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
"github.com/goharbor/harbor/src/core/api"
|
"github.com/goharbor/harbor/src/core/api"
|
||||||
|
j "github.com/goharbor/harbor/src/jobservice/job"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/scan/api/scan"
|
||||||
)
|
)
|
||||||
|
|
||||||
var statusMap = map[string]string{
|
var statusMap = map[string]string{
|
||||||
@ -42,6 +44,10 @@ type Handler struct {
|
|||||||
UUID string
|
UUID string
|
||||||
status string
|
status string
|
||||||
UpstreamJobID string
|
UpstreamJobID string
|
||||||
|
revision int64
|
||||||
|
jobName string
|
||||||
|
checkIn string
|
||||||
|
statusCode uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare ...
|
// Prepare ...
|
||||||
@ -74,20 +80,30 @@ func (h *Handler) Prepare() {
|
|||||||
h.Abort("200")
|
h.Abort("200")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
h.statusCode = (uint16)(j.Status(data.Status).Code())
|
||||||
h.status = status
|
h.status = status
|
||||||
|
h.revision = data.Metadata.Revision
|
||||||
|
h.jobName = data.Metadata.JobName
|
||||||
|
h.checkIn = data.CheckIn
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleAdminJob handles the webhook of admin jobs
|
// HandleAdminJob handles the webhook of admin jobs
|
||||||
func (h *Handler) HandleAdminJob() {
|
func (h *Handler) HandleAdminJob() {
|
||||||
log.Infof("received admin job status update event: job-%d, status-%s", h.id, h.status)
|
log.Infof("received admin job status update event: job-%d, job_uuid-%s, status-%s, revision-%d", h.id, h.UUID, h.status, h.revision)
|
||||||
|
|
||||||
// create the mapping relationship between the jobs in database and jobservice
|
// create the mapping relationship between the jobs in database and jobservice
|
||||||
if err := dao.SetAdminJobUUID(h.id, h.UUID); err != nil {
|
if err := dao.SetAdminJobUUID(h.id, h.UUID); err != nil {
|
||||||
h.SendInternalServerError(err)
|
h.SendInternalServerError(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := dao.UpdateAdminJobStatus(h.id, h.status); err != nil {
|
if err := dao.UpdateAdminJobStatus(h.id, h.status, h.statusCode, h.revision); err != nil {
|
||||||
log.Errorf("Failed to update job status, id: %d, status: %s", h.id, h.status)
|
log.Errorf("Failed to update job status, id: %d, status: %s", h.id, h.status)
|
||||||
h.SendInternalServerError(err)
|
h.SendInternalServerError(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For scan all job
|
||||||
|
if h.jobName == job.ImageScanAllJob {
|
||||||
|
scan.HandleCheckIn(h.checkIn)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,141 +0,0 @@
|
|||||||
// Copyright 2018 The 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 scan
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common"
|
|
||||||
"github.com/goharbor/harbor/src/common/dao"
|
|
||||||
"github.com/goharbor/harbor/src/jobservice/job"
|
|
||||||
"github.com/goharbor/harbor/src/jobservice/job/impl/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
// All query the DB and Registry for all image and tags,
|
|
||||||
// then call Harbor's API to scan each of them.
|
|
||||||
type All struct {
|
|
||||||
registryURL string
|
|
||||||
secret string
|
|
||||||
tokenServiceEndpoint string
|
|
||||||
harborAPIEndpoint string
|
|
||||||
coreClient *http.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
// MaxFails implements the interface in job/Interface
|
|
||||||
func (sa *All) MaxFails() uint {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShouldRetry implements the interface in job/Interface
|
|
||||||
func (sa *All) ShouldRetry() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate implements the interface in job/Interface
|
|
||||||
func (sa *All) Validate(params job.Parameters) error {
|
|
||||||
if len(params) > 0 {
|
|
||||||
return fmt.Errorf("the parms should be empty for scan all job")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run implements the interface in job/Interface
|
|
||||||
func (sa *All) Run(ctx job.Context, params job.Parameters) error {
|
|
||||||
logger := ctx.GetLogger()
|
|
||||||
logger.Info("Scanning all the images in the registry")
|
|
||||||
err := sa.init(ctx)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("Failed to initialize the job handler, error: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
repos, err := dao.GetRepositories()
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("Failed to get the list of repositories, error: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, r := range repos {
|
|
||||||
repoClient, err := utils.NewRepositoryClientForJobservice(r.Name, sa.registryURL, sa.secret, sa.tokenServiceEndpoint)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("Failed to get repo client for repo: %s, error: %v", r.Name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
tags, err := repoClient.ListTag()
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("Failed to get tags for repo: %s, error: %v", r.Name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, t := range tags {
|
|
||||||
logger.Infof("Calling harbor-core API to scan image, %s:%s", r.Name, t)
|
|
||||||
resp, err := sa.coreClient.Post(fmt.Sprintf("%s/repositories/%s/tags/%s/scan", sa.harborAPIEndpoint, r.Name, t),
|
|
||||||
"application/json",
|
|
||||||
bytes.NewReader([]byte("{}")))
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("Failed to trigger image scan, error: %v", err)
|
|
||||||
} else {
|
|
||||||
data, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("Failed to read response, error: %v", err)
|
|
||||||
} else if resp.StatusCode != http.StatusAccepted {
|
|
||||||
logger.Errorf("Unexpected response code: %d, data: %v", resp.StatusCode, data)
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sa *All) init(ctx job.Context) error {
|
|
||||||
if v, err := getAttrFromCtx(ctx, common.RegistryURL); err == nil {
|
|
||||||
sa.registryURL = v
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if v := os.Getenv("JOBSERVICE_SECRET"); len(v) > 0 {
|
|
||||||
sa.secret = v
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("failed to read evnironment variable JOBSERVICE_SECRET")
|
|
||||||
}
|
|
||||||
sa.coreClient, _ = utils.GetClient()
|
|
||||||
if v, err := getAttrFromCtx(ctx, common.TokenServiceURL); err == nil {
|
|
||||||
sa.tokenServiceEndpoint = v
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if v, err := getAttrFromCtx(ctx, common.CoreURL); err == nil {
|
|
||||||
v = strings.TrimSuffix(v, "/")
|
|
||||||
sa.harborAPIEndpoint = v + "/api"
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAttrFromCtx(ctx job.Context, key string) (string, error) {
|
|
||||||
if v, ok := ctx.Get(key); ok && len(v.(string)) > 0 {
|
|
||||||
return v.(string), nil
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("failed to get required property: %s", key)
|
|
||||||
}
|
|
@ -80,3 +80,9 @@ func (s Status) Compare(another Status) int {
|
|||||||
func (s Status) String() string {
|
func (s Status) String() string {
|
||||||
return string(s)
|
return string(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Final returns if the status is final status
|
||||||
|
// e.g: "Stopped", "Error" or "Success"
|
||||||
|
func (s Status) Final() bool {
|
||||||
|
return s.Code() == 3
|
||||||
|
}
|
||||||
|
@ -34,7 +34,6 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/jobservice/job/impl/notification"
|
"github.com/goharbor/harbor/src/jobservice/job/impl/notification"
|
||||||
"github.com/goharbor/harbor/src/jobservice/job/impl/replication"
|
"github.com/goharbor/harbor/src/jobservice/job/impl/replication"
|
||||||
"github.com/goharbor/harbor/src/jobservice/job/impl/sample"
|
"github.com/goharbor/harbor/src/jobservice/job/impl/sample"
|
||||||
"github.com/goharbor/harbor/src/jobservice/job/impl/scan"
|
|
||||||
"github.com/goharbor/harbor/src/jobservice/lcm"
|
"github.com/goharbor/harbor/src/jobservice/lcm"
|
||||||
"github.com/goharbor/harbor/src/jobservice/logger"
|
"github.com/goharbor/harbor/src/jobservice/logger"
|
||||||
"github.com/goharbor/harbor/src/jobservice/mgt"
|
"github.com/goharbor/harbor/src/jobservice/mgt"
|
||||||
@ -43,6 +42,7 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/jobservice/worker/cworker"
|
"github.com/goharbor/harbor/src/jobservice/worker/cworker"
|
||||||
"github.com/goharbor/harbor/src/pkg/retention"
|
"github.com/goharbor/harbor/src/pkg/retention"
|
||||||
sc "github.com/goharbor/harbor/src/pkg/scan"
|
sc "github.com/goharbor/harbor/src/pkg/scan"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/scan/all"
|
||||||
"github.com/goharbor/harbor/src/pkg/scheduler"
|
"github.com/goharbor/harbor/src/pkg/scheduler"
|
||||||
"github.com/gomodule/redigo/redis"
|
"github.com/gomodule/redigo/redis"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@ -243,7 +243,7 @@ func (bs *Bootstrap) loadAndRunRedisWorkerPool(
|
|||||||
job.SampleJob: (*sample.Job)(nil),
|
job.SampleJob: (*sample.Job)(nil),
|
||||||
// Functional jobs
|
// Functional jobs
|
||||||
job.ImageScanJob: (*sc.Job)(nil),
|
job.ImageScanJob: (*sc.Job)(nil),
|
||||||
job.ImageScanAllJob: (*scan.All)(nil),
|
job.ImageScanAllJob: (*all.Job)(nil),
|
||||||
job.ImageGC: (*gc.GarbageCollector)(nil),
|
job.ImageGC: (*gc.GarbageCollector)(nil),
|
||||||
job.Replication: (*replication.Replication)(nil),
|
job.Replication: (*replication.Replication)(nil),
|
||||||
job.ReplicationScheduler: (*replication.Scheduler)(nil),
|
job.ReplicationScheduler: (*replication.Scheduler)(nil),
|
||||||
|
47
src/pkg/scan/all/checkin.go
Normal file
47
src/pkg/scan/all/checkin.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// 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 all
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckInData is designed for checking the data generated by the scan all job.
|
||||||
|
type CheckInData struct {
|
||||||
|
Artifacts []*v1.Artifact `json:"artifacts"`
|
||||||
|
Requester string `json:"requester"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToJSON marshals `CheckInData` to JSON str
|
||||||
|
func (c *CheckInData) ToJSON() (string, error) {
|
||||||
|
data, err := json.Marshal(c)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "ToJSON : CheckInData")
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromJSON unmarshal give bytes to `CheckInData`
|
||||||
|
func (c *CheckInData) FromJSON(data []byte) error {
|
||||||
|
if err := json.Unmarshal(data, c); err != nil {
|
||||||
|
return errors.Wrap(err, "FromJSON : CheckInData")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
163
src/pkg/scan/all/job.go
Normal file
163
src/pkg/scan/all/job.go
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
// 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 all
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/goharbor/harbor/src/common/dao"
|
||||||
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
|
"github.com/goharbor/harbor/src/jobservice/job"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/art"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/q"
|
||||||
|
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// The max number of the goroutines to retrieve the tags
|
||||||
|
maxProcessors = 25
|
||||||
|
// Job parameter key for the admin job ID
|
||||||
|
jobParamAJID = "admin_job_id"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Job query the DB and Registry for all image and tags,
|
||||||
|
// then call Harbor's API to scan each of them.
|
||||||
|
type Job struct{}
|
||||||
|
|
||||||
|
// MaxFails implements the interface in job/Interface
|
||||||
|
func (sa *Job) MaxFails() uint {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldRetry implements the interface in job/Interface
|
||||||
|
func (sa *Job) ShouldRetry() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate implements the interface in job/Interface
|
||||||
|
func (sa *Job) Validate(params job.Parameters) error {
|
||||||
|
_, err := parseAJID(params)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "job validation: scan all job")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run implements the interface in job/Interface
|
||||||
|
func (sa *Job) Run(ctx job.Context, params job.Parameters) error {
|
||||||
|
logger := ctx.GetLogger()
|
||||||
|
logger.Info("Scanning all the images in the registry")
|
||||||
|
|
||||||
|
// No need to check error any more as it has been checked in job validation.
|
||||||
|
requester, _ := parseAJID(params)
|
||||||
|
|
||||||
|
// List all the repositories of registry
|
||||||
|
// TODO: REPLACE DAO WITH CORRESPONDING MANAGER OR CTL
|
||||||
|
repos, err := dao.GetRepositories()
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, "list repositories : scan all job")
|
||||||
|
logger.Error(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Infof("Found %d repositories", len(repos))
|
||||||
|
|
||||||
|
// Initialize tokens
|
||||||
|
tokens := make(chan bool, maxProcessors)
|
||||||
|
for i := 0; i < maxProcessors; i++ {
|
||||||
|
// Assign tokens at first
|
||||||
|
tokens <- true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the tags under the repository
|
||||||
|
for _, r := range repos {
|
||||||
|
// Get token first
|
||||||
|
<-tokens
|
||||||
|
|
||||||
|
go func(repo *models.RepoRecord) {
|
||||||
|
defer func() {
|
||||||
|
// Return the token when process ending
|
||||||
|
tokens <- true
|
||||||
|
}()
|
||||||
|
|
||||||
|
logger.Infof("Scan artifacts under repository: %s", repo.Name)
|
||||||
|
|
||||||
|
// Query artifacts under the repository
|
||||||
|
query := &q.Query{
|
||||||
|
Keywords: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
query.Keywords["repo"] = repo.Name
|
||||||
|
|
||||||
|
al, err := art.DefaultController.List(query)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Failed to get tags for repo: %s, error: %v", repo.Name, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(al) > 0 {
|
||||||
|
// Check in the data
|
||||||
|
arts := make([]*v1.Artifact, 0)
|
||||||
|
|
||||||
|
for _, a := range al {
|
||||||
|
artf := &v1.Artifact{
|
||||||
|
NamespaceID: repo.ProjectID,
|
||||||
|
Repository: repo.Name,
|
||||||
|
Tag: a.Tag,
|
||||||
|
Digest: a.Digest,
|
||||||
|
MimeType: v1.MimeTypeDockerArtifact, // default
|
||||||
|
}
|
||||||
|
|
||||||
|
arts = append(arts, artf)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Infof("Found %d artifacts under repository %s", len(arts), repo.Name)
|
||||||
|
|
||||||
|
ck := &CheckInData{
|
||||||
|
Artifacts: arts,
|
||||||
|
Requester: requester,
|
||||||
|
}
|
||||||
|
|
||||||
|
jsn, err := ck.ToJSON()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(errors.Wrap(err, "scan all job"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ctx.Checkin(jsn); err != nil {
|
||||||
|
logger.Error(errors.Wrap(err, "check in data: scan all job"))
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Infof("Check in scanning artifacts for repository: %s", repo.Name)
|
||||||
|
// Debug more
|
||||||
|
logger.Debugf("Check in: %s\n", jsn)
|
||||||
|
} else {
|
||||||
|
logger.Infof("No scanning artifacts found under repository: %s", repo.Name)
|
||||||
|
}
|
||||||
|
}(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAJID(params job.Parameters) (string, error) {
|
||||||
|
if len(params) > 0 {
|
||||||
|
if v, ok := params[jobParamAJID]; ok {
|
||||||
|
if id, y := v.(string); y {
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", errors.Errorf("missing required job parameter: %s", jobParamAJID)
|
||||||
|
}
|
34
src/pkg/scan/all/stats.go
Normal file
34
src/pkg/scan/all/stats.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// 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 all
|
||||||
|
|
||||||
|
// Stats provides the overall progress of the scan all process.
|
||||||
|
type Stats struct {
|
||||||
|
Total uint `json:"total"`
|
||||||
|
// Status including `Success`, `Error` or `Stopped` will be counted as completed.
|
||||||
|
// This data may be influenced by job retrying
|
||||||
|
Completed uint `json:"completed"`
|
||||||
|
Metrics StatusMetrics `json:"metrics"`
|
||||||
|
Requester string `json:"requester"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatusMetrics contains the metrics of each status.
|
||||||
|
// The key should be the following valid status texts:
|
||||||
|
// - "pending"
|
||||||
|
// - "running"
|
||||||
|
// - "success"
|
||||||
|
// - "error"
|
||||||
|
// - "stopped"
|
||||||
|
type StatusMetrics map[string]uint
|
42
src/pkg/scan/api/scan/all_handler.go
Normal file
42
src/pkg/scan/api/scan/all_handler.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
// 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 scan
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/scan/all"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandleCheckIn handles the check in data of the scan all job
|
||||||
|
func HandleCheckIn(checkIn string) {
|
||||||
|
if len(checkIn) == 0 {
|
||||||
|
// Nothing to handle, directly return
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ck := &all.CheckInData{}
|
||||||
|
if err := ck.FromJSON([]byte(checkIn)); err != nil {
|
||||||
|
log.Error(errors.Wrap(err, "handle check in"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start to scan the artifacts
|
||||||
|
for _, art := range ck.Artifacts {
|
||||||
|
if err := DefaultController.Scan(art, WithRequester(ck.Requester)); err != nil {
|
||||||
|
// Just logged
|
||||||
|
log.Error(errors.Wrap(err, "handle check in"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -31,6 +31,7 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/pkg/robot"
|
"github.com/goharbor/harbor/src/pkg/robot"
|
||||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
"github.com/goharbor/harbor/src/pkg/robot/model"
|
||||||
sca "github.com/goharbor/harbor/src/pkg/scan"
|
sca "github.com/goharbor/harbor/src/pkg/scan"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/scan/all"
|
||||||
sc "github.com/goharbor/harbor/src/pkg/scan/api/scanner"
|
sc "github.com/goharbor/harbor/src/pkg/scan/api/scanner"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
||||||
@ -115,11 +116,17 @@ func NewController() Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan ...
|
// Scan ...
|
||||||
func (bc *basicController) Scan(artifact *v1.Artifact) error {
|
func (bc *basicController) Scan(artifact *v1.Artifact, options ...Option) error {
|
||||||
if artifact == nil {
|
if artifact == nil {
|
||||||
return errors.New("nil artifact to scan")
|
return errors.New("nil artifact to scan")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse options
|
||||||
|
ops, err := parseOptions(options...)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "scan controller: scan")
|
||||||
|
}
|
||||||
|
|
||||||
r, err := bc.sc.GetRegistrationByProject(artifact.NamespaceID)
|
r, err := bc.sc.GetRegistrationByProject(artifact.NamespaceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "scan controller: scan")
|
return errors.Wrap(err, "scan controller: scan")
|
||||||
@ -171,6 +178,14 @@ func (bc *basicController) Scan(artifact *v1.Artifact) error {
|
|||||||
TrackID: trackID,
|
TrackID: trackID,
|
||||||
MimeType: pm,
|
MimeType: pm,
|
||||||
}
|
}
|
||||||
|
// Set requester if it is specified
|
||||||
|
if len(ops.Requester) > 0 {
|
||||||
|
reportPlaceholder.Requester = ops.Requester
|
||||||
|
} else {
|
||||||
|
// Use the trackID as the requester
|
||||||
|
reportPlaceholder.Requester = trackID
|
||||||
|
}
|
||||||
|
|
||||||
_, e := bc.manager.Create(reportPlaceholder)
|
_, e := bc.manager.Create(reportPlaceholder)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
// Check if it is a status conflict error with common error format.
|
// Check if it is a status conflict error with common error format.
|
||||||
@ -377,7 +392,21 @@ func (bc *basicController) HandleJobHooks(trackID string, change *job.StatusChan
|
|||||||
|
|
||||||
// DeleteReports ...
|
// DeleteReports ...
|
||||||
func (bc *basicController) DeleteReports(digests ...string) error {
|
func (bc *basicController) DeleteReports(digests ...string) error {
|
||||||
return bc.manager.DeleteByDigests(digests...)
|
if err := bc.manager.DeleteByDigests(digests...); err != nil {
|
||||||
|
return errors.Wrap(err, "scan controller: delete reports")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStats ...
|
||||||
|
func (bc *basicController) GetStats(requester string) (*all.Stats, error) {
|
||||||
|
sts, err := bc.manager.GetStats(requester)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "scan controller: delete reports")
|
||||||
|
}
|
||||||
|
|
||||||
|
return sts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// makeBasicAuthorization creates authorization from a robot account based on the arguments for scanning.
|
// makeBasicAuthorization creates authorization from a robot account based on the arguments for scanning.
|
||||||
@ -509,3 +538,14 @@ func makeBearerAuthorization(repository string, username string) (string, error)
|
|||||||
|
|
||||||
return fmt.Sprintf("Bearer %s", accessToken.Token), nil
|
return fmt.Sprintf("Bearer %s", accessToken.Token), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseOptions(options ...Option) (*Options, error) {
|
||||||
|
ops := &Options{}
|
||||||
|
for _, op := range options {
|
||||||
|
if err := op(ops); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ops, nil
|
||||||
|
}
|
||||||
|
@ -30,6 +30,7 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/pkg/q"
|
"github.com/goharbor/harbor/src/pkg/q"
|
||||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
"github.com/goharbor/harbor/src/pkg/robot/model"
|
||||||
sca "github.com/goharbor/harbor/src/pkg/scan"
|
sca "github.com/goharbor/harbor/src/pkg/scan"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/scan/all"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
||||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||||
@ -105,6 +106,7 @@ func (suite *ControllerTestSuite) SetupSuite() {
|
|||||||
Status: "Pending",
|
Status: "Pending",
|
||||||
StatusCode: 0,
|
StatusCode: 0,
|
||||||
TrackID: "the-uuid-123",
|
TrackID: "the-uuid-123",
|
||||||
|
Requester: "the-uuid-123",
|
||||||
}).Return("r-uuid", nil)
|
}).Return("r-uuid", nil)
|
||||||
mgr.On("UpdateScanJobID", "the-uuid-123", "the-job-id").Return(nil)
|
mgr.On("UpdateScanJobID", "the-uuid-123", "the-job-id").Return(nil)
|
||||||
|
|
||||||
@ -352,6 +354,16 @@ func (mrm *MockReportManager) DeleteByDigests(digests ...string) error {
|
|||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mrm *MockReportManager) GetStats(requester string) (*all.Stats, error) {
|
||||||
|
args := mrm.Called(requester)
|
||||||
|
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return args.Get(0).(*all.Stats), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
// MockScannerController ...
|
// MockScannerController ...
|
||||||
type MockScannerController struct {
|
type MockScannerController struct {
|
||||||
mock.Mock
|
mock.Mock
|
||||||
|
@ -16,6 +16,7 @@ package scan
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/goharbor/harbor/src/jobservice/job"
|
"github.com/goharbor/harbor/src/jobservice/job"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/scan/all"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/report"
|
"github.com/goharbor/harbor/src/pkg/scan/report"
|
||||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||||
@ -29,10 +30,11 @@ type Controller interface {
|
|||||||
//
|
//
|
||||||
// Arguments:
|
// Arguments:
|
||||||
// artifact *v1.Artifact : artifact to be scanned
|
// artifact *v1.Artifact : artifact to be scanned
|
||||||
|
// options ...Option : options for triggering a scan
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// error : non nil error if any errors occurred
|
// error : non nil error if any errors occurred
|
||||||
Scan(artifact *v1.Artifact) error
|
Scan(artifact *v1.Artifact, options ...Option) 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
|
||||||
//
|
//
|
||||||
@ -86,4 +88,14 @@ type Controller interface {
|
|||||||
// Returns:
|
// Returns:
|
||||||
// error : non nil error if any errors occurred
|
// error : non nil error if any errors occurred
|
||||||
DeleteReports(digests ...string) error
|
DeleteReports(digests ...string) error
|
||||||
|
|
||||||
|
// Get the stats of the scan reports requested by the given requester.
|
||||||
|
//
|
||||||
|
// Arguments:
|
||||||
|
// requester string : requester identity
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// *all.AllStats: stats object including the related metric data
|
||||||
|
// error : non nil error if any errors occurred
|
||||||
|
GetStats(requester string) (*all.Stats, error)
|
||||||
}
|
}
|
||||||
|
38
src/pkg/scan/api/scan/options.go
Normal file
38
src/pkg/scan/api/scan/options.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// 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 scan
|
||||||
|
|
||||||
|
// Options keep the settings/configurations for scanning.
|
||||||
|
type Options struct {
|
||||||
|
// Mark the scan triggered by who.
|
||||||
|
// Identified by the UUID.
|
||||||
|
Requester string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option represents an option item by func template.
|
||||||
|
// The validation result of the options are marked by nil/non-nil error.
|
||||||
|
// e.g:
|
||||||
|
// If the option is required and the input arg is empty,
|
||||||
|
// then a non nil error should be returned at then.
|
||||||
|
type Option func(options *Options) error
|
||||||
|
|
||||||
|
// WithRequester sets the requester option.
|
||||||
|
func WithRequester(Requester string) Option {
|
||||||
|
return func(options *Options) error {
|
||||||
|
options.Requester = Requester
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
@ -26,6 +26,7 @@ type Report struct {
|
|||||||
MimeType string `orm:"column(mime_type)"`
|
MimeType string `orm:"column(mime_type)"`
|
||||||
JobID string `orm:"column(job_id)"`
|
JobID string `orm:"column(job_id)"`
|
||||||
TrackID string `orm:"column(track_id)"`
|
TrackID string `orm:"column(track_id)"`
|
||||||
|
Requester string `orm:"column(requester)"`
|
||||||
Status string `orm:"column(status)"`
|
Status string `orm:"column(status)"`
|
||||||
StatusCode int `orm:"column(status_code)"`
|
StatusCode int `orm:"column(status_code)"`
|
||||||
StatusRevision int64 `orm:"column(status_rev)"`
|
StatusRevision int64 `orm:"column(status_rev)"`
|
||||||
|
@ -16,6 +16,7 @@ package scan
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/astaxie/beego/orm"
|
"github.com/astaxie/beego/orm"
|
||||||
@ -123,9 +124,18 @@ func UpdateReportStatus(trackID string, status string, statusCode int, statusRev
|
|||||||
data["end_time"] = time.Now().UTC()
|
data["end_time"] = time.Now().UTC()
|
||||||
}
|
}
|
||||||
|
|
||||||
count, err := qt.Filter("track_id", trackID).
|
// qt generates sql statements:
|
||||||
Filter("status_rev__lte", statusRev).
|
// UPDATE "scan_report" SET "end_time" = $1, "status" = $2, "status_code" = $3, "status_rev" = $4
|
||||||
Filter("status_code__lte", statusCode).Update(data)
|
// WHERE "id" IN ( SELECT T0."id" FROM "scan_report" T0 WHERE ( T0."status_rev" = $5 AND T0."status_code" < $6 )
|
||||||
|
// OR ( T0."status_rev" < $7 ) AND T0."track_id" = $8 )
|
||||||
|
cond := orm.NewCondition()
|
||||||
|
c1 := cond.And("status_rev", statusRev).And("status_code__lt", statusCode)
|
||||||
|
c2 := cond.And("status_rev__lt", statusRev)
|
||||||
|
c := cond.AndCond(c1).OrCond(c2)
|
||||||
|
|
||||||
|
count, err := qt.SetCond(c).
|
||||||
|
Filter("track_id", trackID).
|
||||||
|
Update(data)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -151,3 +161,28 @@ func UpdateJobID(trackID string, jobID string) error {
|
|||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetScanStats gets the scan stats organized by status
|
||||||
|
func GetScanStats(requester string) (map[string]uint, error) {
|
||||||
|
res := make(orm.Params)
|
||||||
|
|
||||||
|
o := dao.GetOrmer()
|
||||||
|
if _, err := o.Raw("select status, count(status) from (select status from scan_report where requester=? group by track_id, status) as scan_status group by status").
|
||||||
|
SetArgs(requester).
|
||||||
|
RowsToMap(&res, "status", "count"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m := make(map[string]uint)
|
||||||
|
for k, v := range res {
|
||||||
|
vl, err := strconv.ParseInt(v.(string), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(errors.Wrap(err, "get scan stats"))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
m[k] = uint(vl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/jobservice/job"
|
"github.com/goharbor/harbor/src/jobservice/job"
|
||||||
"github.com/goharbor/harbor/src/pkg/q"
|
"github.com/goharbor/harbor/src/pkg/q"
|
||||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
@ -48,17 +49,13 @@ func (suite *ReportTestSuite) SetupTest() {
|
|||||||
TrackID: "track-uuid",
|
TrackID: "track-uuid",
|
||||||
Digest: "digest1001",
|
Digest: "digest1001",
|
||||||
RegistrationUUID: "ruuid",
|
RegistrationUUID: "ruuid",
|
||||||
|
Requester: "requester",
|
||||||
MimeType: v1.MimeTypeNativeReport,
|
MimeType: v1.MimeTypeNativeReport,
|
||||||
Status: job.PendingStatus.String(),
|
Status: job.PendingStatus.String(),
|
||||||
StatusCode: job.PendingStatus.Code(),
|
StatusCode: job.PendingStatus.Code(),
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := CreateReport(r)
|
suite.create(r)
|
||||||
require.NoError(suite.T(), err)
|
|
||||||
require.Condition(suite.T(), func() (success bool) {
|
|
||||||
success = id > 0
|
|
||||||
return
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TearDownTest clears enf for test case.
|
// TearDownTest clears enf for test case.
|
||||||
@ -124,9 +121,112 @@ func (suite *ReportTestSuite) TestReportUpdateStatus() {
|
|||||||
err := UpdateReportStatus("track-uuid", job.RunningStatus.String(), job.RunningStatus.Code(), 1000)
|
err := UpdateReportStatus("track-uuid", job.RunningStatus.String(), job.RunningStatus.Code(), 1000)
|
||||||
require.NoError(suite.T(), err)
|
require.NoError(suite.T(), err)
|
||||||
|
|
||||||
err = UpdateReportStatus("track-uuid", job.RunningStatus.String(), job.RunningStatus.Code(), 900)
|
err = checkStatus("track-uuid", job.RunningStatus.String())
|
||||||
|
suite.NoError(err, "regular status update")
|
||||||
|
|
||||||
|
err = UpdateReportStatus("track-uuid", job.SuccessStatus.String(), job.SuccessStatus.Code(), 900)
|
||||||
require.NoError(suite.T(), err)
|
require.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
err = checkStatus("track-uuid", job.RunningStatus.String())
|
||||||
|
suite.NoError(err, "update with outdated revision")
|
||||||
|
|
||||||
err = UpdateReportStatus("track-uuid", job.PendingStatus.String(), job.PendingStatus.Code(), 1000)
|
err = UpdateReportStatus("track-uuid", job.PendingStatus.String(), job.PendingStatus.Code(), 1000)
|
||||||
require.NoError(suite.T(), err)
|
require.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
err = checkStatus("track-uuid", job.RunningStatus.String())
|
||||||
|
suite.NoError(err, "update with same revision and previous status")
|
||||||
|
|
||||||
|
err = UpdateReportStatus("track-uuid", job.PendingStatus.String(), job.PendingStatus.Code(), 1001)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
err = checkStatus("track-uuid", job.PendingStatus.String())
|
||||||
|
suite.NoError(err, "update latest revision and previous status")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestReportGetStats ...
|
||||||
|
func (suite *ReportTestSuite) TestReportGetStats() {
|
||||||
|
// Two more for getting stats
|
||||||
|
r2 := &Report{
|
||||||
|
UUID: "uuid2",
|
||||||
|
TrackID: "track-uuid2",
|
||||||
|
Digest: "digest1003",
|
||||||
|
RegistrationUUID: "ruuid",
|
||||||
|
Requester: "requester",
|
||||||
|
MimeType: v1.MimeTypeNativeReport,
|
||||||
|
Status: job.RunningStatus.String(),
|
||||||
|
StatusCode: job.RunningStatus.Code(),
|
||||||
|
}
|
||||||
|
suite.create(r2)
|
||||||
|
|
||||||
|
r3 := &Report{
|
||||||
|
UUID: "uuid3",
|
||||||
|
TrackID: "track-uuid2",
|
||||||
|
Digest: "digest1003",
|
||||||
|
RegistrationUUID: "ruuid",
|
||||||
|
Requester: "requester",
|
||||||
|
MimeType: v1.MimeTypeRawReport,
|
||||||
|
Status: job.RunningStatus.String(),
|
||||||
|
StatusCode: job.RunningStatus.Code(),
|
||||||
|
}
|
||||||
|
suite.create(r3)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := DeleteReport("uuid2")
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
err = DeleteReport("uuid3")
|
||||||
|
suite.NoError(err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
m, err := GetScanStats("requester")
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
suite.Equal(2, len(m))
|
||||||
|
suite.Condition(func() (success bool) {
|
||||||
|
v, ok := m[job.RunningStatus.String()]
|
||||||
|
vv, ook := m[job.PendingStatus.String()]
|
||||||
|
|
||||||
|
success = ok && ook && v == 1 && vv == 1
|
||||||
|
|
||||||
|
return
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ReportTestSuite) create(r *Report) {
|
||||||
|
id, err := CreateReport(r)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
require.Condition(suite.T(), func() (success bool) {
|
||||||
|
success = id > 0
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func list(trackID string) ([]*Report, error) {
|
||||||
|
kws := make(map[string]interface{})
|
||||||
|
kws["track_id"] = trackID
|
||||||
|
query := &q.Query{
|
||||||
|
Keywords: kws,
|
||||||
|
}
|
||||||
|
|
||||||
|
l, err := ListReports(query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return l, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkStatus(trackID string, status string) error {
|
||||||
|
l, err := list(trackID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range l {
|
||||||
|
if r.Status != status {
|
||||||
|
return errors.Errorf("status is not matched: current %s : expected %s", r.Status, status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -109,6 +109,9 @@ func ListRegistrations(query *q.Query) ([]*Registration, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Order the list
|
||||||
|
qt = qt.OrderBy("-is_default", "-create_time")
|
||||||
|
|
||||||
l := make([]*Registration, 0)
|
l := make([]*Registration, 0)
|
||||||
_, err := qt.All(&l)
|
_, err := qt.All(&l)
|
||||||
|
|
||||||
|
@ -17,6 +17,8 @@ package report
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/pkg/scan/all"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/jobservice/job"
|
"github.com/goharbor/harbor/src/jobservice/job"
|
||||||
"github.com/goharbor/harbor/src/pkg/q"
|
"github.com/goharbor/harbor/src/pkg/q"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
||||||
@ -237,3 +239,49 @@ func (bm *basicManager) DeleteByDigests(digests ...string) error {
|
|||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetStats ...
|
||||||
|
func (bm *basicManager) GetStats(requester string) (*all.Stats, error) {
|
||||||
|
if len(requester) == 0 {
|
||||||
|
return nil, errors.New("empty requester")
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := scan.GetScanStats(requester)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "report manager: get stats")
|
||||||
|
}
|
||||||
|
|
||||||
|
sts := &all.Stats{
|
||||||
|
Metrics: make(all.StatusMetrics),
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range m {
|
||||||
|
// Increase the total metrics
|
||||||
|
sts.Total += v
|
||||||
|
|
||||||
|
s := job.Status(k)
|
||||||
|
// Increase the completed metrics if the status is not predefined ones or
|
||||||
|
// the status is the final status.
|
||||||
|
if s.Validate() != nil || s.Final() {
|
||||||
|
sts.Completed += v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not standard error status.
|
||||||
|
// Convert it to standard error status.
|
||||||
|
if s.Validate() != nil {
|
||||||
|
tv := v
|
||||||
|
|
||||||
|
if val, ok := sts.Metrics[job.ErrorStatus.String()]; ok {
|
||||||
|
tv = val + v
|
||||||
|
}
|
||||||
|
sts.Metrics[job.ErrorStatus.String()] = tv
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sts.Metrics[k] = v
|
||||||
|
}
|
||||||
|
sts.Requester = requester
|
||||||
|
|
||||||
|
return sts, nil
|
||||||
|
}
|
||||||
|
@ -53,6 +53,7 @@ func (suite *TestManagerSuite) SetupTest() {
|
|||||||
RegistrationUUID: "ruuid",
|
RegistrationUUID: "ruuid",
|
||||||
MimeType: v1.MimeTypeNativeReport,
|
MimeType: v1.MimeTypeNativeReport,
|
||||||
TrackID: "tid001",
|
TrackID: "tid001",
|
||||||
|
Requester: "requester",
|
||||||
}
|
}
|
||||||
|
|
||||||
uuid, err := suite.m.Create(rp)
|
uuid, err := suite.m.Create(rp)
|
||||||
@ -188,3 +189,37 @@ func (suite *TestManagerSuite) TestManagerDeleteByDigests() {
|
|||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.Nil(r)
|
suite.Nil(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestManagerGetStats ...
|
||||||
|
func (suite *TestManagerSuite) TestManagerGetStats() {
|
||||||
|
// Mock new data
|
||||||
|
rp := &scan.Report{
|
||||||
|
Digest: "d1001",
|
||||||
|
RegistrationUUID: "ruuid",
|
||||||
|
MimeType: v1.MimeTypeNativeReport,
|
||||||
|
TrackID: "tid002",
|
||||||
|
Requester: "requester",
|
||||||
|
}
|
||||||
|
|
||||||
|
uuid, err := suite.m.Create(rp)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
require.NotEmpty(suite.T(), uuid)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := scan.DeleteReport(uuid)
|
||||||
|
suite.NoError(err, "clear test data")
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = suite.m.UpdateStatus("tid002", job.SuccessStatus.String(), 1000)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
st, err := suite.m.GetStats("requester")
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
require.NotNil(suite.T(), st)
|
||||||
|
|
||||||
|
suite.Equal(uint(2), st.Total)
|
||||||
|
suite.Equal(uint(1), st.Completed)
|
||||||
|
suite.Equal(2, len(st.Metrics))
|
||||||
|
suite.Equal(uint(1), st.Metrics[job.SuccessStatus.String()])
|
||||||
|
suite.Equal(uint(1), st.Metrics[job.PendingStatus.String()])
|
||||||
|
}
|
||||||
|
@ -14,7 +14,10 @@
|
|||||||
|
|
||||||
package report
|
package report
|
||||||
|
|
||||||
import "github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
import (
|
||||||
|
"github.com/goharbor/harbor/src/pkg/scan/all"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
||||||
|
)
|
||||||
|
|
||||||
// Manager is used to manage the scan reports.
|
// Manager is used to manage the scan reports.
|
||||||
type Manager interface {
|
type Manager interface {
|
||||||
@ -96,4 +99,14 @@ type Manager interface {
|
|||||||
// Returns:
|
// Returns:
|
||||||
// error : non nil error if any errors occurred
|
// error : non nil error if any errors occurred
|
||||||
DeleteByDigests(digests ...string) error
|
DeleteByDigests(digests ...string) error
|
||||||
|
|
||||||
|
// GetStats retrieves and calculates the overall report stats organized by status targeting the
|
||||||
|
// given requester.
|
||||||
|
// Arguments:
|
||||||
|
// requester string : the requester of the scan (all)
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// *all.AllStats: stats object including the related metric data
|
||||||
|
// error : non nil error if any errors occurred
|
||||||
|
GetStats(requester string) (*all.Stats, error)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user