mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-23 00:57:44 +01:00
add scan report CRUD supporting and
- change error collection in scan job - add dead client checking in client pool - change key word type to interface{} for q.Query - update bearer authorizer - add required UT cases Signed-off-by: Steven Zou <szou@vmware.com>
This commit is contained in:
parent
0c19eba8c2
commit
d616bc3509
@ -19,12 +19,14 @@ CREATE TABLE scanner_registration
|
|||||||
CREATE TABLE scan_report
|
CREATE TABLE scan_report
|
||||||
(
|
(
|
||||||
id SERIAL PRIMARY KEY NOT NULL,
|
id SERIAL PRIMARY KEY NOT NULL,
|
||||||
|
uuid VARCHAR(64) UNIQUE NOT NULL,
|
||||||
digest VARCHAR(256) NOT NULL,
|
digest VARCHAR(256) NOT NULL,
|
||||||
registration_uuid VARCHAR(64) NOT NULL,
|
registration_uuid VARCHAR(64) NOT NULL,
|
||||||
mime_type VARCHAR(256) NOT NULL,
|
mime_type VARCHAR(256) NOT NULL,
|
||||||
job_id VARCHAR(32),
|
job_id VARCHAR(32),
|
||||||
status VARCHAR(16) NOT NULL,
|
status VARCHAR(16) NOT NULL,
|
||||||
status_code INTEGER DEFAULT 0,
|
status_code INTEGER DEFAULT 0,
|
||||||
|
status_rev BIGINT DEFAULT 0,
|
||||||
report JSON,
|
report JSON,
|
||||||
start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
end_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
end_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
@ -76,7 +76,7 @@ func (sa *ScannerAPI) List() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get query key words
|
// Get query key words
|
||||||
kws := make(map[string]string)
|
kws := make(map[string]interface{})
|
||||||
properties := []string{"name", "description", "url"}
|
properties := []string{"name", "description", "url"}
|
||||||
for _, k := range properties {
|
for _, k := range properties {
|
||||||
kw := sa.GetString(k)
|
kw := sa.GetString(k)
|
||||||
@ -316,7 +316,7 @@ func (sa *ScannerAPI) get() *scanner.Registration {
|
|||||||
|
|
||||||
func (sa *ScannerAPI) checkDuplicated(property, value string) bool {
|
func (sa *ScannerAPI) checkDuplicated(property, value string) bool {
|
||||||
// Explicitly check if conflict
|
// Explicitly check if conflict
|
||||||
kw := make(map[string]string)
|
kw := make(map[string]interface{})
|
||||||
kw[property] = value
|
kw[property] = value
|
||||||
|
|
||||||
query := &q.Query{
|
query := &q.Query{
|
||||||
|
@ -296,7 +296,7 @@ func (suite *ScannerAPITestSuite) TestScannerAPIProjectScanner() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ScannerAPITestSuite) mockQuery(r *scanner.Registration) {
|
func (suite *ScannerAPITestSuite) mockQuery(r *scanner.Registration) {
|
||||||
kw := make(map[string]string, 1)
|
kw := make(map[string]interface{}, 1)
|
||||||
kw["name"] = r.Name
|
kw["name"] = r.Name
|
||||||
query := &q.Query{
|
query := &q.Query{
|
||||||
Keywords: kw,
|
Keywords: kw,
|
||||||
@ -304,7 +304,7 @@ func (suite *ScannerAPITestSuite) mockQuery(r *scanner.Registration) {
|
|||||||
emptyL := make([]*scanner.Registration, 0)
|
emptyL := make([]*scanner.Registration, 0)
|
||||||
suite.mockC.On("ListRegistrations", query).Return(emptyL, nil)
|
suite.mockC.On("ListRegistrations", query).Return(emptyL, nil)
|
||||||
|
|
||||||
kw2 := make(map[string]string, 1)
|
kw2 := make(map[string]interface{}, 1)
|
||||||
kw2["url"] = r.URL
|
kw2["url"] = r.URL
|
||||||
query2 := &q.Query{
|
query2 := &q.Query{
|
||||||
Keywords: kw2,
|
Keywords: kw2,
|
||||||
|
@ -21,5 +21,5 @@ type Query struct {
|
|||||||
// Page size
|
// Page size
|
||||||
PageSize int64
|
PageSize int64
|
||||||
// List of key words
|
// List of key words
|
||||||
Keywords map[string]string
|
Keywords map[string]interface{}
|
||||||
}
|
}
|
||||||
|
140
src/pkg/scan/dao/scan/report.go
Normal file
140
src/pkg/scan/dao/scan/report.go
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/astaxie/beego/orm"
|
||||||
|
"github.com/goharbor/harbor/src/common/dao"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/q"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
orm.RegisterModel(new(Report))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateReport creates new report
|
||||||
|
func CreateReport(r *Report) (int64, error) {
|
||||||
|
o := dao.GetOrmer()
|
||||||
|
return o.Insert(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteReport deletes the given report
|
||||||
|
func DeleteReport(uuid string) error {
|
||||||
|
o := dao.GetOrmer()
|
||||||
|
qt := o.QueryTable(new(Report))
|
||||||
|
|
||||||
|
// Delete report with query way
|
||||||
|
count, err := qt.Filter("uuid", uuid).Delete()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
return errors.Errorf("no report with uuid %s deleted", uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListReports lists the reports with given query parameters.
|
||||||
|
// Keywords in query here will be enforced with `exact` way.
|
||||||
|
func ListReports(query *q.Query) ([]*Report, error) {
|
||||||
|
o := dao.GetOrmer()
|
||||||
|
qt := o.QueryTable(new(Report))
|
||||||
|
|
||||||
|
if query != nil {
|
||||||
|
if len(query.Keywords) > 0 {
|
||||||
|
for k, v := range query.Keywords {
|
||||||
|
if vv, ok := v.([]interface{}); ok {
|
||||||
|
qt = qt.Filter(fmt.Sprintf("%s__in", k), vv...)
|
||||||
|
}
|
||||||
|
|
||||||
|
qt = qt.Filter(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.PageNumber > 0 && query.PageSize > 0 {
|
||||||
|
qt = qt.Limit(query.PageSize, (query.PageNumber-1)*query.PageSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
l := make([]*Report, 0)
|
||||||
|
_, err := qt.All(&l)
|
||||||
|
|
||||||
|
return l, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateReportData only updates the `report` column with conditions matched.
|
||||||
|
func UpdateReportData(uuid string, report string, statusRev int64) error {
|
||||||
|
o := dao.GetOrmer()
|
||||||
|
qt := o.QueryTable(new(Report))
|
||||||
|
|
||||||
|
data := make(orm.Params)
|
||||||
|
data["report"] = report
|
||||||
|
data["status_rev"] = statusRev
|
||||||
|
|
||||||
|
count, err := qt.Filter("uuid", uuid).
|
||||||
|
Filter("status_rev__lte", statusRev).Update(data)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
return errors.Errorf("no report with uuid %s updated", uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateReportStatus updates the report `status` with conditions matched.
|
||||||
|
func UpdateReportStatus(uuid string, status string, statusCode int, statusRev int64) error {
|
||||||
|
o := dao.GetOrmer()
|
||||||
|
qt := o.QueryTable(new(Report))
|
||||||
|
|
||||||
|
data := make(orm.Params)
|
||||||
|
data["status"] = status
|
||||||
|
data["status_code"] = statusCode
|
||||||
|
data["status_rev"] = statusRev
|
||||||
|
|
||||||
|
count, err := qt.Filter("uuid", uuid).
|
||||||
|
Filter("status_rev__lte", statusRev).
|
||||||
|
Filter("status_code__lte", statusCode).Update(data)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
return errors.Errorf("no report with uuid %s updated", uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateJobID updates the report `job_id` column
|
||||||
|
func UpdateJobID(uuid string, jobID string) error {
|
||||||
|
o := dao.GetOrmer()
|
||||||
|
qt := o.QueryTable(new(Report))
|
||||||
|
|
||||||
|
params := make(orm.Params, 1)
|
||||||
|
params["job_id"] = jobID
|
||||||
|
_, err := qt.Filter("uuid", uuid).Update(params)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
131
src/pkg/scan/dao/scan/report_test.go
Normal file
131
src/pkg/scan/dao/scan/report_test.go
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
// 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 (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/common/dao"
|
||||||
|
"github.com/goharbor/harbor/src/jobservice/job"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/q"
|
||||||
|
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReportTestSuite is test suite of testing report DAO.
|
||||||
|
type ReportTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestReport is the entry of ReportTestSuite.
|
||||||
|
func TestReport(t *testing.T) {
|
||||||
|
suite.Run(t, &ReportTestSuite{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupSuite prepares env for test suite.
|
||||||
|
func (suite *ReportTestSuite) SetupSuite() {
|
||||||
|
dao.PrepareTestForPostgresSQL()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupTest prepares env for test case.
|
||||||
|
func (suite *ReportTestSuite) SetupTest() {
|
||||||
|
r := &Report{
|
||||||
|
UUID: "uuid",
|
||||||
|
Digest: "digest1001",
|
||||||
|
RegistrationUUID: "ruuid",
|
||||||
|
MimeType: v1.MimeTypeNativeReport,
|
||||||
|
Status: job.PendingStatus.String(),
|
||||||
|
StatusCode: job.PendingStatus.Code(),
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := CreateReport(r)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
require.Condition(suite.T(), func() (success bool) {
|
||||||
|
success = id > 0
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TearDownTest clears enf for test case.
|
||||||
|
func (suite *ReportTestSuite) TearDownTest() {
|
||||||
|
err := DeleteReport("uuid")
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestReportList tests list reports with query parameters.
|
||||||
|
func (suite *ReportTestSuite) TestReportList() {
|
||||||
|
query1 := &q.Query{
|
||||||
|
PageSize: 1,
|
||||||
|
PageNumber: 1,
|
||||||
|
Keywords: map[string]interface{}{
|
||||||
|
"digest": "digest1001",
|
||||||
|
"registration_uuid": "ruuid",
|
||||||
|
"mime_type": v1.MimeTypeNativeReport,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
l, err := ListReports(query1)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
require.Equal(suite.T(), 1, len(l))
|
||||||
|
|
||||||
|
query2 := &q.Query{
|
||||||
|
PageSize: 1,
|
||||||
|
PageNumber: 1,
|
||||||
|
Keywords: map[string]interface{}{
|
||||||
|
"digest": "digest1002",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
l, err = ListReports(query2)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
require.Equal(suite.T(), 0, len(l))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestReportUpdateJobID tests update job ID of the report.
|
||||||
|
func (suite *ReportTestSuite) TestReportUpdateJobID() {
|
||||||
|
err := UpdateJobID("uuid", "jobid001")
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
l, err := ListReports(nil)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
require.Equal(suite.T(), 1, len(l))
|
||||||
|
assert.Equal(suite.T(), "jobid001", l[0].JobID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestReportUpdateReportData tests update the report data.
|
||||||
|
func (suite *ReportTestSuite) TestReportUpdateReportData() {
|
||||||
|
err := UpdateReportData("uuid", "{}", 1000)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
l, err := ListReports(nil)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
require.Equal(suite.T(), 1, len(l))
|
||||||
|
assert.Equal(suite.T(), "{}", l[0].Report)
|
||||||
|
|
||||||
|
err = UpdateReportData("uuid", "{\"a\": 900}", 900)
|
||||||
|
require.Error(suite.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestReportUpdateStatus tests update the report status.
|
||||||
|
func (suite *ReportTestSuite) TestReportUpdateStatus() {
|
||||||
|
err := UpdateReportStatus("uuid", job.RunningStatus.String(), job.RunningStatus.Code(), 1000)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
err = UpdateReportStatus("uuid", job.RunningStatus.String(), job.RunningStatus.Code(), 900)
|
||||||
|
require.Error(suite.T(), err)
|
||||||
|
|
||||||
|
err = UpdateReportStatus("uuid", job.PendingStatus.String(), job.PendingStatus.Code(), 1000)
|
||||||
|
require.Error(suite.T(), err)
|
||||||
|
}
|
@ -107,7 +107,7 @@ func (suite *RegistrationDAOTestSuite) TestList() {
|
|||||||
require.Equal(suite.T(), 1, len(l))
|
require.Equal(suite.T(), 1, len(l))
|
||||||
|
|
||||||
// with query and found items
|
// with query and found items
|
||||||
keywords := make(map[string]string)
|
keywords := make(map[string]interface{})
|
||||||
keywords["description"] = "sample"
|
keywords["description"] = "sample"
|
||||||
l, err = ListRegistrations(&q.Query{
|
l, err = ListRegistrations(&q.Query{
|
||||||
PageSize: 5,
|
PageSize: 5,
|
||||||
|
@ -129,43 +129,15 @@ func (j *Job) Run(ctx job.Context, params job.Parameters) error {
|
|||||||
return errors.Wrap(err, "run scan job")
|
return errors.Wrap(err, "run scan job")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exit signal
|
// For collecting errors
|
||||||
exit := make(chan bool, 1)
|
errs := make([]error, len(mimes))
|
||||||
// Done signal
|
|
||||||
done := make(chan bool, 1)
|
|
||||||
// Collect errors
|
|
||||||
eChan := make(chan error, 1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer func() {
|
|
||||||
// Done!
|
|
||||||
done <- true
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case e := <-eChan:
|
|
||||||
if err != nil {
|
|
||||||
err = errors.Wrap(e, err.Error())
|
|
||||||
} else {
|
|
||||||
err = e
|
|
||||||
}
|
|
||||||
case <-exit:
|
|
||||||
// Gracefully exit
|
|
||||||
return
|
|
||||||
case <-ctx.SystemContext().Done():
|
|
||||||
// Terminated by system
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Concurrently retrieving report by different mime types
|
// Concurrently retrieving report by different mime types
|
||||||
wg := &sync.WaitGroup{}
|
wg := &sync.WaitGroup{}
|
||||||
wg.Add(len(mimes))
|
wg.Add(len(mimes))
|
||||||
|
|
||||||
for _, mt := range mimes {
|
for i, mt := range mimes {
|
||||||
go func(m string) {
|
go func(i int, m string) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
// Log info
|
// Log info
|
||||||
@ -191,13 +163,13 @@ func (j *Job) Run(ctx job.Context, params job.Parameters) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
eChan <- errors.Wrap(err, fmt.Sprintf("check scan report with mime type %s", m))
|
errs[i] = errors.Wrap(err, fmt.Sprintf("check scan report with mime type %s", m))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the data is aligned with the v1 spec.
|
// Make sure the data is aligned with the v1 spec.
|
||||||
if _, err = report.ResolveData(m, []byte(rawReport)); err != nil {
|
if _, err = report.ResolveData(m, []byte(rawReport)); err != nil {
|
||||||
eChan <- errors.Wrap(err, "scan job: resolve report data")
|
errs[i] = errors.Wrap(err, "scan job: resolve report data")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,25 +194,33 @@ func (j *Job) Run(ctx job.Context, params job.Parameters) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send error and exit
|
// Send error and exit
|
||||||
eChan <- errors.Wrap(er, fmt.Sprintf("check in scan report for mime type %s", m))
|
errs[i] = errors.Wrap(er, fmt.Sprintf("check in scan report for mime type %s", m))
|
||||||
return
|
return
|
||||||
case <-ctx.SystemContext().Done():
|
case <-ctx.SystemContext().Done():
|
||||||
// Terminated by system
|
// Terminated by system
|
||||||
return
|
return
|
||||||
case <-time.After(checkTimeout):
|
case <-time.After(checkTimeout):
|
||||||
eChan <- errors.New("check scan report timeout")
|
errs[i] = errors.New("check scan report timeout")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}(mt)
|
}(i, mt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for all the retrieving routines are completed
|
// Wait for all the retrieving routines are completed
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
// Stop error collection goroutine
|
|
||||||
exit <- true
|
// Merge errors
|
||||||
// done!
|
for _, e := range errs {
|
||||||
<-done
|
if e != nil {
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(e, err.Error())
|
||||||
|
} else {
|
||||||
|
err = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Log error to the job log
|
// Log error to the job log
|
||||||
if err != nil {
|
if err != nil {
|
||||||
myLogger.Error(err)
|
myLogger.Error(err)
|
||||||
|
169
src/pkg/scan/report/base_manager.go
Normal file
169
src/pkg/scan/report/base_manager.go
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
// 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 report
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/jobservice/job"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/q"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// basicManager is a default implementation of report manager.
|
||||||
|
type basicManager struct{}
|
||||||
|
|
||||||
|
// NewManager news basic manager.
|
||||||
|
func NewManager() Manager {
|
||||||
|
return &basicManager{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create ...
|
||||||
|
func (bm *basicManager) Create(r *scan.Report) (string, error) {
|
||||||
|
// Validate report object
|
||||||
|
if r == nil {
|
||||||
|
return "", errors.New("nil scan report object")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r.Digest) == 0 || len(r.RegistrationUUID) == 0 || len(r.MimeType) == 0 {
|
||||||
|
return "", errors.New("malformed scan report object")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there is existing report copy
|
||||||
|
// Limit only one scanning performed by a given provider on the specified artifact can be there
|
||||||
|
kws := make(map[string]interface{}, 3)
|
||||||
|
kws["digest"] = r.Digest
|
||||||
|
kws["registration_uuid"] = r.RegistrationUUID
|
||||||
|
kws["mime_type"] = []interface{}{r.MimeType}
|
||||||
|
|
||||||
|
existingCopies, err := scan.ListReports(&q.Query{
|
||||||
|
PageNumber: 1,
|
||||||
|
PageSize: 1,
|
||||||
|
Keywords: kws,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "check existence of report")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete existing copy
|
||||||
|
if len(existingCopies) > 0 {
|
||||||
|
theCopy := existingCopies[0]
|
||||||
|
|
||||||
|
// Status conflict
|
||||||
|
theStatus := job.Status(theCopy.Status)
|
||||||
|
if theStatus.Compare(job.RunningStatus) <= 0 {
|
||||||
|
return "", errors.Errorf("conflict: a previous scanning is %s", theCopy.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise it will be a completed report
|
||||||
|
// Clear it before insert this new one
|
||||||
|
if err := scan.DeleteReport(theCopy.UUID); err != nil {
|
||||||
|
return "", errors.Wrap(err, "clear old scan report")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign uuid
|
||||||
|
UUID, err := uuid.NewUUID()
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "create report: new UUID")
|
||||||
|
}
|
||||||
|
r.UUID = UUID.String()
|
||||||
|
|
||||||
|
// Fill in / override the related properties
|
||||||
|
r.StartTime = time.Now().UTC()
|
||||||
|
r.Status = job.PendingStatus.String()
|
||||||
|
r.StatusCode = job.PendingStatus.Code()
|
||||||
|
|
||||||
|
// Insert
|
||||||
|
if _, err = scan.CreateReport(r); err != nil {
|
||||||
|
return "", errors.Wrap(err, "create report")
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.UUID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBy ...
|
||||||
|
func (bm *basicManager) GetBy(digest string, registrationUUID string, mimeTypes []string) ([]*scan.Report, error) {
|
||||||
|
if len(digest) == 0 {
|
||||||
|
return nil, errors.New("empty digest to get report data")
|
||||||
|
}
|
||||||
|
|
||||||
|
kws := make(map[string]interface{})
|
||||||
|
kws["digest"] = digest
|
||||||
|
if len(registrationUUID) > 0 {
|
||||||
|
kws["registration_uuid"] = registrationUUID
|
||||||
|
}
|
||||||
|
if len(mimeTypes) > 0 {
|
||||||
|
kws["mime_type"] = mimeTypes
|
||||||
|
}
|
||||||
|
// Query all
|
||||||
|
query := &q.Query{
|
||||||
|
PageNumber: 0,
|
||||||
|
Keywords: kws,
|
||||||
|
}
|
||||||
|
|
||||||
|
return scan.ListReports(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateScanJobID ...
|
||||||
|
func (bm *basicManager) UpdateScanJobID(uuid string, jobID string) error {
|
||||||
|
if len(uuid) == 0 || len(jobID) == 0 {
|
||||||
|
return errors.New("bad arguments")
|
||||||
|
}
|
||||||
|
|
||||||
|
return scan.UpdateJobID(uuid, jobID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStatus ...
|
||||||
|
func (bm *basicManager) UpdateStatus(uuid string, status string, rev int64) error {
|
||||||
|
if len(uuid) == 0 {
|
||||||
|
return errors.New("missing uuid")
|
||||||
|
}
|
||||||
|
|
||||||
|
if rev <= 0 {
|
||||||
|
return errors.New("invalid data revision")
|
||||||
|
}
|
||||||
|
|
||||||
|
stCode := job.ErrorStatus.Code()
|
||||||
|
st := job.Status(status)
|
||||||
|
// Check if it is job valid status.
|
||||||
|
// Probably an error happened before submitting jobs.
|
||||||
|
if st.Code() != -1 {
|
||||||
|
// Assign error code
|
||||||
|
stCode = st.Code()
|
||||||
|
}
|
||||||
|
|
||||||
|
return scan.UpdateReportStatus(uuid, status, stCode, rev)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateReportData ...
|
||||||
|
func (bm *basicManager) UpdateReportData(uuid string, report string, rev int64) error {
|
||||||
|
if len(uuid) == 0 {
|
||||||
|
return errors.New("missing uuid")
|
||||||
|
}
|
||||||
|
|
||||||
|
if rev <= 0 {
|
||||||
|
return errors.New("invalid data revision")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(report) == 0 {
|
||||||
|
return errors.New("missing report JSON data")
|
||||||
|
}
|
||||||
|
|
||||||
|
return scan.UpdateReportData(uuid, report, rev)
|
||||||
|
}
|
156
src/pkg/scan/report/base_manager_test.go
Normal file
156
src/pkg/scan/report/base_manager_test.go
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
// 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 report
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/common/dao"
|
||||||
|
"github.com/goharbor/harbor/src/jobservice/job"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
||||||
|
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestManagerSuite is a test suite for the report manager.
|
||||||
|
type TestManagerSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
m Manager
|
||||||
|
rpUUID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestManager is an entry of suite TestManagerSuite.
|
||||||
|
func TestManager(t *testing.T) {
|
||||||
|
suite.Run(t, &TestManagerSuite{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupSuite prepares test env for suite TestManagerSuite.
|
||||||
|
func (suite *TestManagerSuite) SetupSuite() {
|
||||||
|
dao.PrepareTestForPostgresSQL()
|
||||||
|
|
||||||
|
suite.m = NewManager()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupTest prepares env for test cases.
|
||||||
|
func (suite *TestManagerSuite) SetupTest() {
|
||||||
|
rp := &scan.Report{
|
||||||
|
Digest: "d1000",
|
||||||
|
RegistrationUUID: "ruuid",
|
||||||
|
MimeType: v1.MimeTypeNativeReport,
|
||||||
|
}
|
||||||
|
|
||||||
|
uuid, err := suite.m.Create(rp)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
require.NotEmpty(suite.T(), uuid)
|
||||||
|
suite.rpUUID = uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
// TearDownTest clears test env for test cases.
|
||||||
|
func (suite *TestManagerSuite) TearDownTest() {
|
||||||
|
// No delete method defined in manager as no requirement,
|
||||||
|
// so, to clear env, call dao method here
|
||||||
|
err := scan.DeleteReport(suite.rpUUID)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestManagerCreateWithExisting tests the case that a copy already is there when creating report.
|
||||||
|
func (suite *TestManagerSuite) TestManagerCreateWithExisting() {
|
||||||
|
err := suite.m.UpdateStatus(suite.rpUUID, job.SuccessStatus.String(), 2000)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
rp := &scan.Report{
|
||||||
|
Digest: "d1000",
|
||||||
|
RegistrationUUID: "ruuid",
|
||||||
|
MimeType: v1.MimeTypeNativeReport,
|
||||||
|
}
|
||||||
|
|
||||||
|
uuid, err := suite.m.Create(rp)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
require.NotEmpty(suite.T(), uuid)
|
||||||
|
|
||||||
|
assert.NotEqual(suite.T(), suite.rpUUID, uuid)
|
||||||
|
suite.rpUUID = uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestManagerGetBy tests the get by method.
|
||||||
|
func (suite *TestManagerSuite) TestManagerGetBy() {
|
||||||
|
l, err := suite.m.GetBy("d1000", "ruuid", []string{v1.MimeTypeNativeReport})
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
require.Equal(suite.T(), 1, len(l))
|
||||||
|
assert.Equal(suite.T(), suite.rpUUID, l[0].UUID)
|
||||||
|
|
||||||
|
l, err = suite.m.GetBy("d1000", "ruuid", nil)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
require.Equal(suite.T(), 1, len(l))
|
||||||
|
assert.Equal(suite.T(), suite.rpUUID, l[0].UUID)
|
||||||
|
|
||||||
|
l, err = suite.m.GetBy("d1000", "", nil)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
require.Equal(suite.T(), 1, len(l))
|
||||||
|
assert.Equal(suite.T(), suite.rpUUID, l[0].UUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestManagerUpdateJobID tests update job ID method.
|
||||||
|
func (suite *TestManagerSuite) TestManagerUpdateJobID() {
|
||||||
|
l, err := suite.m.GetBy("d1000", "ruuid", []string{v1.MimeTypeNativeReport})
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
require.Equal(suite.T(), 1, len(l))
|
||||||
|
|
||||||
|
oldJID := l[0].JobID
|
||||||
|
|
||||||
|
err = suite.m.UpdateScanJobID(suite.rpUUID, "jID1001")
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
l, err = suite.m.GetBy("d1000", "ruuid", []string{v1.MimeTypeNativeReport})
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
require.Equal(suite.T(), 1, len(l))
|
||||||
|
|
||||||
|
assert.NotEqual(suite.T(), oldJID, l[0].JobID)
|
||||||
|
assert.Equal(suite.T(), "jID1001", l[0].JobID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestManagerUpdateStatus tests update status method
|
||||||
|
func (suite *TestManagerSuite) TestManagerUpdateStatus() {
|
||||||
|
l, err := suite.m.GetBy("d1000", "ruuid", []string{v1.MimeTypeNativeReport})
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
require.Equal(suite.T(), 1, len(l))
|
||||||
|
|
||||||
|
oldSt := l[0].Status
|
||||||
|
|
||||||
|
err = suite.m.UpdateStatus(suite.rpUUID, job.SuccessStatus.String(), 10000)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
l, err = suite.m.GetBy("d1000", "ruuid", []string{v1.MimeTypeNativeReport})
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
require.Equal(suite.T(), 1, len(l))
|
||||||
|
|
||||||
|
assert.NotEqual(suite.T(), oldSt, l[0].Status)
|
||||||
|
assert.Equal(suite.T(), job.SuccessStatus.String(), l[0].Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestManagerUpdateReportData tests update job report data.
|
||||||
|
func (suite *TestManagerSuite) TestManagerUpdateReportData() {
|
||||||
|
err := suite.m.UpdateReportData(suite.rpUUID, "{\"a\":1000}", 1000)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
l, err := suite.m.GetBy("d1000", "ruuid", []string{v1.MimeTypeNativeReport})
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
require.Equal(suite.T(), 1, len(l))
|
||||||
|
|
||||||
|
assert.Equal(suite.T(), "{\"a\":1000}", l[0].Report)
|
||||||
|
}
|
80
src/pkg/scan/report/manager.go
Normal file
80
src/pkg/scan/report/manager.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
// 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 report
|
||||||
|
|
||||||
|
import "github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
||||||
|
|
||||||
|
// Manager is used to manage the scan reports.
|
||||||
|
type Manager interface {
|
||||||
|
// Create a new report record.
|
||||||
|
//
|
||||||
|
// Arguments:
|
||||||
|
// r *scan.Report : report model to be created
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// string : uuid of the new report
|
||||||
|
// error : non nil error if any errors occurred
|
||||||
|
//
|
||||||
|
Create(r *scan.Report) (string, error)
|
||||||
|
|
||||||
|
// Update the scan job ID of the given report.
|
||||||
|
//
|
||||||
|
// Arguments:
|
||||||
|
// uuid string : uuid to identify the report
|
||||||
|
// jobID string: scan job ID
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// error : non nil error if any errors occurred
|
||||||
|
//
|
||||||
|
UpdateScanJobID(uuid string, jobID string) error
|
||||||
|
|
||||||
|
// Update the status (mapping to the scan job status) of the given report.
|
||||||
|
//
|
||||||
|
// Arguments:
|
||||||
|
// uuid string : uuid to identify the report
|
||||||
|
// status string: status info
|
||||||
|
// rev int64 : data revision info
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// error : non nil error if any errors occurred
|
||||||
|
//
|
||||||
|
UpdateStatus(uuid string, status string, rev int64) error
|
||||||
|
|
||||||
|
// Update the report data (with JSON format) of the given report.
|
||||||
|
//
|
||||||
|
// Arguments:
|
||||||
|
// uuid string : uuid to identify the report
|
||||||
|
// report string: report JSON data
|
||||||
|
// rev int64 : data revision info
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// error : non nil error if any errors occurred
|
||||||
|
//
|
||||||
|
UpdateReportData(uuid string, report string, rev int64) error
|
||||||
|
|
||||||
|
// Get the reports for the given digest by other properties.
|
||||||
|
//
|
||||||
|
// Arguments:
|
||||||
|
// digest string : digest of the artifact
|
||||||
|
// registrationUUID string : [optional] the report generated by which registration.
|
||||||
|
// If it is empty, reports by all the registrations are retrieved.
|
||||||
|
// mimeTypes []string : [optional] mime types of the reports requiring
|
||||||
|
// If empty array is specified, reports with all the supported mimes are retrieved.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// []*scan.Report : report list
|
||||||
|
// error : non nil error if any errors occurred
|
||||||
|
GetBy(digest string, registrationUUID string, mimeTypes []string) ([]*scan.Report, error)
|
||||||
|
}
|
@ -17,6 +17,8 @@ package auth
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// bearerAuthorizer authorizes the request by adding `Authorization Bearer credential` header
|
// bearerAuthorizer authorizes the request by adding `Authorization Bearer credential` header
|
||||||
@ -31,7 +33,7 @@ func (ba *bearerAuthorizer) Authorize(req *http.Request) error {
|
|||||||
req.Header.Add(authorization, fmt.Sprintf("%s %s", ba.typeID, ba.accessCred))
|
req.Header.Add(authorization, fmt.Sprintf("%s %s", ba.typeID, ba.accessCred))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return errors.Errorf("%s: %s", ba.typeID, "missing data to authorize request")
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBearerAuth create bearer authorizer
|
// NewBearerAuth create bearer authorizer
|
||||||
|
@ -15,16 +15,24 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultDeadCheckInterval = 1 * time.Minute
|
||||||
|
defaultExpireTime = 5 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
// DefaultClientPool is a default client pool.
|
// DefaultClientPool is a default client pool.
|
||||||
var DefaultClientPool = NewClientPool()
|
var DefaultClientPool = NewClientPool(nil)
|
||||||
|
|
||||||
// ClientPool defines operations for the client pool which provides v1 client cache.
|
// ClientPool defines operations for the client pool which provides v1 client cache.
|
||||||
type ClientPool interface {
|
type ClientPool interface {
|
||||||
@ -39,16 +47,47 @@ type ClientPool interface {
|
|||||||
Get(r *scanner.Registration) (Client, error)
|
Get(r *scanner.Registration) (Client, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PoolConfig provides configurations for the client pool.
|
||||||
|
type PoolConfig struct {
|
||||||
|
// Interval for checking dead instance.
|
||||||
|
DeadCheckInterval time.Duration
|
||||||
|
// Expire time for the instance to be marked as dead.
|
||||||
|
ExpireTime time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// poolItem append timestamp for the caching client instance.
|
||||||
|
type poolItem struct {
|
||||||
|
c Client
|
||||||
|
timestamp time.Time
|
||||||
|
}
|
||||||
|
|
||||||
// basicClientPool is default implementation of client pool interface.
|
// basicClientPool is default implementation of client pool interface.
|
||||||
type basicClientPool struct {
|
type basicClientPool struct {
|
||||||
pool *sync.Map
|
pool *sync.Map
|
||||||
|
config *PoolConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClientPool news a basic client pool.
|
// NewClientPool news a basic client pool.
|
||||||
func NewClientPool() ClientPool {
|
func NewClientPool(config *PoolConfig) ClientPool {
|
||||||
return &basicClientPool{
|
bcp := &basicClientPool{
|
||||||
pool: &sync.Map{},
|
pool: &sync.Map{},
|
||||||
|
config: config,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set config
|
||||||
|
if bcp.config == nil {
|
||||||
|
bcp.config = &PoolConfig{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bcp.config.DeadCheckInterval == 0 {
|
||||||
|
bcp.config.DeadCheckInterval = defaultDeadCheckInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
if bcp.config.ExpireTime == 0 {
|
||||||
|
bcp.config.ExpireTime = defaultExpireTime
|
||||||
|
}
|
||||||
|
|
||||||
|
return bcp
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get client for the specified registration.
|
// Get client for the specified registration.
|
||||||
@ -57,38 +96,7 @@ func NewClientPool() ClientPool {
|
|||||||
// If one day, we have to clear unactivated client instances in the pool,
|
// If one day, we have to clear unactivated client instances in the pool,
|
||||||
// add the following func after the first time initializing the client.
|
// add the following func after the first time initializing the client.
|
||||||
// pool item represents the client with a timestamp of last accessed.
|
// pool item represents the client with a timestamp of last accessed.
|
||||||
//
|
|
||||||
// type poolItem struct {
|
|
||||||
// c Client
|
|
||||||
// timestamp time.Time
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// func (bcp *basicClientPool) deadCheck(key string, item *poolItem) {
|
|
||||||
// // Run in a separate goroutine
|
|
||||||
// go func() {
|
|
||||||
// // As we do not have a global context, let's watch the system signal to
|
|
||||||
// // exit the goroutine correctly.
|
|
||||||
// sig := make(chan os.Signal, 1)
|
|
||||||
// signal.Notify(sig, os.Interrupt, syscall.SIGTERM, os.Kill)
|
|
||||||
//
|
|
||||||
// tk := time.NewTicker(bcp.config.DeadCheckInterval)
|
|
||||||
// defer tk.Stop()
|
|
||||||
//
|
|
||||||
// for {
|
|
||||||
// select {
|
|
||||||
// case t := <-tk.C:
|
|
||||||
// if item.timestamp.Add(bcp.config.ExpireTime).Before(t.UTC()) {
|
|
||||||
// // Expired
|
|
||||||
// bcp.pool.Delete(key)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// case <-sig:
|
|
||||||
// // Terminated by system
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }()
|
|
||||||
// }
|
|
||||||
func (bcp *basicClientPool) Get(r *scanner.Registration) (Client, error) {
|
func (bcp *basicClientPool) Get(r *scanner.Registration) (Client, error) {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return nil, errors.New("nil scanner registration")
|
return nil, errors.New("nil scanner registration")
|
||||||
@ -100,7 +108,7 @@ func (bcp *basicClientPool) Get(r *scanner.Registration) (Client, error) {
|
|||||||
|
|
||||||
k := key(r)
|
k := key(r)
|
||||||
|
|
||||||
c, ok := bcp.pool.Load(k)
|
item, ok := bcp.pool.Load(k)
|
||||||
if !ok {
|
if !ok {
|
||||||
nc, err := NewClient(r)
|
nc, err := NewClient(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -108,21 +116,54 @@ func (bcp *basicClientPool) Get(r *scanner.Registration) (Client, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cache it
|
// Cache it
|
||||||
bcp.pool.Store(k, nc)
|
npi := &poolItem{
|
||||||
c = nc
|
c: nc,
|
||||||
|
timestamp: time.Now().UTC(),
|
||||||
|
}
|
||||||
|
|
||||||
|
bcp.pool.Store(k, npi)
|
||||||
|
item = npi
|
||||||
|
|
||||||
|
// dead check
|
||||||
|
bcp.deadCheck(k, npi)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.(Client), nil
|
return item.(*poolItem).c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bcp *basicClientPool) deadCheck(key string, item *poolItem) {
|
||||||
|
// Run in a separate goroutine
|
||||||
|
go func() {
|
||||||
|
// As we do not have a global context, let's watch the system signal to
|
||||||
|
// exit the goroutine correctly.
|
||||||
|
sig := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sig, os.Interrupt, syscall.SIGTERM, os.Kill)
|
||||||
|
|
||||||
|
tk := time.NewTicker(bcp.config.DeadCheckInterval)
|
||||||
|
defer tk.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case t := <-tk.C:
|
||||||
|
if item.timestamp.Add(bcp.config.ExpireTime).Before(t.UTC()) {
|
||||||
|
// Expired
|
||||||
|
bcp.pool.Delete(key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-sig:
|
||||||
|
// Terminated by system
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func key(r *scanner.Registration) string {
|
func key(r *scanner.Registration) string {
|
||||||
raw := fmt.Sprintf("%s:%s:%s:%s:%v",
|
return fmt.Sprintf("%s:%s:%s:%s:%v",
|
||||||
r.UUID,
|
r.UUID,
|
||||||
r.URL,
|
r.URL,
|
||||||
r.Auth,
|
r.Auth,
|
||||||
r.AccessCredential,
|
r.AccessCredential,
|
||||||
r.SkipCertVerify,
|
r.SkipCertVerify,
|
||||||
)
|
)
|
||||||
|
|
||||||
return base64.StdEncoding.EncodeToString([]byte(raw))
|
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ package v1
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/rest/auth"
|
"github.com/goharbor/harbor/src/pkg/scan/rest/auth"
|
||||||
@ -34,12 +35,16 @@ type ClientPoolTestSuite struct {
|
|||||||
|
|
||||||
// TestClientPool is the entry of ClientPoolTestSuite.
|
// TestClientPool is the entry of ClientPoolTestSuite.
|
||||||
func TestClientPool(t *testing.T) {
|
func TestClientPool(t *testing.T) {
|
||||||
suite.Run(t, new(ClientPoolTestSuite))
|
suite.Run(t, &ClientPoolTestSuite{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupSuite sets up test suite env.
|
// SetupSuite sets up test suite env.
|
||||||
func (suite *ClientPoolTestSuite) SetupSuite() {
|
func (suite *ClientPoolTestSuite) SetupSuite() {
|
||||||
suite.pool = NewClientPool()
|
cfg := &PoolConfig{
|
||||||
|
DeadCheckInterval: 100 * time.Millisecond,
|
||||||
|
ExpireTime: 300 * time.Millisecond,
|
||||||
|
}
|
||||||
|
suite.pool = NewClientPool(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestClientPoolGet tests the get method of client pool.
|
// TestClientPoolGet tests the get method of client pool.
|
||||||
@ -66,4 +71,12 @@ func (suite *ClientPoolTestSuite) TestClientPoolGet() {
|
|||||||
|
|
||||||
p2 := fmt.Sprintf("%p", client2.(*basicClient))
|
p2 := fmt.Sprintf("%p", client2.(*basicClient))
|
||||||
assert.Equal(suite.T(), p1, p2)
|
assert.Equal(suite.T(), p1, p2)
|
||||||
|
|
||||||
|
<-time.After(400 * time.Millisecond)
|
||||||
|
client3, err := suite.pool.Get(r)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
require.NotNil(suite.T(), client3)
|
||||||
|
|
||||||
|
p3 := fmt.Sprintf("%p", client3.(*basicClient))
|
||||||
|
assert.NotEqual(suite.T(), p2, p3)
|
||||||
}
|
}
|
||||||
|
@ -60,19 +60,17 @@ type Spec struct {
|
|||||||
|
|
||||||
// NewSpec news V1 spec
|
// NewSpec news V1 spec
|
||||||
func NewSpec(base string) *Spec {
|
func NewSpec(base string) *Spec {
|
||||||
baseRoute := "http://localhost"
|
s := &Spec{}
|
||||||
|
|
||||||
if len(base) > 0 {
|
if len(base) > 0 {
|
||||||
if strings.HasSuffix(base, "/") {
|
if strings.HasSuffix(base, "/") {
|
||||||
baseRoute = base[:len(base)-1]
|
s.baseRoute = base[:len(base)-1]
|
||||||
} else {
|
} else {
|
||||||
baseRoute = base
|
s.baseRoute = base
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Spec{
|
return s
|
||||||
baseRoute: baseRoute,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metadata API
|
// Metadata API
|
||||||
|
@ -63,7 +63,7 @@ func (suite *BasicManagerTestSuite) TearDownSuite() {
|
|||||||
|
|
||||||
// TestList tests list registrations
|
// TestList tests list registrations
|
||||||
func (suite *BasicManagerTestSuite) TestList() {
|
func (suite *BasicManagerTestSuite) TestList() {
|
||||||
m := make(map[string]string, 1)
|
m := make(map[string]interface{}, 1)
|
||||||
m["name"] = "forUT"
|
m["name"] = "forUT"
|
||||||
|
|
||||||
l, err := suite.mgr.List(&q.Query{
|
l, err := suite.mgr.List(&q.Query{
|
||||||
|
Loading…
Reference in New Issue
Block a user