System Artifact Manager database schema creation, model definitons, and tests (#16678)

Closes:
https://github.com/goharbor/harbor/issues/16540
https://github.com/goharbor/harbor/issues/16541
https://github.com/goharbor/harbor/issues/16542

Signed-off-by: prahaladdarkin <prahaladd@vmware.com>
This commit is contained in:
prahaladdarkin 2022-05-09 15:02:57 +05:30 committed by GitHub
parent 1f797fafc4
commit 27ec871185
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1823 additions and 1 deletions

View File

@ -1,2 +1,18 @@
/* Correct project_metadata.public value, should only be true or false, other invaild value will be rewrite to false */
UPDATE project_metadata SET value='false' WHERE name='public' AND value NOT IN('true', 'false');
UPDATE project_metadata SET value='false' WHERE name='public' AND value NOT IN('true', 'false');
/*
System Artifact Manager
Github proposal link : https://github.com/goharbor/community/pull/181
*/
CREATE TABLE IF NOT EXISTS system_artifact (
id SERIAL NOT NULL PRIMARY KEY,
repository varchar(256) NOT NULL,
digest varchar(255) NOT NULL DEFAULT '' ,
size bigint NOT NULL DEFAULT 0 ,
vendor varchar(255) NOT NULL DEFAULT '' ,
type varchar(255) NOT NULL DEFAULT '' ,
create_time timestamp default CURRENT_TIMESTAMP,
extra_attrs text NOT NULL DEFAULT '' ,
UNIQUE ("repository", "digest", "vendor")
);

View File

@ -0,0 +1,55 @@
package systemartifact
import (
"context"
"github.com/goharbor/harbor/src/jobservice/logger"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/systemartifact/dao"
"github.com/goharbor/harbor/src/pkg/systemartifact/model"
"time"
)
var (
DefaultCleanupWindowSeconds = 86400
)
// Selector provides an interface that can be implemented
// by consumers of the system artifact management framework to
// provide a custom clean-up criteria. This allows producers of the
// system artifact data to control the lifespan of the generated artifact
// records and data.
// Every system data artifact produces must register a cleanup criteria.
type Selector interface {
// List all system artifacts created greater than 24 hours.
List(ctx context.Context) ([]*model.SystemArtifact, error)
// ListWithFilters allows retrieval of system artifact records that match
// multiple filter and sort criteria that can be specified by the clients
ListWithFilters(ctx context.Context, query *q.Query) ([]*model.SystemArtifact, error)
}
var DefaultSelector = Default()
func Default() Selector {
return &defaultSelector{dao: dao.NewSystemArtifactDao()}
}
// defaultSelector is a default implementation of the Selector which select system artifacts
// older than 24 hours for clean-up
type defaultSelector struct {
dao dao.DAO
}
func (cleanupCriteria *defaultSelector) ListWithFilters(ctx context.Context, query *q.Query) ([]*model.SystemArtifact, error) {
return cleanupCriteria.dao.List(ctx, query)
}
func (cleanupCriteria *defaultSelector) List(ctx context.Context) ([]*model.SystemArtifact, error) {
currentTime := time.Now()
duration := time.Duration(DefaultCleanupWindowSeconds) * time.Second
timeRange := q.Range{Max: currentTime.Add(-duration).Format(time.RFC3339)}
logger.Debugf("Cleaning up system artifacts with range: %v", timeRange)
query := q.New(map[string]interface{}{"create_time": &timeRange})
return cleanupCriteria.dao.List(ctx, query)
}

View File

@ -0,0 +1,162 @@
package systemartifact
import (
"context"
common_dao "github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/systemartifact/dao"
"github.com/goharbor/harbor/src/pkg/systemartifact/model"
htesting "github.com/goharbor/harbor/src/testing"
"github.com/stretchr/testify/suite"
"testing"
"time"
)
type defaultCleanupCriteriaTestSuite struct {
htesting.Suite
dao dao.DAO
ctx context.Context
cleanupCriteria Selector
}
func (suite *defaultCleanupCriteriaTestSuite) SetupSuite() {
suite.Suite.SetupSuite()
suite.dao = dao.NewSystemArtifactDao()
suite.cleanupCriteria = DefaultSelector
common_dao.PrepareTestForPostgresSQL()
suite.ctx = orm.Context()
sa := model.SystemArtifact{}
suite.ClearTables = append(suite.ClearTables, sa.TableName())
}
func (suite *defaultCleanupCriteriaTestSuite) TestList() {
// insert a normal system artifact
currentTime := time.Now()
{
saNow := model.SystemArtifact{
Repository: "test_repo1000",
Digest: "test_digest1000",
Size: int64(100),
Vendor: "test_vendor1000",
Type: "test_repo_type",
CreateTime: currentTime,
ExtraAttrs: "",
}
oneDayAndElevenMinutesAgo := time.Duration(96500) * time.Second
sa1 := model.SystemArtifact{
Repository: "test_repo2000",
Digest: "test_digest2000",
Size: int64(100),
Vendor: "test_vendor2000",
Type: "test_repo_type",
CreateTime: currentTime.Add(-oneDayAndElevenMinutesAgo),
ExtraAttrs: "",
}
twoDaysAgo := time.Duration(172800) * time.Second
sa2 := model.SystemArtifact{
Repository: "test_repo3000",
Digest: "test_digest3000",
Size: int64(100),
Vendor: "test_vendor3000",
Type: "test_repo_type",
CreateTime: currentTime.Add(-twoDaysAgo),
ExtraAttrs: "",
}
id1, err := suite.dao.Create(suite.ctx, &saNow)
id2, err := suite.dao.Create(suite.ctx, &sa1)
id3, err := suite.dao.Create(suite.ctx, &sa2)
suite.NoError(err, "Unexpected error when inserting test record")
suite.NotEqual(0, id1, "Expected a valid record identifier but was 0")
suite.NotEqual(0, id2, "Expected a valid record identifier but was 0")
suite.NotEqual(0, id3, "Expected a valid record identifier but was 0")
actualSysArtifactIds := make(map[int64]bool)
sysArtifactList, err := suite.cleanupCriteria.List(suite.ctx)
for _, sysArtifact := range sysArtifactList {
actualSysArtifactIds[sysArtifact.ID] = true
}
expectedSysArtifactIds := map[int64]bool{id2: true, id3: true}
for k := range expectedSysArtifactIds {
_, ok := actualSysArtifactIds[k]
suite.Truef(ok, "Expected system artifact : %v not present in the list", k)
}
}
}
func (suite *defaultCleanupCriteriaTestSuite) TestListWithFilters() {
// insert a normal system artifact
currentTime := time.Now()
{
saNow := model.SystemArtifact{
Repository: "test_repo73000",
Digest: "test_digest73000",
Size: int64(100),
Vendor: "test_vendor73000",
Type: "test_repo_type",
CreateTime: currentTime,
ExtraAttrs: "",
}
oneDayAndElevenMinutesAgo := time.Duration(96500) * time.Second
sa1 := model.SystemArtifact{
Repository: "test_repo29000",
Digest: "test_digest29000",
Size: int64(100),
Vendor: "test_vendor29000",
Type: "test_repo_type",
CreateTime: currentTime.Add(-oneDayAndElevenMinutesAgo),
ExtraAttrs: "",
}
twoDaysAgo := time.Duration(172800) * time.Second
sa2 := model.SystemArtifact{
Repository: "test_repo37000",
Digest: "test_digest37000",
Size: int64(100),
Vendor: "test_vendor37000",
Type: "test_repo_type",
CreateTime: currentTime.Add(-twoDaysAgo),
ExtraAttrs: "",
}
id1, err := suite.dao.Create(suite.ctx, &saNow)
id2, err := suite.dao.Create(suite.ctx, &sa1)
id3, err := suite.dao.Create(suite.ctx, &sa2)
suite.NoError(err, "Unexpected error when inserting test record")
suite.NotEqual(0, id1, "Expected a valid record identifier but was 0")
suite.NotEqual(0, id2, "Expected a valid record identifier but was 0")
suite.NotEqual(0, id3, "Expected a valid record identifier but was 0")
actualSysArtifactIds := make(map[int64]bool)
query := q.Query{Keywords: map[string]interface{}{"vendor": "test_vendor37000", "repository": "test_repo37000"}}
sysArtifactList, err := suite.cleanupCriteria.ListWithFilters(suite.ctx, &query)
for _, sysArtifact := range sysArtifactList {
actualSysArtifactIds[sysArtifact.ID] = true
}
expectedSysArtifactIds := map[int64]bool{id3: true}
for k := range expectedSysArtifactIds {
_, ok := actualSysArtifactIds[k]
suite.Truef(ok, "Expected system artifact : %v not present in the list", k)
}
}
}
func TestCleanupCriteriaTestSuite(t *testing.T) {
suite.Run(t, &defaultCleanupCriteriaTestSuite{})
}

View File

@ -0,0 +1,128 @@
package dao
import (
"context"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/systemartifact/model"
)
const (
sizeQuery = "select sum(size) as total_size from system_artifact"
totalSizeColumn = "total_size"
)
// DAO defines an data access interface for manging the CRUD and read of system
// artifact tracking records
type DAO interface {
// Create a system artifact tracking record.
Create(ctx context.Context, systemArtifact *model.SystemArtifact) (int64, error)
// Get a system artifact tracking record identified by vendor, repository and digest
Get(ctx context.Context, vendor, repository, digest string) (*model.SystemArtifact, error)
// Delete a system artifact tracking record identified by vendor, repository and digest
Delete(ctx context.Context, vendor, repository, digest string) error
// List all the system artifact records that match the criteria specified
// within the query.
List(ctx context.Context, query *q.Query) ([]*model.SystemArtifact, error)
// Size returns the sum of all the system artifacts.
Size(ctx context.Context) (int64, error)
}
// NewSystemArtifactDao returns an instance of the system artifact dao layer
func NewSystemArtifactDao() DAO {
return &systemArtifactDAO{}
}
// The default implementation of the system artifact DAO.
type systemArtifactDAO struct{}
func (*systemArtifactDAO) Create(ctx context.Context, systemArtifact *model.SystemArtifact) (int64, error) {
ormer, err := orm.FromContext(ctx)
if err != nil {
return 0, err
}
id, err := ormer.Insert(systemArtifact)
if err != nil {
if e := orm.AsConflictError(err, "system artifact with repository name %s and digest %s already exists",
systemArtifact.Repository, systemArtifact.Digest); e != nil {
err = e
}
return int64(0), err
}
return id, nil
}
func (*systemArtifactDAO) Get(ctx context.Context, vendor, repository, digest string) (*model.SystemArtifact, error) {
ormer, err := orm.FromContext(ctx)
if err != nil {
return nil, err
}
sa := model.SystemArtifact{Repository: repository, Digest: digest, Vendor: vendor}
err = ormer.Read(&sa, "vendor", "repository", "digest")
if err != nil {
if e := orm.AsNotFoundError(err, "system artifact with repository name %s and digest %s not found",
repository, digest); e != nil {
err = e
}
return nil, err
}
return &sa, nil
}
func (*systemArtifactDAO) Delete(ctx context.Context, vendor, repository, digest string) error {
ormer, err := orm.FromContext(ctx)
if err != nil {
return err
}
sa := model.SystemArtifact{
Repository: repository,
Digest: digest,
Vendor: vendor,
}
_, err = ormer.Delete(&sa, "vendor", "repository", "digest")
return err
}
func (*systemArtifactDAO) List(ctx context.Context, query *q.Query) ([]*model.SystemArtifact, error) {
qs, err := orm.QuerySetter(ctx, &model.SystemArtifact{}, query)
if err != nil {
return nil, err
}
var systemArtifactRecords []*model.SystemArtifact
_, err = qs.All(&systemArtifactRecords)
if err != nil {
return nil, err
}
return systemArtifactRecords, nil
}
func (d *systemArtifactDAO) Size(ctx context.Context) (int64, error) {
ormer, err := orm.FromContext(ctx)
if err != nil {
return int64(0), err
}
var totalSize int64
if err := ormer.Raw(sizeQuery).QueryRow(&totalSize); err != nil {
return int64(0), err
}
return totalSize, nil
}

View File

@ -0,0 +1,319 @@
package dao
import (
"context"
common_dao "github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/systemartifact/model"
htesting "github.com/goharbor/harbor/src/testing"
"github.com/stretchr/testify/suite"
"testing"
"time"
)
type daoTestSuite struct {
htesting.Suite
dao DAO
ctx context.Context
id int64
}
func (suite *daoTestSuite) SetupSuite() {
suite.Suite.SetupSuite()
suite.dao = &systemArtifactDAO{}
common_dao.PrepareTestForPostgresSQL()
suite.ctx = orm.Context()
sa := model.SystemArtifact{}
suite.ClearTables = append(suite.ClearTables, sa.TableName())
}
func (suite *daoTestSuite) SetupTest() {
}
func (suite *daoTestSuite) TeardownTest() {
suite.ExecSQL("delete from system_artifact")
suite.TearDownSuite()
}
func (suite *daoTestSuite) TestCreate() {
suite.ExecSQL("delete from system_artifact")
// insert a normal system artifact
{
sa := model.SystemArtifact{
Repository: "test_repo",
Digest: "test_digest",
Size: int64(100),
Vendor: "test_vendor",
Type: "test_repo_type",
CreateTime: time.Now(),
ExtraAttrs: "",
}
id, err := suite.dao.Create(suite.ctx, &sa)
suite.NoError(err, "Unexpected error when inserting test record")
suite.NotEqual(0, id, "Expected a valid record identifier but was 0")
}
// attempt to create another system artifact with same data and then create a unique constraint violation error
{
sa := model.SystemArtifact{
Repository: "test_repo",
Digest: "test_digest",
Size: int64(100),
Vendor: "test_vendor",
Type: "test_repo_type",
CreateTime: time.Now(),
ExtraAttrs: "",
}
id, err := suite.dao.Create(suite.ctx, &sa)
suite.Equal(int64(0), id, "Expected id to be 0 owing to unique constraint violation")
suite.Error(err, "Expected error to be not nil")
errWithInfo := err.(*errors.Error)
suite.Equalf(errors.ConflictCode, errWithInfo.Code, "Expected conflict code but was %s", errWithInfo.Code)
}
}
func (suite *daoTestSuite) TestGet() {
// insert a normal system artifact and attempt to get it
{
sa := model.SystemArtifact{
Repository: "test_repo1",
Digest: "test_digest1",
Size: int64(100),
Vendor: "test_vendor",
Type: "test_repo_type",
CreateTime: time.Now(),
ExtraAttrs: "",
}
id, err := suite.dao.Create(suite.ctx, &sa)
suite.NoError(err, "Unexpected error when inserting test record")
suite.NotEqual(0, id, "Expected a valid record identifier but was 0")
saRead, err := suite.dao.Get(suite.ctx, "test_vendor", "test_repo1", "test_digest1")
suite.NoErrorf(err, "Unexpected error when reading system artifact: %v", err)
suite.Equalf(id, saRead.ID, "The ID for inserted system record %d is not equal to the read system record %d", id, saRead.ID)
}
// attempt to retrieve a non-existent system artifact record with incorrect repo name and correct digest
{
saRead, err := suite.dao.Get(suite.ctx, "test_vendor", "test_repo2", "test_digest1")
suite.Errorf(err, "Expected no record found error for provided repository and digest")
suite.Nil(saRead, "Expected system artifact record to be nil")
errWithInfo := err.(*errors.Error)
suite.Equalf(errors.NotFoundCode, errWithInfo.Code, "Expected not found code but was %s", errWithInfo.Code)
}
// attempt to retrieve a non-existent system artifact record with correct repo name and incorrect digest
{
saRead, err := suite.dao.Get(suite.ctx, "test_vendor", "test_repo1", "test_digest2")
suite.Errorf(err, "Expected no record found error for provided repository and digest")
suite.Nil(saRead, "Expected system artifact record to be nil")
errWithInfo := err.(*errors.Error)
suite.Equalf(errors.NotFoundCode, errWithInfo.Code, "Expected not found code but was %s", errWithInfo.Code)
}
// multiple system artifact records from different vendors.
// insert a normal system artifact and attempt to get it
{
sa_vendor1 := model.SystemArtifact{
Repository: "test_repo10",
Digest: "test_digest10",
Size: int64(100),
Vendor: "test_vendor10",
Type: "test_repo_type",
CreateTime: time.Now(),
ExtraAttrs: "",
}
sa_vendor2 := model.SystemArtifact{
Repository: "test_repo20",
Digest: "test_digest20",
Size: int64(100),
Vendor: "test_vendor20",
Type: "test_repo_type",
CreateTime: time.Now(),
ExtraAttrs: "",
}
idVendor1, err := suite.dao.Create(suite.ctx, &sa_vendor1)
idVendor2, err := suite.dao.Create(suite.ctx, &sa_vendor2)
suite.NoError(err, "Unexpected error when inserting test record")
suite.NotEqual(0, idVendor1, "Expected a valid record identifier but was 0")
suite.NotEqual(0, idVendor2, "Expected a valid record identifier but was 0")
saRead, err := suite.dao.Get(suite.ctx, "test_vendor10", "test_repo10", "test_digest10")
saRead2, err := suite.dao.Get(suite.ctx, "test_vendor20", "test_repo20", "test_digest20")
suite.NoErrorf(err, "Unexpected error when reading system artifact: %v", err)
suite.Equalf(idVendor1, saRead.ID, "The ID for inserted system record %d is not equal to the read system record %d", idVendor1, saRead.ID)
suite.Equalf(idVendor2, saRead2.ID, "The ID for inserted system record %d is not equal to the read system record %d", idVendor2, saRead2.ID)
}
}
func (suite *daoTestSuite) TestDelete() {
// insert a normal system artifact and attempt to get it
{
sa := model.SystemArtifact{
Repository: "test_repo3",
Digest: "test_digest3",
Size: int64(100),
Vendor: "test_vendor",
Type: "test_repo_type",
CreateTime: time.Now(),
ExtraAttrs: "",
}
id, err := suite.dao.Create(suite.ctx, &sa)
suite.NoError(err, "Unexpected error when inserting test record")
suite.NotEqual(0, id, "Expected a valid record identifier but was 0")
err = suite.dao.Delete(suite.ctx, "test_vendor", "test_repo3", "test_digest3")
suite.NoErrorf(err, "Unexpected error when reading system artifact: %v", err)
}
// attempt to delete a non-existent system artifact record with incorrect repo name and correct digest
{
err := suite.dao.Delete(suite.ctx, "test_vendor", "test_repo4", "test_digest3")
suite.NoErrorf(err, "Attempt to delete a non-existent system artifact should not fail")
}
// attempt to retrieve a non-existent system artifact record with correct repo name and incorrect digest
{
err := suite.dao.Delete(suite.ctx, "test_vendor", "test_repo3", "test_digest4")
suite.NoErrorf(err, "Attempt to delete a non-existent system artifact should not fail")
}
// multiple system artifact records from different vendors.
// insert a normal system artifact and attempt to get it
{
sa_vendor1 := model.SystemArtifact{
Repository: "test_repo200",
Digest: "test_digest200",
Size: int64(100),
Vendor: "test_vendor200",
Type: "test_repo_type",
CreateTime: time.Now(),
ExtraAttrs: "",
}
sa_vendor2 := model.SystemArtifact{
Repository: "test_repo300",
Digest: "test_digest300",
Size: int64(100),
Vendor: "test_vendor300",
Type: "test_repo_type",
CreateTime: time.Now(),
ExtraAttrs: "",
}
idVendor1, err := suite.dao.Create(suite.ctx, &sa_vendor1)
idVendor2, err := suite.dao.Create(suite.ctx, &sa_vendor2)
suite.NoError(err, "Unexpected error when inserting test record")
suite.NotEqual(0, idVendor1, "Expected a valid record identifier but was 0")
suite.NotEqual(0, idVendor2, "Expected a valid record identifier but was 0")
saRead, err := suite.dao.Get(suite.ctx, "test_vendor200", "test_repo200", "test_digest200")
saRead2, err := suite.dao.Get(suite.ctx, "test_vendor300", "test_repo300", "test_digest300")
suite.NoErrorf(err, "Unexpected error when reading system artifact: %v", err)
suite.Equalf(idVendor1, saRead.ID, "The ID for inserted system record %d is not equal to the read system record %d", idVendor1, saRead.ID)
suite.Equalf(idVendor2, saRead2.ID, "The ID for inserted system record %d is not equal to the read system record %d", idVendor2, saRead2.ID)
err = suite.dao.Delete(suite.ctx, "test_vendor200", "test_repo200", "test_digest200")
suite.NoErrorf(err, "Unexpected error when reading system artifact: %v", err)
saRead, err = suite.dao.Get(suite.ctx, "test_vendor200", "test_repo200", "test_digest200")
suite.Errorf(err, "Expected no record found error for provided repository and digest")
suite.Nil(saRead, "Expected system artifact record to be nil")
errWithInfo := err.(*errors.Error)
suite.Equalf(errors.NotFoundCode, errWithInfo.Code, "Expected not found code but was %s", errWithInfo.Code)
saRead3, err := suite.dao.Get(suite.ctx, "test_vendor300", "test_repo300", "test_digest300")
suite.Equalf(idVendor2, saRead2.ID, "The ID for inserted system record %d is not equal to the read system record %d", idVendor2, saRead3.ID)
}
}
func (suite *daoTestSuite) TestList() {
expectedSystemArtifactIds := make(map[int64]bool)
// insert a normal system artifact
{
sa := model.SystemArtifact{
Repository: "test_repo4",
Digest: "test_digest4",
Size: int64(100),
Vendor: "test_vendor",
Type: "test_repo_type",
CreateTime: time.Now(),
ExtraAttrs: "",
}
id, err := suite.dao.Create(suite.ctx, &sa)
suite.NoError(err, "Unexpected error when inserting test record")
suite.NotEqual(0, id, "Expected a valid record identifier but was 0")
expectedSystemArtifactIds[id] = true
}
// attempt to read all the system artifact records
{
query := q.Query{}
query.Keywords = map[string]interface{}{"repository": "test_repo4", "digest": "test_digest4"}
sysArtifacts, err := suite.dao.List(suite.ctx, &query)
suite.NotNilf(sysArtifacts, "Expected system artifacts list to be non-nil")
suite.NoErrorf(err, "Unexpected error when listing system artifact records : %v", err)
suite.Equalf(1, len(sysArtifacts), "Expected system artifacts list of size 1 but was: %d", len(sysArtifacts))
// iterate through the system artifact and validate that the ids are in the expected list of ids
for _, sysArtifact := range sysArtifacts {
_, ok := expectedSystemArtifactIds[sysArtifact.ID]
suite.Truef(ok, "Expected system artifact id %d to be present but was absent", sysArtifact.ID)
}
}
}
func (suite *daoTestSuite) TestSize() {
// insert a normal system artifact
{
sa := model.SystemArtifact{
Repository: "test_repo8",
Digest: "test_digest8",
Size: int64(100),
Vendor: "test_vendor",
Type: "test_repo_type",
CreateTime: time.Now(),
ExtraAttrs: "",
}
id, err := suite.dao.Create(suite.ctx, &sa)
suite.NoError(err, "Unexpected error when inserting test record")
suite.NotEqual(0, id, "Expected a valid record identifier but was 0")
sa1 := model.SystemArtifact{
Repository: "test_repo9",
Digest: "test_digest9",
Size: int64(500),
Vendor: "test_vendor",
Type: "test_repo_type",
CreateTime: time.Now(),
ExtraAttrs: "",
}
id, err = suite.dao.Create(suite.ctx, &sa1)
suite.NoError(err, "Unexpected error when inserting test record")
suite.NotEqual(0, id, "Expected a valid record identifier but was 0")
size, err := suite.dao.Size(suite.ctx)
suite.NoError(err, "Unexpected error when calculating record size")
suite.Truef(size > int64(0), "Expected size to be non-zero")
}
}
func TestDaoTestSuite(t *testing.T) {
suite.Run(t, &daoTestSuite{})
}

View File

@ -0,0 +1,253 @@
package systemartifact
import (
"context"
"fmt"
"github.com/goharbor/harbor/src/jobservice/logger"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/pkg/registry"
"github.com/goharbor/harbor/src/pkg/systemartifact/dao"
"github.com/goharbor/harbor/src/pkg/systemartifact/model"
"io"
"sync"
)
var (
Mgr = NewManager()
keyFormat = "%s:%s"
)
const repositoryFormat = "sys_harbor/%s/%s"
// Manager provides a low-level interface for harbor services
// to create registry artifacts containing arbitrary data but which
// are not standard OCI artifacts.
// By using this framework, harbor components can create artifacts for
// cross component data sharing. The framework abstracts out the book-keeping
// logic involved in managing and tracking system artifacts.
// The Manager ultimately relies on the harbor registry client to perform
// the BLOB related operations into the registry.
type Manager interface {
// Create a system artifact described by artifact record.
// The reader would be used to read from the underlying data artifact.
// Returns a system artifact tracking record id or any errors encountered in the data artifact upload process.
// Invoking this API would result in a repository being created with the specified name and digest within the registry.
Create(ctx context.Context, artifactRecord *model.SystemArtifact, reader io.Reader) (int64, error)
// Read a system artifact described by repository name and digest.
// The reader is responsible for closing the IO stream after the read completes.
Read(ctx context.Context, vendor string, repository string, digest string) (io.ReadCloser, error)
// Delete deletes a system artifact identified by a repository name and digest.
// Also deletes the tracking record from the underlying table.
Delete(ctx context.Context, vendor string, repository string, digest string) error
// Exists checks for the existence of a system artifact identified by repository and digest.
// A system artifact is considered as in existence if both the following conditions are true:
// 1. There is a system artifact tracking record within the Harbor DB
// 2. There is a BLOB corresponding to the repository name and digest obtained from system artifact record.
Exists(ctx context.Context, vendor string, repository string, digest string) (bool, error)
// GetStorageSize returns the total disk space used by the system artifacts stored in the registry.
GetStorageSize(ctx context.Context) (int64, error)
// RegisterCleanupCriteria a clean-up criteria for a specific vendor and artifact type combination.
RegisterCleanupCriteria(vendor string, artifactType string, criteria Selector)
// GetCleanupCriteria returns a clean-up criteria for a specific vendor and artifact type combination.
// if no clean-up criteria is found then the default clean-up criteria is returned
GetCleanupCriteria(vendor string, artifactType string) Selector
// Cleanup cleans up the system artifacts (tracking records as well as blobs) based on the
// artifact records selected by the Selector registered for each vendor type.
// Returns the total number of records deleted, the reclaimed size and any error (if encountered)
Cleanup(ctx context.Context) (int64, int64, error)
}
type systemArtifactManager struct {
regCli registry.Client
dao dao.DAO
defaultCleanupCriterion Selector
cleanupCriteria map[string]Selector
lock sync.Mutex
}
func NewManager() Manager {
sysArtifactMgr := &systemArtifactManager{
regCli: registry.Cli,
dao: dao.NewSystemArtifactDao(),
defaultCleanupCriterion: DefaultSelector,
cleanupCriteria: make(map[string]Selector),
}
return sysArtifactMgr
}
func (mgr *systemArtifactManager) Create(ctx context.Context, artifactRecord *model.SystemArtifact, reader io.Reader) (int64, error) {
var artifactId int64
// the entire create operation is executed within a transaction to ensure that any failures
// during the blob creation or tracking record creation result in a rollback of the transaction
createError := orm.WithTransaction(func(ctx context.Context) error {
id, err := mgr.dao.Create(ctx, artifactRecord)
if err != nil {
log.Errorf("Error creating system artifact record for %s/%s/%s: %v", artifactRecord.Vendor, artifactRecord.Repository, artifactRecord.Digest, err)
return err
}
repoName := mgr.getRepositoryName(artifactRecord.Vendor, artifactRecord.Repository)
err = mgr.regCli.PushBlob(repoName, artifactRecord.Digest, artifactRecord.Size, reader)
if err != nil {
return err
}
artifactId = id
return nil
})(ctx)
return artifactId, createError
}
func (mgr *systemArtifactManager) Read(ctx context.Context, vendor string, repository string, digest string) (io.ReadCloser, error) {
sa, err := mgr.dao.Get(ctx, vendor, repository, digest)
if err != nil {
return nil, err
}
repoName := mgr.getRepositoryName(vendor, repository)
_, readCloser, err := mgr.regCli.PullBlob(repoName, sa.Digest)
if err != nil {
return nil, err
}
return readCloser, nil
}
func (mgr *systemArtifactManager) Delete(ctx context.Context, vendor string, repository string, digest string) error {
repoName := mgr.getRepositoryName(vendor, repository)
if err := mgr.regCli.DeleteBlob(repoName, digest); err != nil {
log.Errorf("Error deleting system artifact BLOB : %s. Error: %v", repoName, err)
return err
}
return mgr.dao.Delete(ctx, vendor, repository, digest)
}
func (mgr *systemArtifactManager) Exists(ctx context.Context, vendor string, repository string, digest string) (bool, error) {
_, err := mgr.dao.Get(ctx, vendor, repository, digest)
if err != nil {
return false, err
}
repoName := mgr.getRepositoryName(vendor, repository)
exist, err := mgr.regCli.BlobExist(repoName, digest)
if err != nil {
return false, err
}
return exist, nil
}
func (mgr *systemArtifactManager) GetStorageSize(ctx context.Context) (int64, error) {
return mgr.dao.Size(ctx)
}
func (mgr *systemArtifactManager) RegisterCleanupCriteria(vendor string, artifactType string, criteria Selector) {
key := fmt.Sprintf(keyFormat, vendor, artifactType)
defer mgr.lock.Unlock()
mgr.lock.Lock()
mgr.cleanupCriteria[key] = criteria
}
func (mgr *systemArtifactManager) GetCleanupCriteria(vendor string, artifactType string) Selector {
key := fmt.Sprintf(keyFormat, vendor, artifactType)
defer mgr.lock.Unlock()
mgr.lock.Lock()
if criteria, ok := mgr.cleanupCriteria[key]; ok {
return criteria
}
return DefaultSelector
}
func (mgr *systemArtifactManager) Cleanup(ctx context.Context) (int64, int64, error) {
logger.Info("Starting system artifact cleanup")
// clean up artifact records having customized cleanup criteria first
totalReclaimedSize := int64(0)
totalRecordsDeleted := int64(0)
// get a copy of the registered cleanup criteria and
// iterate through this copy to invoke the cleanup
registeredCriteria := make(map[string]Selector, 0)
mgr.lock.Lock()
for key, val := range mgr.cleanupCriteria {
registeredCriteria[key] = val
}
mgr.lock.Unlock()
for key, val := range registeredCriteria {
logger.Infof("Executing cleanup for 'vendor:artifactType' : %s", key)
deleted, size, err := mgr.cleanup(ctx, val)
totalRecordsDeleted += deleted
totalReclaimedSize += size
if err != nil {
// one vendor error should not impact the clean-up of other vendor types. Hence the cleanup logic would continue
// after logging the error
logger.Errorf("Error when cleaning up system artifacts for 'vendor:artifactType':%s, %v", key, err)
}
}
logger.Info("Executing cleanup for default cleanup criteria")
// clean up artifact records using the default criteria
deleted, size, err := mgr.cleanup(ctx, mgr.defaultCleanupCriterion)
if err != nil {
// one vendor error should not impact the clean-up of other vendor types. Hence the cleanup logic would continue
// after logging the error
logger.Errorf("Error when cleaning up system artifacts for 'vendor:artifactType':%s, %v", "DefaultCriteria", err)
}
totalRecordsDeleted += deleted
totalReclaimedSize += size
return totalRecordsDeleted, totalReclaimedSize, nil
}
func (mgr *systemArtifactManager) cleanup(ctx context.Context, criteria Selector) (int64, int64, error) {
// clean up artifact records having customized cleanup criteria first
totalReclaimedSize := int64(0)
totalRecordsDeleted := int64(0)
isDefaultSelector := criteria == mgr.defaultCleanupCriterion
records, err := criteria.List(ctx)
if err != nil {
return totalRecordsDeleted, totalReclaimedSize, err
}
for _, record := range records {
// skip vendor artifact types with custom clean-up criteria registered
if isDefaultSelector && mgr.isSelectorRegistered(record.Vendor, record.Type) {
continue
}
err = mgr.Delete(ctx, record.Vendor, record.Repository, record.Digest)
if err != nil {
logger.Errorf("Error cleaning up artifact record for vendor: %s, repository: %s, digest: %s", record.Vendor, record.Repository, record.Digest)
return totalRecordsDeleted, totalReclaimedSize, err
}
totalReclaimedSize += record.Size
totalRecordsDeleted += 1
}
return totalRecordsDeleted, totalReclaimedSize, nil
}
func (mgr *systemArtifactManager) getRepositoryName(vendor string, repository string) string {
return fmt.Sprintf(repositoryFormat, vendor, repository)
}
func (mgr *systemArtifactManager) isSelectorRegistered(vendor, artifactType string) bool {
key := fmt.Sprintf(keyFormat, vendor, artifactType)
_, ok := mgr.cleanupCriteria[key]
return ok
}

View File

@ -0,0 +1,492 @@
package systemartifact
import (
"context"
"errors"
"fmt"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/systemartifact/model"
ormtesting "github.com/goharbor/harbor/src/testing/lib/orm"
"github.com/goharbor/harbor/src/testing/mock"
registrytesting "github.com/goharbor/harbor/src/testing/pkg/registry"
"github.com/goharbor/harbor/src/testing/pkg/systemartifact/cleanup"
sysartifactdaotesting "github.com/goharbor/harbor/src/testing/pkg/systemartifact/dao"
"github.com/stretchr/testify/suite"
"io/ioutil"
"os"
"strings"
"testing"
"time"
)
type ManagerTestSuite struct {
suite.Suite
regCli *registrytesting.FakeClient
dao *sysartifactdaotesting.DAO
mgr *systemArtifactManager
cleanupCriteria *cleanup.Selector
}
func (suite *ManagerTestSuite) SetupSuite() {
}
func (suite *ManagerTestSuite) SetupTest() {
suite.regCli = &registrytesting.FakeClient{}
suite.dao = &sysartifactdaotesting.DAO{}
suite.cleanupCriteria = &cleanup.Selector{}
suite.mgr = &systemArtifactManager{
regCli: suite.regCli,
dao: suite.dao,
defaultCleanupCriterion: suite.cleanupCriteria,
cleanupCriteria: make(map[string]Selector),
}
}
func (suite *ManagerTestSuite) TestCreate() {
sa := model.SystemArtifact{
Repository: "test_repo",
Digest: "test_digest",
Size: int64(100),
Vendor: "test_vendor",
Type: "test_type",
CreateTime: time.Now(),
ExtraAttrs: "",
}
suite.dao.On("Create", mock.Anything, &sa, mock.Anything).Return(int64(1), nil).Once()
suite.regCli.On("PushBlob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
reader := strings.NewReader("test data string")
id, err := suite.mgr.Create(orm.NewContext(nil, &ormtesting.FakeOrmer{}), &sa, reader)
suite.Equalf(int64(1), id, "Expected row to correctly inserted")
suite.NoErrorf(err, "Unexpected error when creating artifact: %v", err)
suite.regCli.AssertCalled(suite.T(), "PushBlob")
}
func (suite *ManagerTestSuite) TestCreatePushBlobFails() {
sa := model.SystemArtifact{
Repository: "test_repo",
Digest: "test_digest",
Size: int64(100),
Vendor: "test_vendor",
Type: "test_type",
CreateTime: time.Now(),
ExtraAttrs: "",
}
suite.dao.On("Create", mock.Anything, &sa, mock.Anything).Return(int64(1), nil).Once()
suite.dao.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
suite.regCli.On("PushBlob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("error")).Once()
reader := strings.NewReader("test data string")
id, err := suite.mgr.Create(orm.NewContext(nil, &ormtesting.FakeOrmer{}), &sa, reader)
suite.Equalf(int64(0), id, "Expected no row to be inserted")
suite.Errorf(err, "Expected error when creating artifact: %v", err)
suite.dao.AssertCalled(suite.T(), "Create", mock.Anything, &sa, mock.Anything)
suite.regCli.AssertCalled(suite.T(), "PushBlob")
}
func (suite *ManagerTestSuite) TestCreateArtifactRecordFailure() {
sa := model.SystemArtifact{
Repository: "test_repo",
Digest: "test_digest",
Size: int64(100),
Vendor: "test_vendor",
Type: "test_type",
CreateTime: time.Now(),
ExtraAttrs: "",
}
suite.dao.On("Create", mock.Anything, &sa, mock.Anything).Return(int64(0), errors.New("error")).Once()
suite.regCli.On("PushBlob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
suite.regCli.On("PushBlob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
suite.regCli.On("DeleteBlob", mock.Anything, mock.Anything).Return(nil).Once()
reader := strings.NewReader("test data string")
id, err := suite.mgr.Create(orm.NewContext(nil, &ormtesting.FakeOrmer{}), &sa, reader)
suite.Equalf(int64(0), id, "Expected no row to be inserted")
suite.Errorf(err, "Expected error when creating artifact: %v", err)
suite.dao.AssertCalled(suite.T(), "Create", mock.Anything, mock.Anything)
suite.regCli.AssertNotCalled(suite.T(), "PushBlob")
}
func (suite *ManagerTestSuite) TestRead() {
sa := model.SystemArtifact{
Repository: "test_repo",
Digest: "test_digest",
Size: int64(100),
Vendor: "test_vendor",
Type: "test_type",
CreateTime: time.Now(),
ExtraAttrs: "",
}
dummyRepoFilepath := fmt.Sprintf("/tmp/sys_art_test.dmp_%v", time.Now())
data := []byte("test data")
err := ioutil.WriteFile(dummyRepoFilepath, data, os.ModePerm)
suite.NoErrorf(err, "Unexpected error when creating test repo file: %v", dummyRepoFilepath)
repoHandle, err := os.Open(dummyRepoFilepath)
suite.NoErrorf(err, "Unexpected error when reading test repo file: %v", dummyRepoFilepath)
defer repoHandle.Close()
suite.dao.On("Get", mock.Anything, "test_vendor", "test_repo", "test_digest").Return(&sa, nil).Once()
suite.regCli.On("PullBlob", mock.Anything, mock.Anything).Return(len(data), repoHandle, nil).Once()
readCloser, err := suite.mgr.Read(context.TODO(), "test_vendor", "test_repo", "test_digest")
suite.NoErrorf(err, "Unexpected error when reading artifact: %v", err)
suite.dao.AssertCalled(suite.T(), "Get", mock.Anything, "test_vendor", "test_repo", "test_digest")
suite.regCli.AssertCalled(suite.T(), "PullBlob")
suite.NotNilf(readCloser, "Expected valid read closer instance but was nil")
}
func (suite *ManagerTestSuite) TestReadSystemArtifactRecordNotFound() {
dummyRepoFilepath := fmt.Sprintf("/tmp/sys_art_test.dmp_%v", time.Now())
data := []byte("test data")
err := ioutil.WriteFile(dummyRepoFilepath, data, os.ModePerm)
suite.NoErrorf(err, "Unexpected error when creating test repo file: %v", dummyRepoFilepath)
repoHandle, err := os.Open(dummyRepoFilepath)
suite.NoErrorf(err, "Unexpected error when reading test repo file: %v", dummyRepoFilepath)
defer repoHandle.Close()
errToRet := orm.ErrNoRows
suite.dao.On("Get", mock.Anything, "test_vendor", "test_repo", "test_digest").Return(nil, errToRet).Once()
suite.regCli.On("PullBlob", mock.Anything, mock.Anything).Return(len(data), repoHandle, nil).Once()
readCloser, err := suite.mgr.Read(context.TODO(), "test_vendor", "test_repo", "test_digest")
suite.Errorf(err, "Expected error when reading artifact: %v", errToRet)
suite.dao.AssertCalled(suite.T(), "Get", mock.Anything, "test_vendor", "test_repo", "test_digest")
suite.regCli.AssertNotCalled(suite.T(), "PullBlob")
suite.Nilf(readCloser, "Expected null read closer instance but was valid")
}
func (suite *ManagerTestSuite) TestDelete() {
suite.dao.On("Delete", mock.Anything, "test_vendor", "test_repo", "test_digest").Return(nil).Once()
suite.regCli.On("DeleteBlob", mock.Anything, mock.Anything).Return(nil).Once()
err := suite.mgr.Delete(context.TODO(), "test_vendor", "test_repo", "test_digest")
suite.NoErrorf(err, "Unexpected error when deleting artifact: %v", err)
suite.dao.AssertCalled(suite.T(), "Delete", mock.Anything, "test_vendor", "test_repo", "test_digest")
suite.regCli.AssertCalled(suite.T(), "DeleteBlob")
}
func (suite *ManagerTestSuite) TestDeleteSystemArtifactDeleteError() {
errToRet := orm.ErrNoRows
suite.dao.On("Delete", mock.Anything, "test_vendor", "test_repo", "test_digest").Return(errToRet).Once()
suite.regCli.On("DeleteBlob", mock.Anything, mock.Anything).Return(nil).Once()
err := suite.mgr.Delete(context.TODO(), "test_vendor", "test_repo", "test_digest")
suite.Errorf(err, "Expected error when deleting artifact: %v", err)
suite.dao.AssertCalled(suite.T(), "Delete", mock.Anything, "test_vendor", "test_repo", "test_digest")
suite.regCli.AssertCalled(suite.T(), "DeleteBlob")
}
func (suite *ManagerTestSuite) TestDeleteSystemArtifactBlobDeleteError() {
errToRet := orm.ErrNoRows
suite.dao.On("Delete", mock.Anything, "test_vendor", "test_repo", "test_digest").Return(nil).Once()
suite.regCli.On("DeleteBlob", mock.Anything, mock.Anything).Return(errToRet).Once()
err := suite.mgr.Delete(context.TODO(), "test_vendor", "test_repo", "test_digest")
suite.Errorf(err, "Expected error when deleting artifact: %v", err)
suite.dao.AssertNotCalled(suite.T(), "Delete", mock.Anything, "test_vendor", "test_repo", "test_digest")
suite.regCli.AssertCalled(suite.T(), "DeleteBlob")
}
func (suite *ManagerTestSuite) TestExist() {
sa := model.SystemArtifact{
Repository: "test_repo",
Digest: "test_digest",
Size: int64(100),
Vendor: "test_vendor",
Type: "test_type",
CreateTime: time.Now(),
ExtraAttrs: "",
}
suite.dao.On("Get", mock.Anything, "test_vendor", "test_repo", "test_digest").Return(&sa, nil).Once()
suite.regCli.On("BlobExist", mock.Anything, mock.Anything).Return(true, nil).Once()
exists, err := suite.mgr.Exists(context.TODO(), "test_vendor", "test_repo", "test_digest")
suite.NoErrorf(err, "Unexpected error when checking if artifact exists: %v", err)
suite.dao.AssertCalled(suite.T(), "Get", mock.Anything, "test_vendor", "test_repo", "test_digest")
suite.regCli.AssertCalled(suite.T(), "BlobExist")
suite.True(exists, "Expected exists to be true but was false")
}
func (suite *ManagerTestSuite) TestExistSystemArtifactRecordReadError() {
errToReturn := orm.ErrNoRows
suite.dao.On("Get", mock.Anything, "test_vendor", "test_repo", "test_digest").Return(nil, errToReturn).Once()
suite.regCli.On("BlobExist", mock.Anything, mock.Anything).Return(true, nil).Once()
exists, err := suite.mgr.Exists(context.TODO(), "test_vendor", "test_repo", "test_digest")
suite.Error(err, "Expected error when checking if artifact exists")
suite.dao.AssertCalled(suite.T(), "Get", mock.Anything, "test_vendor", "test_repo", "test_digest")
suite.regCli.AssertNotCalled(suite.T(), "BlobExist")
suite.False(exists, "Expected exists to be false but was true")
}
func (suite *ManagerTestSuite) TestExistSystemArtifactBlobReadError() {
sa := model.SystemArtifact{
Repository: "test_repo",
Digest: "test_digest",
Size: int64(100),
Vendor: "test_vendor",
Type: "test_type",
CreateTime: time.Now(),
ExtraAttrs: "",
}
suite.dao.On("Get", mock.Anything, "test_vendor", "test_repo", "test_digest").Return(&sa, nil).Once()
suite.regCli.On("BlobExist", mock.Anything, mock.Anything).Return(false, errors.New("test error")).Once()
exists, err := suite.mgr.Exists(context.TODO(), "test_vendor", "test_repo", "test_digest")
suite.Error(err, "Expected error when checking if artifact exists")
suite.dao.AssertCalled(suite.T(), "Get", mock.Anything, "test_vendor", "test_repo", "test_digest")
suite.regCli.AssertCalled(suite.T(), "BlobExist")
suite.False(exists, "Expected exists to be false but was true")
}
func (suite *ManagerTestSuite) TestGetStorageSize() {
suite.dao.On("Size", mock.Anything).Return(int64(400), nil).Once()
size, err := suite.mgr.GetStorageSize(context.TODO())
suite.NoErrorf(err, "Unexpected error encountered: %v", err)
suite.dao.AssertCalled(suite.T(), "Size", mock.Anything)
suite.Equalf(int64(400), size, "Expected size to be 400 but was : %v", size)
}
func (suite *ManagerTestSuite) TestGetStorageSizeError() {
suite.dao.On("Size", mock.Anything).Return(int64(0), errors.New("test error")).Once()
size, err := suite.mgr.GetStorageSize(context.TODO())
suite.Errorf(err, "Expected error encountered: %v", err)
suite.dao.AssertCalled(suite.T(), "Size", mock.Anything)
suite.Equalf(int64(0), size, "Expected size to be 0 but was : %v", size)
}
func (suite *ManagerTestSuite) TestCleanupCriteriaRegistration() {
vendor := "test_vendor"
artifactType := "test_artifact_type"
suite.mgr.RegisterCleanupCriteria(vendor, artifactType, suite)
criteria := suite.mgr.GetCleanupCriteria(vendor, artifactType)
suite.Equalf(suite, criteria, "Expected cleanup criteria to be the same as suite")
criteria = suite.mgr.GetCleanupCriteria("test_vendor1", "test_artifact1")
suite.Equalf(DefaultSelector, criteria, "Expected cleanup criteria to be the same as default cleanup criteria")
}
func (suite *ManagerTestSuite) TestCleanup() {
sa1 := model.SystemArtifact{
Repository: "test_repo1",
Digest: "test_digest1",
Size: int64(100),
Vendor: "test_vendor1",
Type: "test_type1",
CreateTime: time.Now(),
ExtraAttrs: "",
}
sa2 := model.SystemArtifact{
Repository: "test_repo2",
Digest: "test_digest2",
Size: int64(300),
Vendor: "test_vendor2",
Type: "test_type2",
CreateTime: time.Now(),
ExtraAttrs: "",
}
sa3 := model.SystemArtifact{
Repository: "test_repo3",
Digest: "test_digest3",
Size: int64(300),
Vendor: "test_vendor3",
Type: "test_type3",
CreateTime: time.Now(),
ExtraAttrs: "",
}
mockCleaupCriteria1 := cleanup.Selector{}
mockCleaupCriteria1.On("List", mock.Anything).Return([]*model.SystemArtifact{&sa1}, nil).Once()
mockCleaupCriteria2 := cleanup.Selector{}
mockCleaupCriteria2.On("List", mock.Anything).Return([]*model.SystemArtifact{&sa2}, nil).Once()
suite.cleanupCriteria.On("List", mock.Anything).Return([]*model.SystemArtifact{&sa3}, nil).Once()
suite.mgr.RegisterCleanupCriteria("test_vendor1", "test_type1", &mockCleaupCriteria1)
suite.mgr.RegisterCleanupCriteria("test_vendor2", "test_type2", &mockCleaupCriteria2)
suite.dao.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Times(3)
suite.regCli.On("DeleteBlob", mock.Anything, mock.Anything).Return(nil).Times(3)
totalDeleted, totalSizeReclaimed, err := suite.mgr.Cleanup(context.TODO())
suite.Equalf(int64(3), totalDeleted, "System artifacts delete; Expected:%d, Actual:%d", int64(3), totalDeleted)
suite.Equalf(int64(700), totalSizeReclaimed, "System artifacts delete; Expected:%d, Actual:%d", int64(700), totalDeleted)
suite.NoErrorf(err, "Unexpected error: %v", err)
}
func (suite *ManagerTestSuite) TestCleanupError() {
sa1 := model.SystemArtifact{
Repository: "test_repo13000",
Digest: "test_digest13000",
Size: int64(100),
Vendor: "test_vendor13000",
Type: "test_type13000",
CreateTime: time.Now(),
ExtraAttrs: "",
}
sa3 := model.SystemArtifact{
Repository: "test_repo33000",
Digest: "test_digest33000",
Size: int64(300),
Vendor: "test_vendor33000",
Type: "test_type33000",
CreateTime: time.Now(),
ExtraAttrs: "",
}
mockCleaupCriteria1 := cleanup.Selector{}
mockCleaupCriteria1.On("List", mock.Anything).Return([]*model.SystemArtifact{&sa1}, nil).Once()
mockCleaupCriteria2 := cleanup.Selector{}
mockCleaupCriteria2.On("List", mock.Anything).Return(nil, errors.New("test error")).Once()
suite.cleanupCriteria.On("List", mock.Anything).Return([]*model.SystemArtifact{&sa3}, nil)
suite.mgr.RegisterCleanupCriteria("test_vendor13000", "test_type13000", &mockCleaupCriteria1)
suite.mgr.RegisterCleanupCriteria("test_vendor23000", "test_type23000", &mockCleaupCriteria2)
suite.dao.On("Delete", mock.Anything, "test_vendor13000", "test_repo13000", "test_digest13000").Return(nil)
suite.dao.On("Delete", mock.Anything, "test_vendor33000", "test_repo33000", "test_digest33000").Return(nil)
suite.dao.On("Delete", mock.Anything, "test_vendor23000", "test_repo23000", mock.Anything).Return(nil)
suite.regCli.On("DeleteBlob", mock.Anything, mock.Anything).Return(nil)
totalDeleted, totalSizeReclaimed, err := suite.mgr.Cleanup(context.TODO())
suite.Equalf(int64(2), totalDeleted, "System artifacts delete; Expected:%d, Actual:%d", int64(2), totalDeleted)
suite.Equalf(int64(400), totalSizeReclaimed, "System artifacts delete; Expected:%d, Actual:%d", int64(400), totalDeleted)
suite.NoError(err, "Expected no error but was %v", err)
}
func (suite *ManagerTestSuite) TestCleanupErrorDefaultCriteria() {
sa1 := model.SystemArtifact{
Repository: "test_repo1",
Digest: "test_digest1",
Size: int64(100),
Vendor: "test_vendor1",
Type: "test_type1",
CreateTime: time.Now(),
ExtraAttrs: "",
}
sa2 := model.SystemArtifact{
Repository: "test_repo2",
Digest: "test_digest2",
Size: int64(300),
Vendor: "test_vendor2",
Type: "test_type2",
CreateTime: time.Now(),
ExtraAttrs: "",
}
mockCleaupCriteria1 := cleanup.Selector{}
mockCleaupCriteria1.On("List", mock.Anything).Return([]*model.SystemArtifact{&sa1}, nil).Once()
mockCleaupCriteria2 := cleanup.Selector{}
mockCleaupCriteria2.On("List", mock.Anything).Return([]*model.SystemArtifact{&sa2}, nil).Once()
suite.cleanupCriteria.On("List", mock.Anything).Return(nil, errors.New("test error"))
suite.mgr.RegisterCleanupCriteria("test_vendor1", "test_type1", &mockCleaupCriteria1)
suite.mgr.RegisterCleanupCriteria("test_vendor2", "test_type2", &mockCleaupCriteria2)
suite.dao.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
suite.regCli.On("DeleteBlob", mock.Anything, mock.Anything).Return(nil)
totalDeleted, totalSizeReclaimed, err := suite.mgr.Cleanup(context.TODO())
suite.Equalf(int64(2), totalDeleted, "System artifacts delete; Expected:%d, Actual:%d", int64(2), totalDeleted)
suite.Equalf(int64(400), totalSizeReclaimed, "System artifacts delete; Expected:%d, Actual:%d", int64(400), totalDeleted)
suite.NoErrorf(err, "Expected no error but was %v", err)
}
func (suite *ManagerTestSuite) TestCleanupErrorForVendor() {
sa1 := model.SystemArtifact{
Repository: "test_repo10000",
Digest: "test_digest10000",
Size: int64(100),
Vendor: "test_vendor10000",
Type: "test_type10000",
CreateTime: time.Now(),
ExtraAttrs: "",
}
sa2 := model.SystemArtifact{
Repository: "test_repo20000",
Digest: "test_digest20000",
Size: int64(300),
Vendor: "test_vendor10000",
Type: "test_type10000",
CreateTime: time.Now(),
ExtraAttrs: "",
}
sa3 := model.SystemArtifact{
Repository: "test_repo30000",
Digest: "test_digest30000",
Size: int64(300),
Vendor: "test_vendor30000",
Type: "test_type30000",
CreateTime: time.Now(),
ExtraAttrs: "",
}
mockCleaupCriteria1 := cleanup.Selector{}
mockCleaupCriteria1.On("List", mock.Anything).Return([]*model.SystemArtifact{&sa1, &sa2}, nil).Times(2)
suite.cleanupCriteria.On("List", mock.Anything).Return([]*model.SystemArtifact{&sa3}, nil).Times(2)
suite.mgr.RegisterCleanupCriteria("test_vendor10000", "test_type10000", &mockCleaupCriteria1)
suite.dao.On("Delete", mock.Anything, "test_vendor10000", "test_repo10000", "test_digest10000").Return(nil).Once()
suite.dao.On("Delete", mock.Anything, "test_vendor10000", "test_repo20000", "test_digest20000").Return(errors.New("test error")).Once()
suite.dao.On("Delete", mock.Anything, "test_vendor30000", "test_repo30000", "test_digest30000").Return(nil).Once()
suite.regCli.On("DeleteBlob", mock.Anything, mock.Anything).Return(nil).Times(3)
totalDeleted, totalSizeReclaimed, err := suite.mgr.Cleanup(context.TODO())
suite.Equalf(int64(2), totalDeleted, "System artifacts delete; Expected:%d, Actual:%d", int64(2), totalDeleted)
suite.Equalf(int64(400), totalSizeReclaimed, "System artifacts delete; Expected:%d, Actual:%d", int64(400), totalDeleted)
suite.NoErrorf(err, "Expected no error, but was %v", err)
}
func (suite *ManagerTestSuite) List(ctx context.Context) ([]*model.SystemArtifact, error) {
return make([]*model.SystemArtifact, 0), nil
}
func (suite *ManagerTestSuite) ListWithFilters(ctx context.Context, query *q.Query) ([]*model.SystemArtifact, error) {
return make([]*model.SystemArtifact, 0), nil
}
func TestManagerTestSuite(t *testing.T) {
mgr := &ManagerTestSuite{}
suite.Run(t, mgr)
}

View File

@ -0,0 +1,43 @@
package model
import (
"github.com/goharbor/harbor/src/lib/orm"
"time"
)
func init() {
orm.RegisterModel(
new(SystemArtifact),
)
}
// SystemArtifact represents a tracking record for each system artifact that has been
// created within registry using the system artifact manager
type SystemArtifact struct {
ID int64 `orm:"pk;auto;column(id)"`
// the name of repository associated with the artifact
Repository string `orm:"column(repository)"`
// the SHA-256 digest of the artifact data.
Digest string `orm:"column(digest)"`
// the size of the artifact data in bytes
Size int64 `orm:"column(size)"`
// the harbor subsystem that created the artifact
Vendor string `orm:"column(vendor)"`
// the type of the system artifact.
// the type field specifies the type of artifact data and is useful when a harbor component generates more than one
// kind of artifact. for e.g. a scan data export job could create a detailed CSV export data file as well
// as an summary export file. here type could be set to "CSVDetail" and "ScanSummary"
Type string `orm:"column(type)"`
// the time of creation of the system artifact
CreateTime time.Time `orm:"column(create_time)"`
// optional extra attributes for the system artifact
ExtraAttrs string `orm:"column(extra_attrs)"`
}
func (sa *SystemArtifact) TableName() string {
return "system_artifact"
}
func (sa *SystemArtifact) TableUnique() [][]string {
return [][]string{{"vendor", "repository_name", "digest"}}
}

View File

@ -56,3 +56,6 @@ package pkg
//go:generate mockery --case snake --dir ../../pkg/accessory/model --name Accessory --output ./accessory/model --outpkg model
//go:generate mockery --case snake --dir ../../pkg/accessory/dao --name DAO --output ./accessory/dao --outpkg dao
//go:generate mockery --case snake --dir ../../pkg/accessory --name Manager --output ./accessory --outpkg accessory
//go:generate mockery --case snake --dir ../../pkg/systemartifact --name Manager --output ./systemartifact --outpkg systemartifact
//go:generate mockery --case snake --dir ../../pkg/systemartifact/ --name Selector --output ./systemartifact/cleanup --outpkg cleanup
//go:generate mockery --case snake --dir ../../pkg/systemartifact/dao --name DAO --output ./systemartifact/dao --outpkg dao

View File

@ -0,0 +1,63 @@
// Code generated by mockery v2.1.0. DO NOT EDIT.
package cleanup
import (
context "context"
model "github.com/goharbor/harbor/src/pkg/systemartifact/model"
mock "github.com/stretchr/testify/mock"
q "github.com/goharbor/harbor/src/lib/q"
)
// Selector is an autogenerated mock type for the Selector type
type Selector struct {
mock.Mock
}
// List provides a mock function with given fields: ctx
func (_m *Selector) List(ctx context.Context) ([]*model.SystemArtifact, error) {
ret := _m.Called(ctx)
var r0 []*model.SystemArtifact
if rf, ok := ret.Get(0).(func(context.Context) []*model.SystemArtifact); ok {
r0 = rf(ctx)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.SystemArtifact)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ListWithFilters provides a mock function with given fields: ctx, query
func (_m *Selector) ListWithFilters(ctx context.Context, query *q.Query) ([]*model.SystemArtifact, error) {
ret := _m.Called(ctx, query)
var r0 []*model.SystemArtifact
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*model.SystemArtifact); ok {
r0 = rf(ctx, query)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.SystemArtifact)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
r1 = rf(ctx, query)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

View File

@ -0,0 +1,120 @@
// Code generated by mockery v2.1.0. DO NOT EDIT.
package dao
import (
context "context"
mock "github.com/stretchr/testify/mock"
model "github.com/goharbor/harbor/src/pkg/systemartifact/model"
q "github.com/goharbor/harbor/src/lib/q"
)
// DAO is an autogenerated mock type for the DAO type
type DAO struct {
mock.Mock
}
// Create provides a mock function with given fields: ctx, systemArtifact
func (_m *DAO) Create(ctx context.Context, systemArtifact *model.SystemArtifact) (int64, error) {
ret := _m.Called(ctx, systemArtifact)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context, *model.SystemArtifact) int64); ok {
r0 = rf(ctx, systemArtifact)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *model.SystemArtifact) error); ok {
r1 = rf(ctx, systemArtifact)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: ctx, vendor, repository, digest
func (_m *DAO) Delete(ctx context.Context, vendor string, repository string, digest string) error {
ret := _m.Called(ctx, vendor, repository, digest)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, string) error); ok {
r0 = rf(ctx, vendor, repository, digest)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: ctx, vendor, repository, digest
func (_m *DAO) Get(ctx context.Context, vendor string, repository string, digest string) (*model.SystemArtifact, error) {
ret := _m.Called(ctx, vendor, repository, digest)
var r0 *model.SystemArtifact
if rf, ok := ret.Get(0).(func(context.Context, string, string, string) *model.SystemArtifact); ok {
r0 = rf(ctx, vendor, repository, digest)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SystemArtifact)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok {
r1 = rf(ctx, vendor, repository, digest)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// List provides a mock function with given fields: ctx, query
func (_m *DAO) List(ctx context.Context, query *q.Query) ([]*model.SystemArtifact, error) {
ret := _m.Called(ctx, query)
var r0 []*model.SystemArtifact
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*model.SystemArtifact); ok {
r0 = rf(ctx, query)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.SystemArtifact)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
r1 = rf(ctx, query)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Size provides a mock function with given fields: ctx
func (_m *DAO) Size(ctx context.Context) (int64, error) {
ret := _m.Called(ctx)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context) int64); ok {
r0 = rf(ctx)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

View File

@ -0,0 +1,168 @@
// Code generated by mockery v2.1.0. DO NOT EDIT.
package systemartifact
import (
context "context"
io "io"
mock "github.com/stretchr/testify/mock"
model "github.com/goharbor/harbor/src/pkg/systemartifact/model"
systemartifact "github.com/goharbor/harbor/src/pkg/systemartifact"
)
// Manager is an autogenerated mock type for the Manager type
type Manager struct {
mock.Mock
}
// Cleanup provides a mock function with given fields: ctx
func (_m *Manager) Cleanup(ctx context.Context) (int64, int64, error) {
ret := _m.Called(ctx)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context) int64); ok {
r0 = rf(ctx)
} else {
r0 = ret.Get(0).(int64)
}
var r1 int64
if rf, ok := ret.Get(1).(func(context.Context) int64); ok {
r1 = rf(ctx)
} else {
r1 = ret.Get(1).(int64)
}
var r2 error
if rf, ok := ret.Get(2).(func(context.Context) error); ok {
r2 = rf(ctx)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// Create provides a mock function with given fields: ctx, artifactRecord, reader
func (_m *Manager) Create(ctx context.Context, artifactRecord *model.SystemArtifact, reader io.Reader) (int64, error) {
ret := _m.Called(ctx, artifactRecord, reader)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context, *model.SystemArtifact, io.Reader) int64); ok {
r0 = rf(ctx, artifactRecord, reader)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *model.SystemArtifact, io.Reader) error); ok {
r1 = rf(ctx, artifactRecord, reader)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: ctx, vendor, repository, digest
func (_m *Manager) Delete(ctx context.Context, vendor string, repository string, digest string) error {
ret := _m.Called(ctx, vendor, repository, digest)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, string) error); ok {
r0 = rf(ctx, vendor, repository, digest)
} else {
r0 = ret.Error(0)
}
return r0
}
// Exists provides a mock function with given fields: ctx, vendor, repository, digest
func (_m *Manager) Exists(ctx context.Context, vendor string, repository string, digest string) (bool, error) {
ret := _m.Called(ctx, vendor, repository, digest)
var r0 bool
if rf, ok := ret.Get(0).(func(context.Context, string, string, string) bool); ok {
r0 = rf(ctx, vendor, repository, digest)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok {
r1 = rf(ctx, vendor, repository, digest)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetCleanupCriteria provides a mock function with given fields: vendor, artifactType
func (_m *Manager) GetCleanupCriteria(vendor string, artifactType string) systemartifact.Selector {
ret := _m.Called(vendor, artifactType)
var r0 systemartifact.Selector
if rf, ok := ret.Get(0).(func(string, string) systemartifact.Selector); ok {
r0 = rf(vendor, artifactType)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(systemartifact.Selector)
}
}
return r0
}
// GetStorageSize provides a mock function with given fields: ctx
func (_m *Manager) GetStorageSize(ctx context.Context) (int64, error) {
ret := _m.Called(ctx)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context) int64); ok {
r0 = rf(ctx)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Read provides a mock function with given fields: ctx, vendor, repository, digest
func (_m *Manager) Read(ctx context.Context, vendor string, repository string, digest string) (io.ReadCloser, error) {
ret := _m.Called(ctx, vendor, repository, digest)
var r0 io.ReadCloser
if rf, ok := ret.Get(0).(func(context.Context, string, string, string) io.ReadCloser); ok {
r0 = rf(ctx, vendor, repository, digest)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(io.ReadCloser)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok {
r1 = rf(ctx, vendor, repository, digest)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RegisterCleanupCriteria provides a mock function with given fields: vendor, artifactType, criteria
func (_m *Manager) RegisterCleanupCriteria(vendor string, artifactType string, criteria systemartifact.Selector) {
_m.Called(vendor, artifactType, criteria)
}