From 27ec871185936b7b47e2c986caa6b7e11493586e Mon Sep 17 00:00:00 2001 From: prahaladdarkin Date: Mon, 9 May 2022 15:02:57 +0530 Subject: [PATCH] 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 --- .../postgresql/0090_2.6.0_schema.up.sql | 18 +- src/pkg/systemartifact/cleanupcriteria.go | 55 ++ .../systemartifact/cleanupcriteria_test.go | 162 ++++++ src/pkg/systemartifact/dao/dao.go | 128 +++++ src/pkg/systemartifact/dao/dao_test.go | 319 ++++++++++++ src/pkg/systemartifact/manager.go | 253 +++++++++ src/pkg/systemartifact/manager_test.go | 492 ++++++++++++++++++ src/pkg/systemartifact/model/model.go | 43 ++ src/testing/pkg/pkg.go | 3 + .../pkg/systemartifact/cleanup/selector.go | 63 +++ src/testing/pkg/systemartifact/dao/dao.go | 120 +++++ src/testing/pkg/systemartifact/manager.go | 168 ++++++ 12 files changed, 1823 insertions(+), 1 deletion(-) create mode 100644 src/pkg/systemartifact/cleanupcriteria.go create mode 100644 src/pkg/systemartifact/cleanupcriteria_test.go create mode 100644 src/pkg/systemartifact/dao/dao.go create mode 100644 src/pkg/systemartifact/dao/dao_test.go create mode 100644 src/pkg/systemartifact/manager.go create mode 100644 src/pkg/systemartifact/manager_test.go create mode 100644 src/pkg/systemartifact/model/model.go create mode 100644 src/testing/pkg/systemartifact/cleanup/selector.go create mode 100644 src/testing/pkg/systemartifact/dao/dao.go create mode 100644 src/testing/pkg/systemartifact/manager.go diff --git a/make/migrations/postgresql/0090_2.6.0_schema.up.sql b/make/migrations/postgresql/0090_2.6.0_schema.up.sql index f840de59d..075c9c2ee 100644 --- a/make/migrations/postgresql/0090_2.6.0_schema.up.sql +++ b/make/migrations/postgresql/0090_2.6.0_schema.up.sql @@ -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'); \ No newline at end of file +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") +); \ No newline at end of file diff --git a/src/pkg/systemartifact/cleanupcriteria.go b/src/pkg/systemartifact/cleanupcriteria.go new file mode 100644 index 000000000..612d7bf7a --- /dev/null +++ b/src/pkg/systemartifact/cleanupcriteria.go @@ -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) +} diff --git a/src/pkg/systemartifact/cleanupcriteria_test.go b/src/pkg/systemartifact/cleanupcriteria_test.go new file mode 100644 index 000000000..67097c78a --- /dev/null +++ b/src/pkg/systemartifact/cleanupcriteria_test.go @@ -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{}) +} diff --git a/src/pkg/systemartifact/dao/dao.go b/src/pkg/systemartifact/dao/dao.go new file mode 100644 index 000000000..f4b362b5a --- /dev/null +++ b/src/pkg/systemartifact/dao/dao.go @@ -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 +} diff --git a/src/pkg/systemartifact/dao/dao_test.go b/src/pkg/systemartifact/dao/dao_test.go new file mode 100644 index 000000000..fbdd4a8bc --- /dev/null +++ b/src/pkg/systemartifact/dao/dao_test.go @@ -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{}) +} diff --git a/src/pkg/systemartifact/manager.go b/src/pkg/systemartifact/manager.go new file mode 100644 index 000000000..85c0dc02e --- /dev/null +++ b/src/pkg/systemartifact/manager.go @@ -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 +} diff --git a/src/pkg/systemartifact/manager_test.go b/src/pkg/systemartifact/manager_test.go new file mode 100644 index 000000000..c511fd9ad --- /dev/null +++ b/src/pkg/systemartifact/manager_test.go @@ -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 = ®istrytesting.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) +} diff --git a/src/pkg/systemartifact/model/model.go b/src/pkg/systemartifact/model/model.go new file mode 100644 index 000000000..b56fb0083 --- /dev/null +++ b/src/pkg/systemartifact/model/model.go @@ -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"}} +} diff --git a/src/testing/pkg/pkg.go b/src/testing/pkg/pkg.go index 9568bea65..36c442606 100644 --- a/src/testing/pkg/pkg.go +++ b/src/testing/pkg/pkg.go @@ -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 diff --git a/src/testing/pkg/systemartifact/cleanup/selector.go b/src/testing/pkg/systemartifact/cleanup/selector.go new file mode 100644 index 000000000..5431c14e0 --- /dev/null +++ b/src/testing/pkg/systemartifact/cleanup/selector.go @@ -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 +} diff --git a/src/testing/pkg/systemartifact/dao/dao.go b/src/testing/pkg/systemartifact/dao/dao.go new file mode 100644 index 000000000..5587838b8 --- /dev/null +++ b/src/testing/pkg/systemartifact/dao/dao.go @@ -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 +} diff --git a/src/testing/pkg/systemartifact/manager.go b/src/testing/pkg/systemartifact/manager.go new file mode 100644 index 000000000..6256f275f --- /dev/null +++ b/src/testing/pkg/systemartifact/manager.go @@ -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) +}