From df1bdc1020a41677d764a9fcca9dd6aa3fd932c8 Mon Sep 17 00:00:00 2001 From: He Weiwei Date: Fri, 31 Jul 2020 17:55:35 +0000 Subject: [PATCH] refactor(project): add more methods to project controller and manager Signed-off-by: He Weiwei --- src/common/dao/base.go | 3 + src/common/dao/group/usergroup_test.go | 2 +- src/common/dao/project/projectmember_test.go | 7 +- src/common/models/member.go | 8 - src/common/models/pro_meta.go | 1 - src/common/models/project.go | 99 +++++- .../security/proxycachesecret/context.go | 13 +- .../security/proxycachesecret/context_test.go | 15 +- src/common/security/v2token/context.go | 17 +- src/common/security/v2token/context_test.go | 32 +- .../webhook/artifact/replication_test.go | 35 +- src/controller/project/controller.go | 125 ++++++-- src/controller/project/controller_test.go | 109 ++++++- src/controller/project/options.go | 8 + src/controller/quota/driver/project/util.go | 2 +- src/controller/repository/controller.go | 2 +- src/controller/repository/controller_test.go | 9 +- src/core/api/api_test.go | 2 +- src/core/api/project.go | 8 +- src/core/api/project_test.go | 7 +- src/core/auth/ldap/ldap_test.go | 2 +- src/lib/orm/orm.go | 5 + src/lib/orm/orm_test.go | 171 ++++++---- src/lib/orm/query.go | 288 ++++++++++++----- src/lib/orm/query_test.go | 126 +++++--- src/lib/q/query.go | 10 + src/lib/q/query_test.go | 41 +++ src/migration/artifact.go | 3 +- src/pkg/immutabletag/controller_test.go | 3 + src/pkg/project/dao/dao.go | 169 ++++++++++ src/pkg/project/dao/dao_test.go | 299 ++++++++++++++++++ src/pkg/project/dao/model.go | 43 +++ src/pkg/project/manager.go | 74 +++-- src/pkg/project/metadata/dao/dao.go | 104 ++++++ src/pkg/project/metadata/dao/dao_test.go | 70 ++++ src/pkg/project/metadata/manager.go | 128 ++++++++ src/pkg/project/metadata/models/metadata.go | 22 ++ src/pkg/project/models/project.go | 43 +++ src/pkg/retention/controller_test.go | 24 +- src/pkg/retention/launcher.go | 5 +- src/pkg/retention/launcher_test.go | 44 +-- src/pkg/user/dao/dao.go | 74 +++++ src/pkg/user/dao/dao_test.go | 46 +++ src/pkg/user/manager.go | 48 +++ src/pkg/user/models/user.go | 35 ++ src/server/middleware/repoproxy/proxy_test.go | 5 +- .../middleware/security/proxy_cache_secret.go | 2 +- src/testing/controller/project/controller.go | 58 ++++ src/testing/pkg/pkg.go | 4 + src/testing/pkg/project/manager.go | 115 +++++-- src/testing/pkg/project/metadata/manager.go | 118 +++++++ src/testing/pkg/scan/allowlist/manager.go | 101 ++++++ src/testing/pkg/user/manager.go | 40 +++ 53 files changed, 2434 insertions(+), 390 deletions(-) create mode 100644 src/lib/q/query_test.go create mode 100644 src/pkg/project/dao/dao.go create mode 100644 src/pkg/project/dao/dao_test.go create mode 100644 src/pkg/project/dao/model.go create mode 100644 src/pkg/project/metadata/dao/dao.go create mode 100644 src/pkg/project/metadata/dao/dao_test.go create mode 100644 src/pkg/project/metadata/manager.go create mode 100644 src/pkg/project/metadata/models/metadata.go create mode 100644 src/pkg/project/models/project.go create mode 100644 src/pkg/user/dao/dao.go create mode 100644 src/pkg/user/dao/dao_test.go create mode 100644 src/pkg/user/manager.go create mode 100644 src/pkg/user/models/user.go create mode 100644 src/testing/pkg/project/metadata/manager.go create mode 100644 src/testing/pkg/scan/allowlist/manager.go create mode 100644 src/testing/pkg/user/manager.go diff --git a/src/common/dao/base.go b/src/common/dao/base.go index 64212dd9b..cbb3b873f 100644 --- a/src/common/dao/base.go +++ b/src/common/dao/base.go @@ -120,6 +120,9 @@ func ClearTable(table string) error { if table == models.UserTable { sql = fmt.Sprintf("delete from %s where user_id > 2", table) } + if table == "project_member" { // make sure admin in library + sql = fmt.Sprintf("delete from %s where id > 1", table) + } if table == "project_metadata" { // make sure library is public sql = fmt.Sprintf("delete from %s where id > 1", table) } diff --git a/src/common/dao/group/usergroup_test.go b/src/common/dao/group/usergroup_test.go index 030c5654a..ee2ca1937 100644 --- a/src/common/dao/group/usergroup_test.go +++ b/src/common/dao/group/usergroup_test.go @@ -68,7 +68,7 @@ func TestMain(m *testing.M) { "delete from project where name='group_project_private'", "delete from harbor_user where username='member_test_01' or username='pm_sample' or username='grouptestu09'", "delete from user_group", - "delete from project_member", + "delete from project_member where id > 1", } dao.ExecuteBatchSQL(initSqls) defer dao.ExecuteBatchSQL(clearSqls) diff --git a/src/common/dao/project/projectmember_test.go b/src/common/dao/project/projectmember_test.go index 916198d2e..1d5649726 100644 --- a/src/common/dao/project/projectmember_test.go +++ b/src/common/dao/project/projectmember_test.go @@ -16,11 +16,12 @@ package project import ( "fmt" + "os" + "testing" + "github.com/goharbor/harbor/src/common/dao/group" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "os" - "testing" "github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common/dao" @@ -67,7 +68,7 @@ func TestMain(m *testing.M) { "delete from project where name='member_test_01' or name='member_test_02'", "delete from harbor_user where username='member_test_01' or username='member_test_02' or username='pm_sample'", "delete from user_group", - "delete from project_member", + "delete from project_member where id > 1", } dao.PrepareTestData(clearSqls, initSqls) cfg.Init() diff --git a/src/common/models/member.go b/src/common/models/member.go index da38e8f6c..4b6373b19 100644 --- a/src/common/models/member.go +++ b/src/common/models/member.go @@ -25,14 +25,6 @@ type Member struct { EntityType string `orm:"column(entity_type)" json:"entity_type"` } -// UserMember ... -type UserMember struct { - ID int `orm:"pk;column(user_id)" json:"user_id"` - Username string `json:"username"` - Rolename string `json:"role_name"` - Role int `json:"role_id"` -} - // MemberReq - Create Project Member Request type MemberReq struct { ProjectID int64 `json:"project_id"` diff --git a/src/common/models/pro_meta.go b/src/common/models/pro_meta.go index af00df3cb..0f3465546 100644 --- a/src/common/models/pro_meta.go +++ b/src/common/models/pro_meta.go @@ -36,5 +36,4 @@ type ProjectMetadata struct { Value string `orm:"column(value)" json:"value"` CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"` UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"` - Deleted bool `orm:"column(deleted)" json:"deleted"` } diff --git a/src/common/models/project.go b/src/common/models/project.go index 14fd511c2..daef9033e 100644 --- a/src/common/models/project.go +++ b/src/common/models/project.go @@ -15,9 +15,14 @@ package models import ( + "context" + "fmt" + "strconv" "strings" "time" + "github.com/astaxie/beego/orm" + "github.com/goharbor/harbor/src/lib/q" "github.com/goharbor/harbor/src/pkg/quota/types" "github.com/goharbor/harbor/src/replication/model" ) @@ -121,9 +126,67 @@ func (p *Project) AutoScan() bool { return isTrue(auto) } -func isTrue(value string) bool { - return strings.ToLower(value) == "true" || - strings.ToLower(value) == "1" +// FilterByPublic returns orm.QuerySeter with public filter +func (p *Project) FilterByPublic(ctx context.Context, qs orm.QuerySeter, key string, value interface{}) orm.QuerySeter { + subQuery := `SELECT project_id FROM project_metadata WHERE name = 'public' AND value = '%s'` + if isTrue(value) { + subQuery = fmt.Sprintf(subQuery, "true") + } else { + subQuery = fmt.Sprintf(subQuery, "false") + } + return qs.FilterRaw("project_id", fmt.Sprintf("IN (%s)", subQuery)) +} + +// FilterByOwner returns orm.QuerySeter with owner filter +func (p *Project) FilterByOwner(ctx context.Context, qs orm.QuerySeter, key string, value string) orm.QuerySeter { + return qs.FilterRaw("owner_id", fmt.Sprintf("IN (SELECT user_id FROM harbor_user WHERE username = '%s')", value)) +} + +// FilterByMember returns orm.QuerySeter with member filter +func (p *Project) FilterByMember(ctx context.Context, qs orm.QuerySeter, key string, value interface{}) orm.QuerySeter { + query, ok := value.(*MemberQuery) + if !ok { + return qs + } + subQuery := `SELECT project_id FROM project_member pm, harbor_user u WHERE pm.entity_id = u.user_id AND pm.entity_type = 'u' AND u.username = '%s'` + subQuery = fmt.Sprintf(subQuery, escape(query.Name)) + if query.Role > 0 { + subQuery = fmt.Sprintf("%s AND pm.role = %d", subQuery, query.Role) + } + + if len(query.GroupIDs) > 0 { + var elems []string + for _, groupID := range query.GroupIDs { + elems = append(elems, strconv.Itoa(groupID)) + } + + tpl := "(%s) UNION (SELECT project_id FROM project_member pm, user_group ug WHERE pm.entity_id = ug.id AND pm.entity_type = 'g' AND ug.id IN (%s))" + subQuery = fmt.Sprintf(tpl, subQuery, strings.TrimSpace(strings.Join(elems, ", "))) + } + + return qs.FilterRaw("project_id", fmt.Sprintf("IN (%s)", subQuery)) +} + +func isTrue(i interface{}) bool { + switch value := i.(type) { + case bool: + return value + case string: + v := strings.ToLower(value) + return v == "true" || v == "1" + default: + return false + } +} + +func escape(str string) string { + // TODO: remove this function when resolve the cycle import between lib/orm, common/dao and common/models. + // We need to move the function to registry the database from common/dao to lib/database, + // then lib/orm no need to depend the PrepareTestForPostgresSQL method in the common/dao + str = strings.Replace(str, `\`, `\\`, -1) + str = strings.Replace(str, `%`, `\%`, -1) + str = strings.Replace(str, `_`, `\_`, -1) + return str } // ProjectQueryParam can be used to set query parameters when listing projects. @@ -147,6 +210,36 @@ type ProjectQueryParam struct { ProjectIDs []int64 // project ID list } +// ToQuery returns q.Query from param +func (param *ProjectQueryParam) ToQuery() *q.Query { + kw := q.KeyWords{} + if param.Name != "" { + kw["name"] = q.FuzzyMatchValue{Value: param.Name} + } + if param.Owner != "" { + kw["owner"] = param.Owner + } + if param.Public != nil { + kw["public"] = *param.Public + } + if param.RegistryID != 0 { + kw["registry_id"] = param.RegistryID + } + if len(param.ProjectIDs) > 0 { + kw["project_id__in"] = param.ProjectIDs + } + if param.Member != nil { + kw["member"] = param.Member + } + + query := q.New(kw) + if param.Pagination != nil { + query.PageNumber = param.Pagination.Page + query.PageSize = param.Pagination.Size + } + return query +} + // MemberQuery filter by member's username and role type MemberQuery struct { Name string // the username of member diff --git a/src/common/security/proxycachesecret/context.go b/src/common/security/proxycachesecret/context.go index 6324d87f0..13a496f83 100644 --- a/src/common/security/proxycachesecret/context.go +++ b/src/common/security/proxycachesecret/context.go @@ -15,6 +15,9 @@ package proxycachesecret import ( + "context" + + "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/utils" "github.com/goharbor/harbor/src/lib/log" @@ -31,14 +34,16 @@ const ( // SecurityContext is the security context for proxy cache secret type SecurityContext struct { repository string - mgr project.Manager + getProject func(interface{}) (*models.Project, error) } // NewSecurityContext returns an instance of the proxy cache secret security context -func NewSecurityContext(repository string) *SecurityContext { +func NewSecurityContext(ctx context.Context, repository string) *SecurityContext { return &SecurityContext{ repository: repository, - mgr: project.Mgr, + getProject: func(i interface{}) (*models.Project, error) { + return project.Mgr.Get(ctx, i) + }, } } @@ -78,7 +83,7 @@ func (s *SecurityContext) Can(action types.Action, resource types.Resource) bool log.Debugf("got no namespace from the resource %s", resource) return false } - project, err := s.mgr.Get(namespace.Identity()) + project, err := s.getProject(namespace.Identity()) if err != nil { log.Errorf("failed to get project %v: %v", namespace.Identity(), err) return false diff --git a/src/common/security/proxycachesecret/context_test.go b/src/common/security/proxycachesecret/context_test.go index 182ba79ee..b8bd41ec1 100644 --- a/src/common/security/proxycachesecret/context_test.go +++ b/src/common/security/proxycachesecret/context_test.go @@ -15,6 +15,7 @@ package proxycachesecret import ( + "context" "testing" "github.com/goharbor/harbor/src/common/models" @@ -27,14 +28,16 @@ import ( type proxyCacheSecretTestSuite struct { suite.Suite sc *SecurityContext - mgr *project.FakeManager + mgr *project.Manager } func (p *proxyCacheSecretTestSuite) SetupTest() { - p.mgr = &project.FakeManager{} + p.mgr = &project.Manager{} p.sc = &SecurityContext{ repository: "library/hello-world", - mgr: p.mgr, + getProject: func(i interface{}) (*models.Project, error) { + return p.mgr.Get(context.TODO(), i) + }, } } @@ -72,7 +75,7 @@ func (p *proxyCacheSecretTestSuite) TestCan() { // the requested project not found action = rbac.ActionPull resource = rbac.NewProjectNamespace(2).Resource(rbac.ResourceRepository) - p.mgr.On("Get", mock.Anything).Return(nil, nil) + p.mgr.On("Get", mock.Anything, mock.Anything).Return(nil, nil) p.False(p.sc.Can(action, resource)) p.mgr.AssertExpectations(p.T()) @@ -82,7 +85,7 @@ func (p *proxyCacheSecretTestSuite) TestCan() { // pass for action pull action = rbac.ActionPull resource = rbac.NewProjectNamespace(1).Resource(rbac.ResourceRepository) - p.mgr.On("Get", mock.Anything).Return(&models.Project{ + p.mgr.On("Get", mock.Anything, mock.Anything).Return(&models.Project{ ProjectID: 1, Name: "library", }, nil) @@ -95,7 +98,7 @@ func (p *proxyCacheSecretTestSuite) TestCan() { // pass for action push action = rbac.ActionPush resource = rbac.NewProjectNamespace(1).Resource(rbac.ResourceRepository) - p.mgr.On("Get", mock.Anything).Return(&models.Project{ + p.mgr.On("Get", mock.Anything, mock.Anything).Return(&models.Project{ ProjectID: 1, Name: "library", }, nil) diff --git a/src/common/security/v2token/context.go b/src/common/security/v2token/context.go index 933ddac0f..d794adde3 100644 --- a/src/common/security/v2token/context.go +++ b/src/common/security/v2token/context.go @@ -5,22 +5,22 @@ import ( "strings" registry_token "github.com/docker/distribution/registry/auth/token" - "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/security" "github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/pkg/permission/types" "github.com/goharbor/harbor/src/pkg/project" + "github.com/goharbor/harbor/src/pkg/project/models" ) // tokenSecurityCtx is used for check permission of an internal signed token. // The intention for this guy is only for support CLI push/pull. It should not be used in other scenario without careful review // Each request should have a different instance of tokenSecurityCtx type tokenSecurityCtx struct { - logger *log.Logger - name string - accessMap map[string]map[types.Action]struct{} - pm project.Manager + logger *log.Logger + name string + accessMap map[string]map[types.Action]struct{} + getProject func(int64) (*models.Project, error) } func (t *tokenSecurityCtx) Name() string { @@ -65,7 +65,7 @@ func (t *tokenSecurityCtx) Can(action types.Action, resource types.Resource) boo t.logger.Warningf("Failed to get project id from namespace: %s", ns) return false } - p, err := t.pm.Get(pid) + p, err := t.getProject(pid) if err != nil { t.logger.Warningf("Failed to get project, id: %d, error: %v", pid, err) return false @@ -109,10 +109,13 @@ func New(ctx context.Context, name string, access []*registry_token.ResourceActi } m[l[0]] = actionMap } + return &tokenSecurityCtx{ logger: logger, name: name, accessMap: m, - pm: project.New(), + getProject: func(id int64) (*models.Project, error) { + return project.Mgr.Get(ctx, id) + }, } } diff --git a/src/common/security/v2token/context_test.go b/src/common/security/v2token/context_test.go index bd4ada488..881c71757 100644 --- a/src/common/security/v2token/context_test.go +++ b/src/common/security/v2token/context_test.go @@ -1,22 +1,38 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package v2token import ( + "context" "testing" "github.com/docker/distribution/registry/auth/token" - "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/pkg/permission/types" + "github.com/goharbor/harbor/src/pkg/project/models" "github.com/goharbor/harbor/src/testing/pkg/project" "github.com/stretchr/testify/assert" - "golang.org/x/net/context" ) func TestAll(t *testing.T) { - mgr := &project.FakeManager{} - mgr.On("Get", int64(1)).Return(&models.Project{ProjectID: 1, Name: "library"}, nil) - mgr.On("Get", int64(2)).Return(&models.Project{ProjectID: 2, Name: "test"}, nil) - mgr.On("Get", int64(3)).Return(&models.Project{ProjectID: 3, Name: "development"}, nil) + ctx := context.TODO() + + mgr := &project.Manager{} + mgr.On("Get", ctx, int64(1)).Return(&models.Project{ProjectID: 1, Name: "library"}, nil) + mgr.On("Get", ctx, int64(2)).Return(&models.Project{ProjectID: 2, Name: "test"}, nil) + mgr.On("Get", ctx, int64(3)).Return(&models.Project{ProjectID: 3, Name: "development"}, nil) access := []*token.ResourceActions{ { @@ -47,7 +63,9 @@ func TestAll(t *testing.T) { } sc := New(context.Background(), "jack", access) tsc := sc.(*tokenSecurityCtx) - tsc.pm = mgr + tsc.getProject = func(id int64) (*models.Project, error) { + return mgr.Get(ctx, id) + } cases := []struct { resource types.Resource diff --git a/src/controller/event/handler/webhook/artifact/replication_test.go b/src/controller/event/handler/webhook/artifact/replication_test.go index 9c2862199..8e9fcea06 100644 --- a/src/controller/event/handler/webhook/artifact/replication_test.go +++ b/src/controller/event/handler/webhook/artifact/replication_test.go @@ -1,7 +1,20 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package artifact import ( - "context" "testing" "time" @@ -15,6 +28,8 @@ import ( "github.com/goharbor/harbor/src/replication" daoModels "github.com/goharbor/harbor/src/replication/dao/models" "github.com/goharbor/harbor/src/replication/model" + projecttesting "github.com/goharbor/harbor/src/testing/controller/project" + "github.com/goharbor/harbor/src/testing/mock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -31,9 +46,6 @@ type fakedReplicationMgr struct { type fakedReplicationRegistryMgr struct { } -type fakedProjectCtl struct { -} - func (f *fakedNotificationPolicyMgr) Create(*models.NotificationPolicy) (int64, error) { return 0, nil } @@ -196,16 +208,6 @@ func (f *fakedReplicationRegistryMgr) HealthCheck() error { return nil } -func (f *fakedProjectCtl) Get(ctx context.Context, projectID int64, options ...project.Option) (*models.Project, error) { - return &models.Project{ProjectID: 1}, nil -} -func (f *fakedProjectCtl) GetByName(ctx context.Context, projectName string, options ...project.Option) (*models.Project, error) { - return &models.Project{ProjectID: 1}, nil -} -func (f *fakedProjectCtl) List(ctx context.Context, query *models.ProjectQueryParam, options ...project.Option) ([]*models.Project, error) { - return nil, nil -} - func TestReplicationHandler_Handle(t *testing.T) { common_dao.PrepareTestForPostgresSQL() config.Init() @@ -227,7 +229,10 @@ func TestReplicationHandler_Handle(t *testing.T) { replication.OperationCtl = &fakedReplicationMgr{} replication.PolicyCtl = &fakedReplicationPolicyMgr{} replication.RegistryMgr = &fakedReplicationRegistryMgr{} - project.Ctl = &fakedProjectCtl{} + projectCtl := &projecttesting.Controller{} + project.Ctl = projectCtl + + mock.OnAnything(projectCtl, "GetByName").Return(&models.Project{ProjectID: 1}, nil) handler := &ReplicationHandler{} diff --git a/src/controller/project/controller.go b/src/controller/project/controller.go index bb63f4c84..8997b1c9d 100644 --- a/src/controller/project/controller.go +++ b/src/controller/project/controller.go @@ -17,12 +17,15 @@ package project import ( "context" - "github.com/goharbor/harbor/src/common/models" - "github.com/goharbor/harbor/src/core/promgr/metamgr" "github.com/goharbor/harbor/src/lib/errors" "github.com/goharbor/harbor/src/lib/log" + "github.com/goharbor/harbor/src/lib/orm" + "github.com/goharbor/harbor/src/lib/q" "github.com/goharbor/harbor/src/pkg/project" + "github.com/goharbor/harbor/src/pkg/project/metadata" + "github.com/goharbor/harbor/src/pkg/project/models" "github.com/goharbor/harbor/src/pkg/scan/allowlist" + "github.com/goharbor/harbor/src/pkg/user" ) var ( @@ -32,6 +35,12 @@ var ( // Controller defines the operations related with blobs type Controller interface { + // Create create project instance + Create(ctx context.Context, project *models.Project) (int64, error) + // Count returns the total count of projects according to the query + Count(ctx context.Context, query *q.Query) (int64, error) + // Delete delete the project by project id + Delete(ctx context.Context, id int64) error // Get get the project by project id Get(ctx context.Context, projectID int64, options ...Option) (*models.Project, error) // GetByName get the project by project name @@ -44,27 +53,69 @@ type Controller interface { func NewController() Controller { return &controller{ projectMgr: project.Mgr, - metaMgr: metamgr.NewDefaultProjectMetadataManager(), + metaMgr: metadata.Mgr, allowlistMgr: allowlist.NewDefaultManager(), + userMgr: user.Mgr, } } type controller struct { projectMgr project.Manager - metaMgr metamgr.ProjectMetadataManager + metaMgr metadata.Manager allowlistMgr allowlist.Manager + userMgr user.Manager +} + +func (c *controller) Create(ctx context.Context, project *models.Project) (int64, error) { + var projectID int64 + h := func(ctx context.Context) (err error) { + projectID, err = c.projectMgr.Create(ctx, project) + if err != nil { + return err + } + + if err := c.allowlistMgr.CreateEmpty(projectID); err != nil { + log.Errorf("failed to create CVE allowlist for project %s: %v", project.Name, err) + return err + } + + if len(project.Metadata) > 0 { + if err = c.metaMgr.Add(ctx, projectID, project.Metadata); err != nil { + log.Errorf("failed to add metadata for project %s: %v", project.Name, err) + return err + } + } + return nil + } + + if err := orm.WithTransaction(h)(ctx); err != nil { + return 0, err + } + + return projectID, nil +} + +func (c *controller) Count(ctx context.Context, query *q.Query) (int64, error) { + return c.projectMgr.Count(ctx, query) +} + +func (c *controller) Delete(ctx context.Context, id int64) error { + return c.projectMgr.Delete(ctx, id) } func (c *controller) Get(ctx context.Context, projectID int64, options ...Option) (*models.Project, error) { - p, err := c.projectMgr.Get(projectID) + p, err := c.projectMgr.Get(ctx, projectID) if err != nil { return nil, err } - if p == nil { - return nil, errors.NotFoundError(nil).WithMessage("project %d not found", projectID) - } - return c.assembleProject(ctx, p, newOptions(options...)) + opts := newOptions(options...) + if opts.WithOwner { + if err := c.loadOwners(ctx, models.Projects{p}); err != nil { + return nil, err + } + } + return c.assembleProject(ctx, p, opts) } func (c *controller) GetByName(ctx context.Context, projectName string, options ...Option) (*models.Project, error) { @@ -72,24 +123,33 @@ func (c *controller) GetByName(ctx context.Context, projectName string, options return nil, errors.BadRequestError(nil).WithMessage("project name required") } - p, err := c.projectMgr.Get(projectName) - if err != nil { - return nil, err - } - if p == nil { - return nil, errors.NotFoundError(nil).WithMessage("project %s not found", projectName) - } - - return c.assembleProject(ctx, p, newOptions(options...)) -} - -func (c *controller) List(ctx context.Context, query *models.ProjectQueryParam, options ...Option) ([]*models.Project, error) { - projects, err := c.projectMgr.List(query) + p, err := c.projectMgr.Get(ctx, projectName) if err != nil { return nil, err } opts := newOptions(options...) + if opts.WithOwner { + if err := c.loadOwners(ctx, models.Projects{p}); err != nil { + return nil, err + } + } + return c.assembleProject(ctx, p, newOptions(options...)) +} + +func (c *controller) List(ctx context.Context, query *models.ProjectQueryParam, options ...Option) ([]*models.Project, error) { + projects, err := c.projectMgr.List(ctx, query) + if err != nil { + return nil, err + } + + opts := newOptions(options...) + if opts.WithOwner { + if err := c.loadOwners(ctx, projects); err != nil { + return nil, err + } + } + for _, p := range projects { if _, err := c.assembleProject(ctx, p, opts); err != nil { return nil, err @@ -99,9 +159,28 @@ func (c *controller) List(ctx context.Context, query *models.ProjectQueryParam, return projects, nil } +func (c *controller) loadOwners(ctx context.Context, projects models.Projects) error { + owners, err := c.userMgr.List(ctx, q.New(q.KeyWords{"user_id__in": projects.OwnerIDs()})) + if err != nil { + return err + } + m := owners.MapByUserID() + for _, p := range projects { + owner, ok := m[p.OwnerID] + if !ok { + log.G(ctx).Warningf("the owner of project %s is not found, owner id is %d", p.Name, p.OwnerID) + continue + } + + p.OwnerName = owner.Username + } + + return nil +} + func (c *controller) assembleProject(ctx context.Context, p *models.Project, opts *Options) (*models.Project, error) { if opts.Metadata { - meta, err := c.metaMgr.Get(p.ProjectID) + meta, err := c.metaMgr.Get(ctx, p.ProjectID) if err != nil { return nil, err } diff --git a/src/controller/project/controller_test.go b/src/controller/project/controller_test.go index 58f28ae41..4415f9bf1 100644 --- a/src/controller/project/controller_test.go +++ b/src/controller/project/controller_test.go @@ -19,9 +19,17 @@ import ( "fmt" "testing" - "github.com/goharbor/harbor/src/common/models" + commonmodels "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/lib/errors" + "github.com/goharbor/harbor/src/lib/orm" + "github.com/goharbor/harbor/src/pkg/project/models" + usermodels "github.com/goharbor/harbor/src/pkg/user/models" + ormtesting "github.com/goharbor/harbor/src/testing/lib/orm" + "github.com/goharbor/harbor/src/testing/mock" "github.com/goharbor/harbor/src/testing/pkg/project" + "github.com/goharbor/harbor/src/testing/pkg/project/metadata" + "github.com/goharbor/harbor/src/testing/pkg/scan/allowlist" + "github.com/goharbor/harbor/src/testing/pkg/user" "github.com/stretchr/testify/suite" ) @@ -29,34 +37,115 @@ type ControllerTestSuite struct { suite.Suite } -func (suite *ControllerTestSuite) TestGetByName() { - mgr := &project.FakeManager{} - mgr.On("Get", "library").Return(&models.Project{ProjectID: 1, Name: "library"}, nil) - mgr.On("Get", "test").Return(nil, nil) - mgr.On("Get", "oops").Return(nil, fmt.Errorf("oops")) +func (suite *ControllerTestSuite) TestCreate() { + ctx := orm.NewContext(context.TODO(), &ormtesting.FakeOrmer{}) + mgr := &project.Manager{} - c := controller{projectMgr: mgr} + allowlistMgr := &allowlist.Manager{} + allowlistMgr.On("CreateEmpty", mock.Anything).Return(nil) + + metadataMgr := &metadata.Manager{} + + c := controller{projectMgr: mgr, allowlistMgr: allowlistMgr, metaMgr: metadataMgr} { - p, err := c.GetByName(context.TODO(), "library", Metadata(false)) + metadataMgr.On("Add", ctx, mock.Anything, mock.Anything).Return(nil).Once() + mgr.On("Create", ctx, mock.Anything).Return(int64(2), nil).Once() + projectID, err := c.Create(ctx, &models.Project{OwnerID: 1, Metadata: map[string]string{"public": "true"}}) + suite.Nil(err) + suite.Equal(int64(2), projectID) + } + + { + metadataMgr.On("Add", ctx, mock.Anything, mock.Anything).Return(fmt.Errorf("oops")).Once() + mgr.On("Create", ctx, mock.Anything).Return(int64(2), nil).Once() + projectID, err := c.Create(ctx, &models.Project{OwnerID: 1, Metadata: map[string]string{"public": "true"}}) + suite.Error(err) + suite.Equal(int64(0), projectID) + } +} + +func (suite *ControllerTestSuite) TestGetByName() { + ctx := context.TODO() + + mgr := &project.Manager{} + mgr.On("Get", ctx, "library").Return(&models.Project{ProjectID: 1, Name: "library"}, nil) + mgr.On("Get", ctx, "test").Return(nil, errors.NotFoundError(nil)) + mgr.On("Get", ctx, "oops").Return(nil, fmt.Errorf("oops")) + + allowlistMgr := &allowlist.Manager{} + + metadataMgr := &metadata.Manager{} + metadataMgr.On("Get", ctx, mock.Anything).Return(map[string]string{"public": "true"}, nil) + + c := controller{projectMgr: mgr, allowlistMgr: allowlistMgr, metaMgr: metadataMgr} + + { + p, err := c.GetByName(ctx, "library") suite.Nil(err) suite.Equal("library", p.Name) suite.Equal(int64(1), p.ProjectID) } { - p, err := c.GetByName(context.TODO(), "test", Metadata(false)) + p, err := c.GetByName(ctx, "test") suite.Error(err) suite.True(errors.IsNotFoundErr(err)) suite.Nil(p) } { - p, err := c.GetByName(context.TODO(), "oops", Metadata(false)) + p, err := c.GetByName(ctx, "oops") suite.Error(err) suite.False(errors.IsNotFoundErr(err)) suite.Nil(p) } + + { + allowlistMgr.On("GetSys").Return(&commonmodels.CVEAllowlist{}, nil) + p, err := c.GetByName(ctx, "library", CVEAllowlist(true)) + suite.Nil(err) + suite.Equal("library", p.Name) + suite.Equal(p.ProjectID, p.CVEAllowlist.ProjectID) + } +} + +func (suite *ControllerTestSuite) TestWithOwner() { + ctx := context.TODO() + + mgr := &project.Manager{} + mgr.On("Get", ctx, int64(1)).Return(&models.Project{ProjectID: 1, OwnerID: 1, Name: "library"}, nil) + mgr.On("Get", ctx, "library").Return(&models.Project{ProjectID: 1, OwnerID: 1, Name: "library"}, nil) + mgr.On("List", ctx, mock.Anything).Return([]*models.Project{ + {ProjectID: 1, OwnerID: 1, Name: "library"}, + }, nil) + + userMgr := &user.Manager{} + userMgr.On("List", ctx, mock.Anything).Return(usermodels.Users{ + &usermodels.User{UserID: 1, Username: "admin"}, + }, nil) + + c := controller{projectMgr: mgr, userMgr: userMgr} + + { + project, err := c.Get(ctx, int64(1), Metadata(false), WithOwner()) + suite.Nil(err) + suite.Equal("admin", project.OwnerName) + } + + { + project, err := c.GetByName(ctx, "library", Metadata(false), WithOwner()) + suite.Nil(err) + suite.Equal("admin", project.OwnerName) + } + + { + param := &models.ProjectQueryParam{ProjectIDs: []int64{1}} + projects, err := c.List(ctx, param, Metadata(false), WithOwner()) + suite.Nil(err) + suite.Len(projects, 1) + suite.Equal("admin", projects[0].OwnerName) + } } func TestControllerTestSuite(t *testing.T) { diff --git a/src/controller/project/options.go b/src/controller/project/options.go index 0dce02338..11902f951 100644 --- a/src/controller/project/options.go +++ b/src/controller/project/options.go @@ -21,6 +21,7 @@ type Option func(*Options) type Options struct { CVEAllowlist bool // get project with cve allowlist Metadata bool // get project with metadata + WithOwner bool } // CVEAllowlist set CVEAllowlist for the Options @@ -37,6 +38,13 @@ func Metadata(metadata bool) Option { } } +// WithOwner set WithOwner for the Options +func WithOwner() Option { + return func(opts *Options) { + opts.WithOwner = true + } +} + func newOptions(options ...Option) *Options { opts := &Options{ Metadata: true, // default get project with metadata diff --git a/src/controller/quota/driver/project/util.go b/src/controller/quota/driver/project/util.go index 7b25e73c3..1741508f1 100644 --- a/src/controller/quota/driver/project/util.go +++ b/src/controller/quota/driver/project/util.go @@ -43,7 +43,7 @@ func getProjectsBatchFn(ctx context.Context, keys dataloader.Keys) []*dataloader projectIDs = append(projectIDs, id) } - projects, err := project.Mgr.List(&models.ProjectQueryParam{ProjectIDs: projectIDs}) + projects, err := project.Mgr.List(ctx, &models.ProjectQueryParam{ProjectIDs: projectIDs}) if err != nil { return handleError(err) } diff --git a/src/controller/repository/controller.go b/src/controller/repository/controller.go index dfac08063..6dbbc4bb2 100644 --- a/src/controller/repository/controller.go +++ b/src/controller/repository/controller.go @@ -87,7 +87,7 @@ func (c *controller) Ensure(ctx context.Context, name string) (bool, int64, erro // the repository doesn't exist, create it first projectName, _ := utils.ParseRepository(name) - project, err := c.proMgr.Get(projectName) + project, err := c.proMgr.Get(ctx, projectName) if err != nil { return false, 0, err } diff --git a/src/controller/repository/controller_test.go b/src/controller/repository/controller_test.go index 0c323a91e..699717baa 100644 --- a/src/controller/repository/controller_test.go +++ b/src/controller/repository/controller_test.go @@ -15,6 +15,8 @@ package repository import ( + "testing" + "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/controller/artifact" "github.com/goharbor/harbor/src/lib/errors" @@ -26,20 +28,19 @@ import ( "github.com/goharbor/harbor/src/testing/pkg/project" "github.com/goharbor/harbor/src/testing/pkg/repository" "github.com/stretchr/testify/suite" - "testing" ) type controllerTestSuite struct { suite.Suite ctl *controller - proMgr *project.FakeManager + proMgr *project.Manager repoMgr *repository.FakeManager argMgr *arttesting.FakeManager artCtl *artifacttesting.Controller } func (c *controllerTestSuite) SetupTest() { - c.proMgr = &project.FakeManager{} + c.proMgr = &project.Manager{} c.repoMgr = &repository.FakeManager{} c.argMgr = &arttesting.FakeManager{} c.artCtl = &artifacttesting.Controller{} @@ -69,7 +70,7 @@ func (c *controllerTestSuite) TestEnsure() { // doesn't exist c.repoMgr.On("GetByName").Return(nil, errors.NotFoundError(nil)) - c.proMgr.On("Get", "library").Return(&models.Project{ + c.proMgr.On("Get", mock.AnythingOfType("*context.valueCtx"), "library").Return(&models.Project{ ProjectID: 1, }, nil) c.repoMgr.On("Create").Return(1, nil) diff --git a/src/core/api/api_test.go b/src/core/api/api_test.go index 2080389cf..72279e62b 100644 --- a/src/core/api/api_test.go +++ b/src/core/api/api_test.go @@ -220,7 +220,7 @@ func TestMain(m *testing.M) { "delete from harbor_label", "delete from robot", "delete from user_group", - "delete from project_member", + "delete from project_member where id > 1", }) ret := m.Run() diff --git a/src/core/api/project.go b/src/core/api/project.go index 83a6ceddf..86970cbbc 100644 --- a/src/core/api/project.go +++ b/src/core/api/project.go @@ -17,7 +17,6 @@ package api import ( "context" "fmt" - "github.com/goharbor/harbor/src/pkg/retention/policy" "net/http" "regexp" "strconv" @@ -33,12 +32,14 @@ import ( "github.com/goharbor/harbor/src/common/utils" errutil "github.com/goharbor/harbor/src/common/utils/error" "github.com/goharbor/harbor/src/controller/event/metadata" + "github.com/goharbor/harbor/src/controller/project" "github.com/goharbor/harbor/src/controller/quota" "github.com/goharbor/harbor/src/core/config" "github.com/goharbor/harbor/src/lib/errors" "github.com/goharbor/harbor/src/lib/log" evt "github.com/goharbor/harbor/src/pkg/notifier/event" "github.com/goharbor/harbor/src/pkg/quota/types" + "github.com/goharbor/harbor/src/pkg/retention/policy" "github.com/goharbor/harbor/src/pkg/scan/vuln" "github.com/goharbor/harbor/src/replication" ) @@ -331,12 +332,13 @@ func (p *ProjectAPI) Delete() { return } - if err = p.ProjectMgr.Delete(p.project.ProjectID); err != nil { + ctx := p.Ctx.Request.Context() + + if err := project.Ctl.Delete(ctx, p.project.ProjectID); err != nil { p.ParseAndHandleError(fmt.Sprintf("failed to delete project %d", p.project.ProjectID), err) return } - ctx := p.Ctx.Request.Context() referenceID := quota.ReferenceID(p.project.ProjectID) q, err := quota.Ctl.GetByRef(ctx, quota.ProjectReference, referenceID) if err != nil { diff --git a/src/core/api/project_test.go b/src/core/api/project_test.go index 354c536cf..17ce38f7a 100644 --- a/src/core/api/project_test.go +++ b/src/core/api/project_test.go @@ -15,15 +15,16 @@ package api import ( "fmt" + "net/http" + "strconv" + "testing" + "github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/testing/apitests/apilib" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "net/http" - "strconv" - "testing" ) var addProject *apilib.ProjectReq diff --git a/src/core/auth/ldap/ldap_test.go b/src/core/auth/ldap/ldap_test.go index 3aaaffbea..5cd1c799b 100644 --- a/src/core/auth/ldap/ldap_test.go +++ b/src/core/auth/ldap/ldap_test.go @@ -91,7 +91,7 @@ func TestMain(m *testing.M) { "delete from project where name='member_test_02'", "delete from harbor_user where username='member_test_01' or username='pm_sample'", "delete from user_group", - "delete from project_member", + "delete from project_member where id > 1", } dao.ExecuteBatchSQL(initSqls) defer dao.ExecuteBatchSQL(clearSqls) diff --git a/src/lib/orm/orm.go b/src/lib/orm/orm.go index 91a0eb19f..351c77e7a 100644 --- a/src/lib/orm/orm.go +++ b/src/lib/orm/orm.go @@ -22,6 +22,11 @@ import ( "github.com/goharbor/harbor/src/lib/log" ) +// RegisterModel ... +func RegisterModel(models ...interface{}) { + orm.RegisterModel(models...) +} + type ormKey struct{} // FromContext returns orm from context diff --git a/src/lib/orm/orm_test.go b/src/lib/orm/orm_test.go index 0eb7b9da3..b57ffdc85 100644 --- a/src/lib/orm/orm_test.go +++ b/src/lib/orm/orm_test.go @@ -21,60 +21,68 @@ import ( "github.com/astaxie/beego/orm" "github.com/goharbor/harbor/src/common/dao" - "github.com/goharbor/harbor/src/common/models" "github.com/stretchr/testify/suite" ) -func addProject(ctx context.Context, project models.Project) (int64, error) { +type Foo struct { + ID int64 `orm:"pk;auto;column(id)"` + Name string `orm:"column(name)"` +} + +func (*Foo) TableName() string { + return "foo" +} + +func addFoo(ctx context.Context, foo Foo) (int64, error) { o, err := FromContext(ctx) if err != nil { return 0, err } - return o.Insert(&project) + return o.Insert(&foo) } -func readProject(ctx context.Context, id int64) (*models.Project, error) { +func readFoo(ctx context.Context, id int64) (*Foo, error) { o, err := FromContext(ctx) if err != nil { return nil, err } - project := &models.Project{ - ProjectID: id, + foo := &Foo{ + ID: id, } - if err := o.Read(project, "project_id"); err != nil { + if err := o.Read(foo, "id"); err != nil { return nil, err } - return project, nil + return foo, nil } -func deleteProject(ctx context.Context, id int64) error { +func deleteFoo(ctx context.Context, id int64) error { o, err := FromContext(ctx) if err != nil { return err } - project := &models.Project{ - ProjectID: id, + foo := &Foo{ + ID: id, } - _, err = o.Delete(project, "project_id") + _, err = o.Delete(foo, "id") return err } -func existProject(ctx context.Context, id int64) bool { +func existFoo(ctx context.Context, id int64) bool { o, err := FromContext(ctx) if err != nil { return false } - project := &models.Project{ - ProjectID: id, + foo := &Foo{ + ID: id, } - if err := o.Read(project, "project_id"); err != nil { + if err := o.Read(foo, "id"); err != nil { return false } @@ -88,7 +96,40 @@ type OrmSuite struct { // SetupSuite ... func (suite *OrmSuite) SetupSuite() { + RegisterModel(&Foo{}) dao.PrepareTestForPostgresSQL() + + o, err := FromContext(Context()) + if err != nil { + suite.Fail("got error %v", err) + } + + sql := ` + CREATE TABLE IF NOT EXISTS foo ( + id SERIAL PRIMARY KEY NOT NULL, + name VARCHAR (30), + UNIQUE (name) + ) + ` + + _, err = o.Raw(sql).Exec() + if err != nil { + suite.Fail("got error %v", err) + } +} + +func (suite *OrmSuite) TearDownSuite() { + o, err := FromContext(Context()) + if err != nil { + suite.Fail("got error %v", err) + } + + sql := `DROP TABLE foo` + + _, err = o.Raw(sql).Exec() + if err != nil { + suite.Fail("got error %v", err) + } } func (suite *OrmSuite) TestContext() { @@ -107,13 +148,13 @@ func (suite *OrmSuite) TestWithTransaction() { var id int64 t1 := WithTransaction(func(ctx context.Context) (err error) { - id, err = addProject(ctx, models.Project{Name: "t1", OwnerID: 1}) + id, err = addFoo(ctx, Foo{Name: "t1"}) return err }) suite.Nil(t1(ctx)) - suite.True(existProject(ctx, id)) - suite.Nil(deleteProject(ctx, id)) + suite.True(existFoo(ctx, id)) + suite.Nil(deleteFoo(ctx, id)) } func (suite *OrmSuite) TestSequentialTransactions() { @@ -122,50 +163,50 @@ func (suite *OrmSuite) TestSequentialTransactions() { var id1, id2 int64 t1 := func(ctx context.Context, retErr error) error { return WithTransaction(func(ctx context.Context) (err error) { - id1, err = addProject(ctx, models.Project{Name: "t1", OwnerID: 1}) + id1, err = addFoo(ctx, Foo{Name: "t1"}) if err != nil { return err } // Ensure t1 created success - suite.True(existProject(ctx, id1)) + suite.True(existFoo(ctx, id1)) return retErr })(ctx) } t2 := func(ctx context.Context, retErr error) error { return WithTransaction(func(ctx context.Context) (err error) { - id2, _ = addProject(ctx, models.Project{Name: "t2", OwnerID: 1}) + id2, _ = addFoo(ctx, Foo{Name: "t2"}) if err != nil { return err } // Ensure t2 created success - suite.True(existProject(ctx, id2)) + suite.True(existFoo(ctx, id2)) return retErr })(ctx) } if suite.Nil(t1(ctx, nil)) { - suite.True(existProject(ctx, id1)) + suite.True(existFoo(ctx, id1)) } if suite.Nil(t2(ctx, nil)) { - suite.True(existProject(ctx, id2)) + suite.True(existFoo(ctx, id2)) } - // delete project t1 and t2 in db - suite.Nil(deleteProject(ctx, id1)) - suite.Nil(deleteProject(ctx, id2)) + // delete foo t1 and t2 in db + suite.Nil(deleteFoo(ctx, id1)) + suite.Nil(deleteFoo(ctx, id2)) if suite.Error(t1(ctx, errors.New("oops"))) { - suite.False(existProject(ctx, id1)) + suite.False(existFoo(ctx, id1)) } if suite.Nil(t2(ctx, nil)) { - suite.True(existProject(ctx, id2)) - suite.Nil(deleteProject(ctx, id2)) + suite.True(existFoo(ctx, id2)) + suite.Nil(deleteFoo(ctx, id2)) } } @@ -174,11 +215,11 @@ func (suite *OrmSuite) TestNestedTransaction() { var id1, id2 int64 nt1 := WithTransaction(func(ctx context.Context) (err error) { - id1, err = addProject(ctx, models.Project{Name: "nt1", OwnerID: 1}) + id1, err = addFoo(ctx, Foo{Name: "nt1"}) return err }) nt2 := WithTransaction(func(ctx context.Context) (err error) { - id2, err = addProject(ctx, models.Project{Name: "nt2", OwnerID: 1}) + id2, err = addFoo(ctx, Foo{Name: "nt2"}) return err }) @@ -193,36 +234,36 @@ func (suite *OrmSuite) TestNestedTransaction() { } // Ensure nt1 and nt2 created success - suite.True(existProject(ctx, id1)) - suite.True(existProject(ctx, id2)) + suite.True(existFoo(ctx, id1)) + suite.True(existFoo(ctx, id2)) return retErr })(ctx) } if suite.Nil(nt(ctx, nil)) { - suite.True(existProject(ctx, id1)) - suite.True(existProject(ctx, id2)) + suite.True(existFoo(ctx, id1)) + suite.True(existFoo(ctx, id2)) - // delete project nt1 and nt2 in db - suite.Nil(deleteProject(ctx, id1)) - suite.Nil(deleteProject(ctx, id2)) - suite.False(existProject(ctx, id1)) - suite.False(existProject(ctx, id2)) + // delete foo nt1 and nt2 in db + suite.Nil(deleteFoo(ctx, id1)) + suite.Nil(deleteFoo(ctx, id2)) + suite.False(existFoo(ctx, id1)) + suite.False(existFoo(ctx, id2)) } if suite.Error(nt(ctx, errors.New("oops"))) { - suite.False(existProject(ctx, id1)) - suite.False(existProject(ctx, id2)) + suite.False(existFoo(ctx, id1)) + suite.False(existFoo(ctx, id2)) } // test nt1 failed but we skip it and nt2 success suite.Nil(nt1(ctx)) - suite.True(existProject(ctx, id1)) + suite.True(existFoo(ctx, id1)) // delete nt1 here because id1 will overwrite in the following transaction defer func(id int64) { - suite.Nil(deleteProject(ctx, id)) + suite.Nil(deleteFoo(ctx, id)) }(id1) t := WithTransaction(func(ctx context.Context) error { @@ -233,16 +274,16 @@ func (suite *OrmSuite) TestNestedTransaction() { } // Ensure t2 created success - suite.True(existProject(ctx, id2)) + suite.True(existFoo(ctx, id2)) return nil }) if suite.Nil(t(ctx)) { - suite.True(existProject(ctx, id2)) + suite.True(existFoo(ctx, id2)) - // delete project t2 in db - suite.Nil(deleteProject(ctx, id2)) + // delete foo t2 in db + suite.Nil(deleteFoo(ctx, id2)) } } @@ -251,11 +292,11 @@ func (suite *OrmSuite) TestNestedSavepoint() { var id1, id2 int64 ns1 := WithTransaction(func(ctx context.Context) (err error) { - id1, err = addProject(ctx, models.Project{Name: "ns1", OwnerID: 1}) + id1, err = addFoo(ctx, Foo{Name: "ns1"}) return err }) ns2 := WithTransaction(func(ctx context.Context) (err error) { - id2, err = addProject(ctx, models.Project{Name: "ns2", OwnerID: 1}) + id2, err = addFoo(ctx, Foo{Name: "ns2"}) return err }) @@ -270,8 +311,8 @@ func (suite *OrmSuite) TestNestedSavepoint() { } // Ensure nt1 and nt2 created success - suite.True(existProject(ctx, id1)) - suite.True(existProject(ctx, id2)) + suite.True(existFoo(ctx, id1)) + suite.True(existFoo(ctx, id2)) return retErr })(ctx) @@ -287,25 +328,25 @@ func (suite *OrmSuite) TestNestedSavepoint() { // transaction commit and s1s2 commit suite.Nil(t(ctx, nil, nil)) // Ensure nt1 and nt2 created success - suite.True(existProject(ctx, id1)) - suite.True(existProject(ctx, id2)) - // delete project nt1 and nt2 in db - suite.Nil(deleteProject(ctx, id1)) - suite.Nil(deleteProject(ctx, id2)) - suite.False(existProject(ctx, id1)) - suite.False(existProject(ctx, id2)) + suite.True(existFoo(ctx, id1)) + suite.True(existFoo(ctx, id2)) + // delete foo nt1 and nt2 in db + suite.Nil(deleteFoo(ctx, id1)) + suite.Nil(deleteFoo(ctx, id2)) + suite.False(existFoo(ctx, id1)) + suite.False(existFoo(ctx, id2)) // transaction commit and s1s2 rollback suite.Nil(t(ctx, nil, errors.New("oops"))) // Ensure nt1 and nt2 created failed - suite.False(existProject(ctx, id1)) - suite.False(existProject(ctx, id2)) + suite.False(existFoo(ctx, id1)) + suite.False(existFoo(ctx, id2)) // transaction rollback and s1s2 commit suite.Error(t(ctx, errors.New("oops"), nil)) // Ensure nt1 and nt2 created failed - suite.False(existProject(ctx, id1)) - suite.False(existProject(ctx, id2)) + suite.False(existFoo(ctx, id1)) + suite.False(existFoo(ctx, id2)) } func TestRunOrmSuite(t *testing.T) { diff --git a/src/lib/orm/query.go b/src/lib/orm/query.go index 945b5b020..d1e642db3 100644 --- a/src/lib/orm/query.go +++ b/src/lib/orm/query.go @@ -18,15 +18,63 @@ import ( "context" "reflect" "strings" + "sync" + "unicode" "github.com/astaxie/beego/orm" "github.com/goharbor/harbor/src/common/dao" + "github.com/goharbor/harbor/src/lib/errors" + "github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/lib/q" ) -// QuerySetter generates the query setter according to the query. "ignoredCols" is used to set the -// columns that will not be queried +// Params ... +type Params = orm.Params + +// QuerySeter ... +type QuerySeter = orm.QuerySeter + +// Escape special characters +func Escape(str string) string { + return dao.Escape(str) +} + +// ParamPlaceholderForIn returns a string that contains placeholders for sql keyword "in" +// e.g. n=3, returns "?,?,?" +func ParamPlaceholderForIn(n int) string { + placeholders := []string{} + for i := 0; i < n; i++ { + placeholders = append(placeholders, "?") + } + return strings.Join(placeholders, ",") +} + +// QuerySetter generates the query setter according to the query. "ignoredCols" is used to set the columns that will not be queried. +// Currently, it supports two ways to generate the query setter, the first one is to generate by the fields of the model, +// and the second one is to generate by the methods their name begins with `FilterBy` of the model. +// e.g. for the following model the queriable fields are : +// "Field2", "customized_field2", "Field3", "field3", "Field4" (or "field4") and "Field5" (or "field5"). +// type Foo struct{ +// Field1 string `orm:"-"` +// Field2 string `orm:"column(customized_field2)"` +// Field3 string +// } +// +// func (f *Foo) FilterByField4(ctx context.Context, qs orm.QuerySeter, key string, value interface{}) orm.QuerySeter { +// // The value is the raw value of key in q.Query +// return qs +// } +// +// func (f *Foo) FilterByField5(ctx context.Context, qs orm.QuerySeter, key, value string) orm.QuerySeter { +// // The value string is the value of key in q.Query which is escaped by `Escape` +// return qs +// } func QuerySetter(ctx context.Context, model interface{}, query *q.Query, ignoredCols ...string) (orm.QuerySeter, error) { + val := reflect.ValueOf(model) + if val.Kind() != reflect.Ptr { + return nil, errors.Errorf(" cannot use non-ptr model struct `%s`", getFullName(reflect.Indirect(val).Type())) + } + ormer, err := FromContext(ctx) if err != nil { return nil, err @@ -36,53 +84,26 @@ func QuerySetter(ctx context.Context, model interface{}, query *q.Query, ignored return qs, nil } - // the program will panic when querying the columns that doesn't exist - // list the supported columns first to avoid the panic - cols := listQueriableCols(model, ignoredCols...) - for k, v := range query.Keywords { - col := strings.SplitN(k, orm.ExprSep, 2)[0] - if _, exist := cols[col]; !exist { - continue - } - - // fuzzy match - f, ok := v.(*q.FuzzyMatchValue) - if ok { - qs = qs.Filter(k+"__icontains", f.Value) - continue - } - - // range - r, ok := v.(*q.Range) - if ok { - if r.Min != nil { - qs = qs.Filter(k+"__gte", r.Min) - } - if r.Max != nil { - qs = qs.Filter(k+"__lte", r.Max) - } - continue - } - - // or list - ol, ok := v.(*q.OrList) - if ok { - if len(ol.Values) > 0 { - qs = qs.Filter(k+"__in", ol.Values...) - } - continue - } - - // and list - _, ok = v.(*q.AndList) - if ok { - // do nothing as and list needs to be handled by the logic of DAO - continue - } - - // exact match - qs = qs.Filter(k, v) + ignored := map[string]bool{} + for _, col := range ignoredCols { + ignored[col] = true } + + columns := queriableColumns(model) + methods := queriableMethods(model) + for k, v := range query.Keywords { + field := strings.SplitN(k, orm.ExprSep, 2)[0] + if ignored[field] { + continue + } + + if columns[field] { + qs = queryByColumn(qs, k, v) + } else if method, ok := methods[snakeCase(field)]; ok { + qs = queryByMethod(ctx, qs, k, v, method, val) + } + } + if query.PageSize > 0 { qs = qs.Limit(query.PageSize) if query.PageNumber > 0 { @@ -92,7 +113,88 @@ func QuerySetter(ctx context.Context, model interface{}, query *q.Query, ignored return qs, nil } -// list the columns that can be queried +// get reflect.Type name with package path. +func getFullName(typ reflect.Type) string { + return typ.PkgPath() + "." + typ.Name() +} + +// convert string to snake case +func snakeCase(str string) string { + delim := '_' + + runes := []rune(str) + + var out []rune + for i := 0; i < len(runes); i++ { + if i > 0 && + (unicode.IsUpper(runes[i])) && + ((i+1 < len(runes) && unicode.IsLower(runes[i+1])) || unicode.IsLower(runes[i-1])) { + out = append(out, delim) + } + out = append(out, unicode.ToLower(runes[i])) + } + + return string(out) +} + +func queryByColumn(qs orm.QuerySeter, key string, value interface{}) orm.QuerySeter { + // fuzzy match + if f, ok := value.(*q.FuzzyMatchValue); ok { + return qs.Filter(key+"__icontains", f.Value) + } + + // range + if r, ok := value.(*q.Range); ok { + if r.Min != nil { + qs = qs.Filter(key+"__gte", r.Min) + } + if r.Max != nil { + qs = qs.Filter(key+"__lte", r.Max) + } + return qs + } + + // or list + if ol, ok := value.(*q.OrList); ok { + if len(ol.Values) > 0 { + qs = qs.Filter(key+"__in", ol.Values...) + } + return qs + } + + // and list + if _, ok := value.(*q.AndList); ok { + // do nothing as and list needs to be handled by the logic of DAO + return qs + } + + // exact match + return qs.Filter(key, value) +} + +func queryByMethod(ctx context.Context, qs orm.QuerySeter, key string, value interface{}, methodName string, reflectVal reflect.Value) orm.QuerySeter { + if mv := reflectVal.MethodByName(methodName); mv.IsValid() { + switch method := mv.Interface().(type) { + case func(context.Context, orm.QuerySeter, string, interface{}) orm.QuerySeter: + return method(ctx, qs, key, value) + case func(context.Context, orm.QuerySeter, string, string) orm.QuerySeter: + if str, ok := value.(string); ok { + return method(ctx, qs, key, Escape(str)) + } + log.Warningf("expected string type for the value of method %s, but actual is %T", methodName, value) + default: + return qs + } + } + + return qs +} + +var ( + cache = sync.Map{} +) + +// get model fields which are columns in orm // e.g. for the following model the columns that can be queried are: // "Field2", "customized_field2", "Field3" and "field3" // type model struct{ @@ -100,20 +202,22 @@ func QuerySetter(ctx context.Context, model interface{}, query *q.Query, ignored // Field2 string `orm:"column(customized_field2)"` // Field3 string // } -// -// set "ignoredCols" to ignore the specified columns -func listQueriableCols(model interface{}, ignoredCols ...string) map[string]struct{} { - if model == nil { - return nil +func queriableColumns(model interface{}) map[string]bool { + typ := reflect.Indirect(reflect.ValueOf(model)).Type() + + key := getFullName(typ) + "-columns" + value, ok := cache.Load(key) + if ok { + return value.(map[string]bool) } - ignored := map[string]struct{}{} - for _, ig := range ignoredCols { - ignored[ig] = struct{}{} - } - cols := map[string]struct{}{} - t := reflect.Indirect(reflect.ValueOf(model)).Type() - for i := 0; i < t.NumField(); i++ { - field := t.Field(i) + + cols := map[string]bool{} + defer func() { + cache.Store(key, cols) + }() + + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) orm := field.Tag.Get("orm") if orm == "-" { continue @@ -129,34 +233,46 @@ func listQueriableCols(model interface{}, ignoredCols ...string) map[string]stru } } } - if len(colName) == 0 { - // TODO convert the field.Name to snake_case + + if colName == "" { + colName = snakeCase(field.Name) } - if _, exist := ignored[colName]; exist { - continue - } - if _, exist := ignored[field.Name]; exist { - continue - } - if len(colName) != 0 { - cols[colName] = struct{}{} - } - cols[field.Name] = struct{}{} + + cols[colName] = true + cols[field.Name] = true } return cols } -// ParamPlaceholderForIn returns a string that contains placeholders for sql keyword "in" -// e.g. n=3, returns "?,?,?" -func ParamPlaceholderForIn(n int) string { - placeholders := []string{} - for i := 0; i < n; i++ { - placeholders = append(placeholders, "?") - } - return strings.Join(placeholders, ",") -} +// get model methods which begin with `FilterBy` +func queriableMethods(model interface{}) map[string]string { + val := reflect.ValueOf(model) -// Escape special characters -func Escape(str string) string { - return dao.Escape(str) + key := getFullName(reflect.Indirect(val).Type()) + "-methods" + value, ok := cache.Load(key) + if ok { + return value.(map[string]string) + } + + methods := map[string]string{} + defer func() { + cache.Store(key, methods) + }() + + prefix := "FilterBy" + typ := val.Type() + for i := 0; i < typ.NumMethod(); i++ { + name := typ.Method(i).Name + + if !strings.HasPrefix(name, prefix) { + continue + } + + field := snakeCase(strings.TrimPrefix(name, prefix)) + if field != "" { + methods[field] = name + } + } + + return methods } diff --git a/src/lib/orm/query_test.go b/src/lib/orm/query_test.go index afd3cb26c..ea3b5bbb7 100644 --- a/src/lib/orm/query_test.go +++ b/src/lib/orm/query_test.go @@ -15,47 +15,95 @@ package orm import ( - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "reflect" "testing" ) -func TestListQueriableCols(t *testing.T) { - type model struct { - Field1 string `orm:"column(field1)" json:"field1"` - Field2 string `orm:"column(customized_field2)"` - Field3 string - Field4 string `orm:"column(field4)"` +func Test_snakeCase(t *testing.T) { + type args struct { + str string + } + tests := []struct { + name string + args args + want string + }{ + {"ProjectID", args{"ProjectID"}, "project_id"}, + {"project_id", args{"project_id"}, "project_id"}, + {"RepositoryName", args{"RepositoryName"}, "repository_name"}, + {"repository_name", args{"repository_name"}, "repository_name"}, + {"ProfileURL", args{"ProfileURL"}, "profile_url"}, + {"City", args{"City"}, "city"}, + {"Address1", args{"Address1"}, "address1"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := snakeCase(tt.args.str); got != tt.want { + t.Errorf("snakeCase() = %v, want %v", got, tt.want) + } + }) + } +} + +type Bar struct { + Field1 string `orm:"-"` + Field2 string `orm:"column(customized_field2)"` + Field3 string + FirstName string +} + +func (Bar) Foo() {} + +func (bar *Bar) FilterBy() {} + +func (bar *Bar) FilterByField4() {} + +func Test_queriableColumns(t *testing.T) { + toWant := func(fields ...string) map[string]bool { + want := map[string]bool{} + + for _, field := range fields { + want[field] = true + } + + return want + } + + type args struct { + model interface{} + } + tests := []struct { + name string + args args + want map[string]bool + }{ + {"bar", args{&Bar{}}, toWant("Field2", "customized_field2", "Field3", "field3", "FirstName", "first_name")}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := queriableColumns(tt.args.model); !reflect.DeepEqual(got, tt.want) { + t.Errorf("queriableColumns() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_queriableMethods(t *testing.T) { + type args struct { + model interface{} + } + tests := []struct { + name string + args args + want map[string]string + }{ + {"bar", args{&Bar{}}, map[string]string{"field4": "FilterByField4"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := queriableMethods(tt.args.model); !reflect.DeepEqual(got, tt.want) { + t.Errorf("queriableMethods() = %v, want %v", got, tt.want) + } + }) } - // without ignoring columns - cols := listQueriableCols(&model{}) - require.Len(t, cols, 7) - _, exist := cols["Field1"] - assert.True(t, exist) - _, exist = cols["field1"] - assert.True(t, exist) - _, exist = cols["Field2"] - assert.True(t, exist) - _, exist = cols["customized_field2"] - assert.True(t, exist) - _, exist = cols["Field3"] - assert.True(t, exist) - _, exist = cols["Field4"] - assert.True(t, exist) - _, exist = cols["field4"] - assert.True(t, exist) - - // with ignoring columns - cols = listQueriableCols(&model{}, "Field4") - require.Len(t, cols, 5) - _, exist = cols["Field1"] - assert.True(t, exist) - _, exist = cols["field1"] - assert.True(t, exist) - _, exist = cols["Field2"] - assert.True(t, exist) - _, exist = cols["customized_field2"] - assert.True(t, exist) - _, exist = cols["Field3"] - assert.True(t, exist) } diff --git a/src/lib/q/query.go b/src/lib/q/query.go index d30920230..267681eda 100644 --- a/src/lib/q/query.go +++ b/src/lib/q/query.go @@ -34,6 +34,16 @@ func New(kw KeyWords) *Query { return &Query{Keywords: kw} } +// MustClone returns the clone of query when it's not nil +// or returns a new Query instance +func MustClone(query *Query) *Query { + if query != nil { + clone := *query + return &clone + } + return New(KeyWords{}) +} + // Range query type Range struct { Min interface{} diff --git a/src/lib/q/query_test.go b/src/lib/q/query_test.go new file mode 100644 index 000000000..3a76ccff2 --- /dev/null +++ b/src/lib/q/query_test.go @@ -0,0 +1,41 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package q + +import ( + "reflect" + "testing" +) + +func TestMustClone(t *testing.T) { + type args struct { + query *Query + } + tests := []struct { + name string + args args + want *Query + }{ + {"ptr", args{New(KeyWords{"public": "true"})}, New(KeyWords{"public": "true"})}, + {"nil", args{nil}, New(KeyWords{})}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := MustClone(tt.args.query); !reflect.DeepEqual(got, tt.want) { + t.Errorf("MustClone() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/src/migration/artifact.go b/src/migration/artifact.go index 6418b41b9..7e24a42b9 100644 --- a/src/migration/artifact.go +++ b/src/migration/artifact.go @@ -16,6 +16,7 @@ package migration import ( "context" + art "github.com/goharbor/harbor/src/controller/artifact" "github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/lib/q" @@ -26,7 +27,7 @@ import ( func upgradeData(ctx context.Context) error { abstractor := art.NewAbstractor() - pros, err := project.Mgr.List() + pros, err := project.Mgr.List(ctx) if err != nil { return err } diff --git a/src/pkg/immutabletag/controller_test.go b/src/pkg/immutabletag/controller_test.go index 2c57b08a3..c20aacb7a 100644 --- a/src/pkg/immutabletag/controller_test.go +++ b/src/pkg/immutabletag/controller_test.go @@ -39,6 +39,9 @@ func (s *ControllerTestSuite) TestImmutableRule() { Name: "TestImmutableRule", OwnerID: 1, }) + if s.Nil(err) { + defer dao.DeleteProject(projectID) + } rule := &model.Metadata{ ProjectID: projectID, diff --git a/src/pkg/project/dao/dao.go b/src/pkg/project/dao/dao.go new file mode 100644 index 000000000..8be4555b8 --- /dev/null +++ b/src/pkg/project/dao/dao.go @@ -0,0 +1,169 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dao + +import ( + "context" + "fmt" + "time" + + "github.com/goharbor/harbor/src/common" + "github.com/goharbor/harbor/src/lib/orm" + "github.com/goharbor/harbor/src/lib/q" + "github.com/goharbor/harbor/src/pkg/project/models" +) + +// DAO is the data access object interface for project +type DAO interface { + // Create create a project instance + Create(ctx context.Context, project *models.Project) (int64, error) + // Count returns the total count of projects according to the query + Count(ctx context.Context, query *q.Query) (total int64, err error) + // Delete delete the project instance by id + Delete(ctx context.Context, id int64) error + // Get get project instance by id + Get(ctx context.Context, id int64) (*models.Project, error) + // GetByName get project instance by name + GetByName(ctx context.Context, name string) (*models.Project, error) + // List list projects + List(ctx context.Context, query *q.Query) ([]*models.Project, error) +} + +// New returns an instance of the default DAO +func New() DAO { + return &dao{} +} + +type dao struct{} + +// Create create a project instance +func (d *dao) Create(ctx context.Context, project *models.Project) (int64, error) { + var projectID int64 + + h := func(ctx context.Context) error { + o, err := orm.FromContext(ctx) + if err != nil { + return err + } + + now := time.Now() + project.CreationTime = now + project.UpdateTime = now + + projectID, err = o.Insert(project) + if err != nil { + return orm.WrapConflictError(err, "The project named %s already exists", project.Name) + } + + member := &Member{ + ProjectID: projectID, + EntityID: project.OwnerID, + Role: common.RoleProjectAdmin, + EntityType: common.UserMember, + CreationTime: now, + UpdateTime: now, + } + + if _, err := o.Insert(member); err != nil { + return err + } + + return nil + } + + if err := orm.WithTransaction(h)(ctx); err != nil { + return 0, err + } + + return projectID, nil +} + +// Count returns the total count of artifacts according to the query +func (d *dao) Count(ctx context.Context, query *q.Query) (total int64, err error) { + query = q.MustClone(query) + query.Keywords["deleted"] = false + query.PageNumber = 0 + query.PageSize = 0 + + qs, err := orm.QuerySetter(ctx, &models.Project{}, query) + if err != nil { + return 0, err + } + return qs.Count() +} + +// Delete delete the project instance by id +func (d *dao) Delete(ctx context.Context, id int64) error { + project, err := d.Get(ctx, id) + if err != nil { + return err + } + + project.Deleted = true + project.Name = fmt.Sprintf("%s#%d", project.Name, project.ProjectID) + + o, err := orm.FromContext(ctx) + if err != nil { + return err + } + + _, err = o.Update(project, "deleted", "name") + return err +} + +// Get get project instance by id +func (d *dao) Get(ctx context.Context, id int64) (*models.Project, error) { + o, err := orm.FromContext(ctx) + if err != nil { + return nil, err + } + + project := &models.Project{ProjectID: id, Deleted: false} + if err = o.Read(project, "project_id", "deleted"); err != nil { + return nil, orm.WrapNotFoundError(err, "project %d not found", id) + } + return project, nil +} + +// GetByName get project instance by name +func (d *dao) GetByName(ctx context.Context, name string) (*models.Project, error) { + o, err := orm.FromContext(ctx) + if err != nil { + return nil, err + } + + project := &models.Project{Name: name, Deleted: false} + if err := o.Read(project, "name", "deleted"); err != nil { + return nil, orm.WrapNotFoundError(err, "project %s not found", name) + } + return project, nil +} + +func (d *dao) List(ctx context.Context, query *q.Query) ([]*models.Project, error) { + query = q.MustClone(query) + query.Keywords["deleted"] = false + + qs, err := orm.QuerySetter(ctx, &models.Project{}, query) + if err != nil { + return nil, err + } + + projects := []*models.Project{} + if _, err := qs.All(&projects); err != nil { + return nil, err + } + + return projects, nil +} diff --git a/src/pkg/project/dao/dao_test.go b/src/pkg/project/dao/dao_test.go new file mode 100644 index 000000000..03832c671 --- /dev/null +++ b/src/pkg/project/dao/dao_test.go @@ -0,0 +1,299 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dao + +import ( + "fmt" + "testing" + + "github.com/goharbor/harbor/src/common" + "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/project/models" + htesting "github.com/goharbor/harbor/src/testing" + "github.com/stretchr/testify/suite" +) + +type DaoTestSuite struct { + htesting.Suite + dao DAO +} + +func (suite *DaoTestSuite) SetupSuite() { + suite.Suite.SetupSuite() + suite.dao = New() +} + +func (suite *DaoTestSuite) WithUser(f func(int64, string), usernames ...string) { + var username string + if len(usernames) > 0 { + username = usernames[0] + } else { + username = suite.RandString(5) + } + + o, err := orm.FromContext(orm.Context()) + if err != nil { + suite.Fail("got error %v", err) + } + + var userID int64 + + email := fmt.Sprintf("%s@example.com", username) + sql := "INSERT INTO harbor_user (username, realname, email, password) VALUES (?, ?, ?, 'Harbor12345') RETURNING user_id" + suite.Nil(o.Raw(sql, username, username, email).QueryRow(&userID)) + + defer func() { + o.Raw("UPDATE harbor_user SET deleted=True WHERE user_id = ?", userID).Exec() + }() + + f(userID, username) +} + +func (suite *DaoTestSuite) WithUserGroup(f func(int64, string), groupNames ...string) { + var groupName string + if len(groupNames) > 0 { + groupName = groupNames[0] + } else { + groupName = suite.RandString(5) + } + + o, err := orm.FromContext(orm.Context()) + if err != nil { + suite.Fail("got error %v", err) + } + + var groupID int64 + + groupDN := fmt.Sprintf("cn=%s,dc=goharbor,dc=io", groupName) + suite.Nil(o.Raw("INSERT INTO user_group (group_name, ldap_group_dn) VALUES (?, ?) RETURNING id", groupName, groupDN).QueryRow(&groupID)) + + defer func() { + o.Raw("DELETE FROM user_group WHERE id = ?", groupID).Exec() + }() + + f(groupID, groupName) +} + +func (suite *DaoTestSuite) TestCreate() { + { + project := &models.Project{ + Name: "foobar", + OwnerID: 1, + } + + projectID, err := suite.dao.Create(orm.Context(), project) + suite.Nil(err) + suite.dao.Delete(orm.Context(), projectID) + } + + { + // project name duplicated + project := &models.Project{ + Name: "library", + OwnerID: 1, + } + + projectID, err := suite.dao.Create(orm.Context(), project) + suite.Error(err) + suite.True(errors.IsConflictErr(err)) + suite.Equal(int64(0), projectID) + } +} + +func (suite *DaoTestSuite) TestCount() { + { + count, err := suite.dao.Count(orm.Context(), q.New(q.KeyWords{"project_id": 1})) + suite.Nil(err) + suite.Equal(int64(1), count) + } +} + +func (suite *DaoTestSuite) TestDelete() { + project := &models.Project{ + Name: "foobar", + OwnerID: 1, + } + + projectID, err := suite.dao.Create(orm.Context(), project) + suite.Nil(err) + + p1, err := suite.dao.Get(orm.Context(), projectID) + suite.Nil(err) + suite.Equal("foobar", p1.Name) + + suite.dao.Delete(orm.Context(), projectID) + + p2, err := suite.dao.Get(orm.Context(), projectID) + suite.Error(err) + suite.True(errors.IsNotFoundErr(err)) + suite.Nil(p2) +} + +func (suite *DaoTestSuite) TestGet() { + { + project, err := suite.dao.Get(orm.Context(), 1) + suite.Nil(err) + suite.Equal("library", project.Name) + } + + { + // not found + project, err := suite.dao.Get(orm.Context(), 10000) + suite.Error(err) + suite.True(errors.IsNotFoundErr(err)) + suite.Nil(project) + } +} + +func (suite *DaoTestSuite) TestGetByName() { + { + project, err := suite.dao.GetByName(orm.Context(), "library") + suite.Nil(err) + suite.Equal("library", project.Name) + } + + { + // not found + project, err := suite.dao.GetByName(orm.Context(), "project10000") + suite.Error(err) + suite.True(errors.IsNotFoundErr(err)) + suite.Nil(project) + } +} + +func (suite *DaoTestSuite) TestList() { + projectNames := []string{"foo1", "foo2", "foo3"} + + var projectIDs []int64 + for _, projectName := range projectNames { + project := &models.Project{ + Name: projectName, + OwnerID: 1, + } + projectID, err := suite.dao.Create(orm.Context(), project) + if suite.Nil(err) { + projectIDs = append(projectIDs, projectID) + } + } + + defer func() { + for _, projectID := range projectIDs { + suite.dao.Delete(orm.Context(), projectID) + } + }() + + projects, err := suite.dao.List(orm.Context(), q.New(q.KeyWords{"project_id__in": projectIDs})) + suite.Nil(err) + suite.Len(projects, len(projectNames)) +} + +func (suite *DaoTestSuite) TestListByPublic() { + { + // default library project + projects, err := suite.dao.List(orm.Context(), q.New(q.KeyWords{"public": true})) + suite.Nil(err) + suite.Len(projects, 1) + } + + { + // default library project + projects, err := suite.dao.List(orm.Context(), q.New(q.KeyWords{"public": "true"})) + suite.Nil(err) + suite.Len(projects, 1) + } + + { + projects, err := suite.dao.List(orm.Context(), q.New(q.KeyWords{"public": false})) + suite.Nil(err) + suite.Len(projects, 0) + } + + { + projects, err := suite.dao.List(orm.Context(), q.New(q.KeyWords{"public": "false"})) + suite.Nil(err) + suite.Len(projects, 0) + } +} + +func (suite *DaoTestSuite) TestListByOwner() { + { + // default library project + projects, err := suite.dao.List(orm.Context(), q.New(q.KeyWords{"owner": "admin"})) + suite.Nil(err) + suite.Len(projects, 1) + } + + { + projects, err := suite.dao.List(orm.Context(), q.New(q.KeyWords{"owner": "owner-not-found"})) + suite.Nil(err) + suite.Len(projects, 0) + } +} + +func (suite *DaoTestSuite) TestListByMember() { + { + // project admin + projects, err := suite.dao.List(orm.Context(), q.New(q.KeyWords{"member": &models.MemberQuery{Name: "admin", Role: common.RoleProjectAdmin}})) + suite.Nil(err) + suite.Len(projects, 1) + } + + { + // guest + projects, err := suite.dao.List(orm.Context(), q.New(q.KeyWords{"member": &models.MemberQuery{Name: "admin", Role: common.RoleGuest}})) + suite.Nil(err) + suite.Len(projects, 0) + } + + { + suite.WithUser(func(userID int64, username string) { + project := &models.Project{ + Name: "project-with-user-group", + OwnerID: int(userID), + } + projectID, err := suite.dao.Create(orm.Context(), project) + suite.Nil(err) + + defer suite.dao.Delete(orm.Context(), projectID) + + suite.WithUserGroup(func(groupID int64, groupName string) { + + o, err := orm.FromContext(orm.Context()) + if err != nil { + suite.Fail("got error %v", err) + } + + var pid int64 + suite.Nil(o.Raw("INSERT INTO project_member (project_id, entity_id, role, entity_type) values (?, ?, ?, ?) RETURNING id", projectID, groupID, common.RoleGuest, "g").QueryRow(&pid)) + defer o.Raw("DELETE FROM project_member WHERE id = ?", pid) + + memberQuery := &models.MemberQuery{ + Name: "admin", + Role: common.RoleProjectAdmin, + GroupIDs: []int{int(groupID)}, + } + projects, err := suite.dao.List(orm.Context(), q.New(q.KeyWords{"member": memberQuery})) + suite.Nil(err) + suite.Len(projects, 2) + }) + }) + } +} + +func TestDaoTestSuite(t *testing.T) { + suite.Run(t, &DaoTestSuite{}) +} diff --git a/src/pkg/project/dao/model.go b/src/pkg/project/dao/model.go new file mode 100644 index 000000000..c6ee3d491 --- /dev/null +++ b/src/pkg/project/dao/model.go @@ -0,0 +1,43 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dao + +import ( + "time" + + "github.com/goharbor/harbor/src/lib/orm" +) + +func init() { + orm.RegisterModel( + new(Member), + ) +} + +// Member holds the details of a member. +type Member struct { + ID int `orm:"pk;auto;column(id)" json:"id"` + ProjectID int64 `orm:"column(project_id)" json:"project_id"` + Role int `orm:"column(role)" json:"role_id"` + EntityID int `orm:"column(entity_id)" json:"entity_id"` + EntityType string `orm:"column(entity_type)" json:"entity_type"` + CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"` +} + +// TableName ... +func (*Member) TableName() string { + return "project_member" +} diff --git a/src/pkg/project/manager.go b/src/pkg/project/manager.go index 5f027ca50..fddda6d1d 100644 --- a/src/pkg/project/manager.go +++ b/src/pkg/project/manager.go @@ -15,10 +15,12 @@ package project import ( - "fmt" + "context" - "github.com/goharbor/harbor/src/common/dao" - "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/lib/errors" + "github.com/goharbor/harbor/src/lib/q" + "github.com/goharbor/harbor/src/pkg/project/dao" + "github.com/goharbor/harbor/src/pkg/project/models" ) var ( @@ -27,40 +29,72 @@ var ( ) // Manager is used for project management -// currently, the interface only defines the methods needed for tag retention -// will expand it when doing refactor type Manager interface { - // List projects according to the query - List(...*models.ProjectQueryParam) ([]*models.Project, error) + // Create create project instance + Create(ctx context.Context, project *models.Project) (int64, error) + + // Count returns the total count of projects according to the query + Count(ctx context.Context, query *q.Query) (total int64, err error) + + // Delete delete the project instance by id + Delete(ctx context.Context, id int64) error + // Get the project specified by the ID or name - Get(interface{}) (*models.Project, error) + Get(ctx context.Context, idOrName interface{}) (*models.Project, error) + + // List projects according to the query + List(ctx context.Context, query ...*models.ProjectQueryParam) ([]*models.Project, error) } // New returns a default implementation of Manager func New() Manager { - return &manager{} + return &manager{dao: dao.New()} } -type manager struct{} +type manager struct { + dao dao.DAO +} -// List projects according to the query -func (m *manager) List(query ...*models.ProjectQueryParam) ([]*models.Project, error) { - var q *models.ProjectQueryParam - if len(query) > 0 { - q = query[0] +// Create create project instance +func (m *manager) Create(ctx context.Context, project *models.Project) (int64, error) { + if project.OwnerID <= 0 { + return 0, errors.BadRequestError(nil).WithMessage("Owner is missing when creating project %s", project.Name) } - return dao.GetProjects(q) + return m.dao.Create(ctx, project) +} + +// Count returns the total count of projects according to the query +func (m *manager) Count(ctx context.Context, query *q.Query) (total int64, err error) { + return m.dao.Count(ctx, query) +} + +// Delete delete the project instance by id +func (m *manager) Delete(ctx context.Context, id int64) error { + return m.dao.Delete(ctx, id) } // Get the project specified by the ID -func (m *manager) Get(idOrName interface{}) (*models.Project, error) { +func (m *manager) Get(ctx context.Context, idOrName interface{}) (*models.Project, error) { id, ok := idOrName.(int64) if ok { - return dao.GetProjectByID(id) + return m.dao.Get(ctx, id) } name, ok := idOrName.(string) if ok { - return dao.GetProjectByName(name) + return m.dao.GetByName(ctx, name) } - return nil, fmt.Errorf("invalid parameter: %v, should be ID(int64) or name(string)", idOrName) + return nil, errors.Errorf("invalid parameter: %v, should be ID(int64) or name(string)", idOrName) +} + +// List projects according to the query +func (m *manager) List(ctx context.Context, query ...*models.ProjectQueryParam) ([]*models.Project, error) { + var param *models.ProjectQueryParam + if len(query) > 0 { + param = query[0] + } + if param == nil { + return m.dao.List(ctx, nil) + } + + return m.dao.List(ctx, param.ToQuery()) } diff --git a/src/pkg/project/metadata/dao/dao.go b/src/pkg/project/metadata/dao/dao.go new file mode 100644 index 000000000..4fb5910fc --- /dev/null +++ b/src/pkg/project/metadata/dao/dao.go @@ -0,0 +1,104 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dao + +import ( + "context" + "time" + + "github.com/goharbor/harbor/src/lib/orm" + "github.com/goharbor/harbor/src/lib/q" + "github.com/goharbor/harbor/src/pkg/project/metadata/models" +) + +// DAO is the data access object interface for project metadata +type DAO interface { + // Create create metadata instances for the project + Create(ctx context.Context, projectID int64, name, value string) (int64, error) + + // Delete delete metadata interfaces filtered the query + Delete(ctx context.Context, query *q.Query) error + + // Update update the value of metadata instance + Update(ctx context.Context, projectID int64, name, value string) error + + // List returns project metadata instances + List(ctx context.Context, query *q.Query) ([]*models.ProjectMetadata, error) +} + +// New returns an instance of the default DAO +func New() DAO { + return &dao{} +} + +type dao struct{} + +// Create create metadata instances for the project +func (d *dao) Create(ctx context.Context, projectID int64, name, value string) (int64, error) { + o, err := orm.FromContext(ctx) + if err != nil { + return 0, err + } + + now := time.Now() + md := &models.ProjectMetadata{ + ProjectID: projectID, + Name: name, + Value: value, + CreationTime: now, + UpdateTime: now, + } + + return o.Insert(md) +} + +// Delete delete metadata interfaces filtered the query +func (d *dao) Delete(ctx context.Context, query *q.Query) error { + qs, err := orm.QuerySetter(ctx, &models.ProjectMetadata{}, query) + if err != nil { + return err + } + + _, err = qs.Delete() + return err +} + +// Update update the metadata instance +func (d *dao) Update(ctx context.Context, projectID int64, name, value string) error { + qs, err := orm.QuerySetter(ctx, &models.ProjectMetadata{}, nil) + if err != nil { + return err + } + + qs = qs.Filter("project_id", projectID).Filter("name", name) + + _, err = qs.Update(orm.Params{"value": value}) + return err +} + +// List returns project metadata instances +func (d *dao) List(ctx context.Context, query *q.Query) ([]*models.ProjectMetadata, error) { + qs, err := orm.QuerySetter(ctx, &models.ProjectMetadata{}, query) + if err != nil { + return nil, err + } + + mds := []*models.ProjectMetadata{} + if _, err := qs.All(&mds); err != nil { + return nil, err + } + + return mds, nil +} diff --git a/src/pkg/project/metadata/dao/dao_test.go b/src/pkg/project/metadata/dao/dao_test.go new file mode 100644 index 000000000..571281459 --- /dev/null +++ b/src/pkg/project/metadata/dao/dao_test.go @@ -0,0 +1,70 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dao + +import ( + "testing" + + "github.com/goharbor/harbor/src/lib/orm" + "github.com/goharbor/harbor/src/lib/q" + htesting "github.com/goharbor/harbor/src/testing" + "github.com/stretchr/testify/suite" +) + +type DaoTestSuite struct { + htesting.Suite + dao DAO +} + +func (suite *DaoTestSuite) SetupSuite() { + suite.Suite.SetupSuite() + suite.dao = New() +} + +func (suite *DaoTestSuite) TestCreate() { + id, err := suite.dao.Create(orm.Context(), 1, "foo", "bar") + if suite.Nil(err) { + defer func() { + suite.Nil(suite.dao.Delete(orm.Context(), q.New(q.KeyWords{"id": id}))) + }() + } + + mds, err := suite.dao.List(orm.Context(), q.New(q.KeyWords{"id": id})) + suite.Nil(err) + suite.Len(mds, 1) + suite.Equal("foo", mds[0].Name) + suite.Equal("bar", mds[0].Value) + +} + +func (suite *DaoTestSuite) TestUpdate() { + id, err := suite.dao.Create(orm.Context(), 1, "foo", "bar") + if suite.Nil(err) { + defer func() { + suite.Nil(suite.dao.Delete(orm.Context(), q.New(q.KeyWords{"id": id}))) + }() + } + + suite.Nil(suite.dao.Update(orm.Context(), 1, "foo", "Bar")) + mds, err := suite.dao.List(orm.Context(), q.New(q.KeyWords{"id": id})) + suite.Nil(err) + suite.Len(mds, 1) + suite.Equal("foo", mds[0].Name) + suite.Equal("Bar", mds[0].Value) +} + +func TestDaoTestSuite(t *testing.T) { + suite.Run(t, &DaoTestSuite{}) +} diff --git a/src/pkg/project/metadata/manager.go b/src/pkg/project/metadata/manager.go new file mode 100644 index 000000000..ac43acf57 --- /dev/null +++ b/src/pkg/project/metadata/manager.go @@ -0,0 +1,128 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package metadata + +import ( + "context" + + "github.com/goharbor/harbor/src/lib/orm" + "github.com/goharbor/harbor/src/lib/q" + "github.com/goharbor/harbor/src/pkg/project/metadata/dao" + "github.com/goharbor/harbor/src/pkg/project/metadata/models" +) + +var ( + // Mgr is the global project metadata manager + Mgr = New() +) + +// Manager defines the operations that a project metadata manager should implement +type Manager interface { + // Add metadatas for project specified by projectID + Add(ctx context.Context, projectID int64, meta map[string]string) error + + // Delete metadatas whose keys are specified in parameter meta, if it is absent, delete all + Delete(ctx context.Context, projectID int64, meta ...string) error + + // Update metadatas + Update(ctx context.Context, projectID int64, meta map[string]string) error + + // Get metadatas whose keys are specified in parameter meta, if it is absent, get all + Get(ctx context.Context, projectID int64, meta ...string) (map[string]string, error) + + // List metadata according to the name and value + List(ctx context.Context, name, value string) ([]*models.ProjectMetadata, error) +} + +// New returns a default implementation of Manager +func New() Manager { + return &manager{dao: dao.New()} +} + +type manager struct { + dao dao.DAO +} + +// Add metadatas for project specified by projectID +func (m *manager) Add(ctx context.Context, projectID int64, meta map[string]string) error { + h := func(ctx context.Context) error { + for name, value := range meta { + if _, err := m.dao.Create(ctx, projectID, name, value); err != nil { + return err + } + } + return nil + } + return orm.WithTransaction(h)(ctx) +} + +// Delete metadatas whose keys are specified in parameter meta, if it is absent, delete all +func (m *manager) Delete(ctx context.Context, projectID int64, meta ...string) error { + return m.dao.Delete(ctx, makeQuery(projectID, meta...)) +} + +// Update metadatas +func (m *manager) Update(ctx context.Context, projectID int64, meta map[string]string) error { + if len(meta) == 0 { + return nil + } + + h := func(ctx context.Context) error { + for name, value := range meta { + if err := m.dao.Update(ctx, projectID, name, value); err != nil { + return err + } + } + + return nil + } + + return orm.WithTransaction(h)(ctx) +} + +// Get metadatas whose keys are specified in parameter meta, if it is absent, get all +func (m *manager) Get(ctx context.Context, projectID int64, meta ...string) (map[string]string, error) { + mds, err := m.dao.List(ctx, makeQuery(projectID, meta...)) + if err != nil { + return nil, err + } + + data := map[string]string{} + for _, md := range mds { + data[md.Name] = md.Value + } + + return data, nil +} + +// List metadata according to the name and value +func (m *manager) List(ctx context.Context, name string, value string) ([]*models.ProjectMetadata, error) { + return m.dao.List(ctx, q.New(q.KeyWords{"name": name, "value": value})) +} + +func makeQuery(projectID int64, meta ...string) *q.Query { + kw := q.KeyWords{ + "project_id": projectID, + } + if len(meta) > 0 { + var names []string + for _, name := range meta { + names = append(names, name) + } + kw["name__in"] = names + } + + return q.New(kw) +} diff --git a/src/pkg/project/metadata/models/metadata.go b/src/pkg/project/metadata/models/metadata.go new file mode 100644 index 000000000..b031ab562 --- /dev/null +++ b/src/pkg/project/metadata/models/metadata.go @@ -0,0 +1,22 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package models + +import ( + "github.com/goharbor/harbor/src/common/models" +) + +// ProjectMetadata ... +type ProjectMetadata = models.ProjectMetadata diff --git a/src/pkg/project/models/project.go b/src/pkg/project/models/project.go new file mode 100644 index 000000000..209028d09 --- /dev/null +++ b/src/pkg/project/models/project.go @@ -0,0 +1,43 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package models + +import ( + "github.com/goharbor/harbor/src/common/models" +) + +// Project ... +type Project = models.Project + +// Projects the connection for Project +type Projects []*models.Project + +// OwnerIDs returns all the owner ids from the projects +func (projects Projects) OwnerIDs() []int { + var ownerIDs []int + for _, project := range projects { + ownerIDs = append(ownerIDs, project.OwnerID) + } + return ownerIDs +} + +// Member ... +type Member = models.Member + +// ProjectQueryParam ... +type ProjectQueryParam = models.ProjectQueryParam + +// MemberQuery ... +type MemberQuery = models.MemberQuery diff --git a/src/pkg/retention/controller_test.go b/src/pkg/retention/controller_test.go index 358cf56a7..beefe1a98 100644 --- a/src/pkg/retention/controller_test.go +++ b/src/pkg/retention/controller_test.go @@ -1,15 +1,31 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package retention import ( "context" + "strings" + "testing" + "github.com/goharbor/harbor/src/pkg/retention/dep" "github.com/goharbor/harbor/src/pkg/retention/policy" "github.com/goharbor/harbor/src/pkg/retention/policy/rule" "github.com/goharbor/harbor/src/pkg/scheduler" + "github.com/goharbor/harbor/src/testing/pkg/project" "github.com/goharbor/harbor/src/testing/pkg/repository" "github.com/stretchr/testify/suite" - "strings" - "testing" ) type ControllerTestSuite struct { @@ -29,7 +45,7 @@ func TestController(t *testing.T) { } func (s *ControllerTestSuite) TestPolicy() { - projectMgr := &fakeProjectManager{} + projectMgr := &project.Manager{} repositoryMgr := &repository.FakeManager{} retentionScheduler := &fakeRetentionScheduler{} retentionLauncher := &fakeLauncher{} @@ -127,7 +143,7 @@ func (s *ControllerTestSuite) TestPolicy() { } func (s *ControllerTestSuite) TestExecution() { - projectMgr := &fakeProjectManager{} + projectMgr := &project.Manager{} repositoryMgr := &repository.FakeManager{} retentionScheduler := &fakeRetentionScheduler{} retentionLauncher := &fakeLauncher{} diff --git a/src/pkg/retention/launcher.go b/src/pkg/retention/launcher.go index af1635fff..95164bd66 100644 --- a/src/pkg/retention/launcher.go +++ b/src/pkg/retention/launcher.go @@ -16,10 +16,11 @@ package retention import ( "fmt" + "time" + beegoorm "github.com/astaxie/beego/orm" "github.com/goharbor/harbor/src/lib/orm" "github.com/goharbor/harbor/src/lib/selector" - "time" "github.com/goharbor/harbor/src/jobservice/job" "github.com/goharbor/harbor/src/lib/selector/selectors/index" @@ -325,7 +326,7 @@ func launcherError(err error) error { } func getProjects(projectMgr project.Manager) ([]*selector.Candidate, error) { - projects, err := projectMgr.List() + projects, err := projectMgr.List(orm.Context()) if err != nil { return nil, err } diff --git a/src/pkg/retention/launcher_test.go b/src/pkg/retention/launcher_test.go index 5fc7a3c4f..7fbb9f844 100644 --- a/src/pkg/retention/launcher_test.go +++ b/src/pkg/retention/launcher_test.go @@ -15,7 +15,8 @@ package retention import ( - "fmt" + "testing" + "github.com/goharbor/harbor/src/common/job" "github.com/goharbor/harbor/src/common/models" _ "github.com/goharbor/harbor/src/lib/selector/selectors/doublestar" @@ -24,42 +25,14 @@ import ( "github.com/goharbor/harbor/src/pkg/retention/policy/rule" "github.com/goharbor/harbor/src/pkg/retention/q" hjob "github.com/goharbor/harbor/src/testing/job" + "github.com/goharbor/harbor/src/testing/mock" + projecttesting "github.com/goharbor/harbor/src/testing/pkg/project" "github.com/goharbor/harbor/src/testing/pkg/repository" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" - "testing" ) -type fakeProjectManager struct { - projects []*models.Project -} - -func (f *fakeProjectManager) List(...*models.ProjectQueryParam) ([]*models.Project, error) { - return f.projects, nil -} -func (f *fakeProjectManager) Get(idOrName interface{}) (*models.Project, error) { - id, ok := idOrName.(int64) - if ok { - for _, pro := range f.projects { - if pro.ProjectID == id { - return pro, nil - } - } - return nil, nil - } - name, ok := idOrName.(string) - if ok { - for _, pro := range f.projects { - if pro.Name == name { - return pro, nil - } - } - return nil, nil - } - return nil, fmt.Errorf("invalid parameter: %v, should be ID(int64) or name(string)", idOrName) -} - type fakeRetentionManager struct{} func (f *fakeRetentionManager) GetTotalOfRetentionExecs(policyID int64) (int64, error) { @@ -145,10 +118,11 @@ func (l *launchTestSuite) SetupTest() { ProjectID: 2, Name: "test", } - l.projectMgr = &fakeProjectManager{ - projects: []*models.Project{ - pro1, pro2, - }} + projectMgr := &projecttesting.Manager{} + mock.OnAnything(projectMgr, "List").Return([]*models.Project{ + pro1, pro2, + }, nil) + l.projectMgr = projectMgr l.repositoryMgr = &repository.FakeManager{} l.retentionMgr = &fakeRetentionManager{} l.jobserviceClient = &hjob.MockJobClient{ diff --git a/src/pkg/user/dao/dao.go b/src/pkg/user/dao/dao.go new file mode 100644 index 000000000..e8a9ac890 --- /dev/null +++ b/src/pkg/user/dao/dao.go @@ -0,0 +1,74 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dao + +import ( + "context" + "strings" + + "github.com/goharbor/harbor/src/lib/orm" + "github.com/goharbor/harbor/src/lib/q" + "github.com/goharbor/harbor/src/pkg/user/models" +) + +// DAO is the data access object interface for user +type DAO interface { + // List list users + List(ctx context.Context, query *q.Query) ([]*models.User, error) +} + +// New returns an instance of the default DAO +func New() DAO { + return &dao{} +} + +type dao struct{} + +// List list users +func (d *dao) List(ctx context.Context, query *q.Query) ([]*models.User, error) { + query = q.MustClone(query) + if query.Sorting == "" { + query.Sorting = "username" + } + + excludeAdmin := true + for key := range query.Keywords { + str := strings.ToLower(key) + if str == "user_id__in" { + excludeAdmin = false + break + } else if str == "user_id" { + excludeAdmin = false + break + } + } + + if excludeAdmin { + // Exclude admin account when not filter by UserIDs, see https://github.com/goharbor/harbor/issues/2527 + query.Keywords["user_id__gt"] = 1 + } + + qs, err := orm.QuerySetter(ctx, &models.User{}, query) + if err != nil { + return nil, err + } + + users := []*models.User{} + if _, err := qs.OrderBy(query.Sorting).All(&users); err != nil { + return nil, err + } + + return users, nil +} diff --git a/src/pkg/user/dao/dao_test.go b/src/pkg/user/dao/dao_test.go new file mode 100644 index 000000000..f61ffc89d --- /dev/null +++ b/src/pkg/user/dao/dao_test.go @@ -0,0 +1,46 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dao + +import ( + "testing" + + "github.com/goharbor/harbor/src/lib/orm" + "github.com/goharbor/harbor/src/lib/q" + htesting "github.com/goharbor/harbor/src/testing" + "github.com/stretchr/testify/suite" +) + +type DaoTestSuite struct { + htesting.Suite + dao DAO +} + +func (suite *DaoTestSuite) SetupSuite() { + suite.Suite.SetupSuite() + suite.dao = New() +} + +func (suite *DaoTestSuite) TestList() { + { + users, err := suite.dao.List(orm.Context(), q.New(q.KeyWords{"user_id": 1})) + suite.Nil(err) + suite.Len(users, 1) + } +} + +func TestDaoTestSuite(t *testing.T) { + suite.Run(t, &DaoTestSuite{}) +} diff --git a/src/pkg/user/manager.go b/src/pkg/user/manager.go new file mode 100644 index 000000000..25a6695f8 --- /dev/null +++ b/src/pkg/user/manager.go @@ -0,0 +1,48 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package user + +import ( + "context" + + "github.com/goharbor/harbor/src/lib/q" + "github.com/goharbor/harbor/src/pkg/user/dao" + "github.com/goharbor/harbor/src/pkg/user/models" +) + +var ( + // Mgr is the global project manager + Mgr = New() +) + +// Manager is used for user management +type Manager interface { + // List users according to the query + List(ctx context.Context, query *q.Query) (models.Users, error) +} + +// New returns a default implementation of Manager +func New() Manager { + return &manager{dao: dao.New()} +} + +type manager struct { + dao dao.DAO +} + +// List users according to the query +func (m *manager) List(ctx context.Context, query *q.Query) (models.Users, error) { + return m.dao.List(ctx, query) +} diff --git a/src/pkg/user/models/user.go b/src/pkg/user/models/user.go new file mode 100644 index 000000000..a17cfa86e --- /dev/null +++ b/src/pkg/user/models/user.go @@ -0,0 +1,35 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package models + +import ( + "github.com/goharbor/harbor/src/common/models" +) + +// User ... +type User = models.User + +// Users the collection for User +type Users []*User + +// MapByUserID returns map which key is UserID of the user and value is the user itself +func (users Users) MapByUserID() map[int]*User { + m := map[int]*User{} + for _, user := range users { + m[user.UserID] = user + } + + return m +} diff --git a/src/server/middleware/repoproxy/proxy_test.go b/src/server/middleware/repoproxy/proxy_test.go index fd2923d67..e75b79012 100644 --- a/src/server/middleware/repoproxy/proxy_test.go +++ b/src/server/middleware/repoproxy/proxy_test.go @@ -16,12 +16,13 @@ package repoproxy import ( "context" + "testing" + "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/security" "github.com/goharbor/harbor/src/common/security/proxycachesecret" securitySecret "github.com/goharbor/harbor/src/common/security/secret" "github.com/goharbor/harbor/src/core/config" - "testing" ) func TestIsProxyProject(t *testing.T) { @@ -60,7 +61,7 @@ func TestIsProxySession(t *testing.T) { sc1 := securitySecret.NewSecurityContext("123456789", config.SecretStore) otherCtx := security.NewContext(context.Background(), sc1) - sc2 := proxycachesecret.NewSecurityContext("library/hello-world") + sc2 := proxycachesecret.NewSecurityContext(context.Background(), "library/hello-world") proxyCtx := security.NewContext(context.Background(), sc2) cases := []struct { name string diff --git a/src/server/middleware/security/proxy_cache_secret.go b/src/server/middleware/security/proxy_cache_secret.go index 2045d3229..30ac757c0 100644 --- a/src/server/middleware/security/proxy_cache_secret.go +++ b/src/server/middleware/security/proxy_cache_secret.go @@ -38,5 +38,5 @@ func (p *proxyCacheSecret) Generate(req *http.Request) security.Context { return nil } log.Debugf("a proxy cache secret security context generated for request %s %s", req.Method, req.URL.Path) - return proxycachesecret.NewSecurityContext(artifact.Repository) + return proxycachesecret.NewSecurityContext(req.Context(), artifact.Repository) } diff --git a/src/testing/controller/project/controller.go b/src/testing/controller/project/controller.go index 61afc88d7..d0a468dcc 100644 --- a/src/testing/controller/project/controller.go +++ b/src/testing/controller/project/controller.go @@ -9,6 +9,8 @@ import ( mock "github.com/stretchr/testify/mock" project "github.com/goharbor/harbor/src/controller/project" + + q "github.com/goharbor/harbor/src/lib/q" ) // Controller is an autogenerated mock type for the Controller type @@ -16,6 +18,62 @@ type Controller struct { mock.Mock } +// Count provides a mock function with given fields: ctx, query +func (_m *Controller) Count(ctx context.Context, query *q.Query) (int64, error) { + ret := _m.Called(ctx, query) + + var r0 int64 + if rf, ok := ret.Get(0).(func(context.Context, *q.Query) int64); ok { + r0 = rf(ctx, query) + } else { + r0 = ret.Get(0).(int64) + } + + 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 +} + +// Create provides a mock function with given fields: ctx, _a1 +func (_m *Controller) Create(ctx context.Context, _a1 *models.Project) (int64, error) { + ret := _m.Called(ctx, _a1) + + var r0 int64 + if rf, ok := ret.Get(0).(func(context.Context, *models.Project) int64); ok { + r0 = rf(ctx, _a1) + } else { + r0 = ret.Get(0).(int64) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *models.Project) error); ok { + r1 = rf(ctx, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Delete provides a mock function with given fields: ctx, id +func (_m *Controller) Delete(ctx context.Context, id int64) error { + ret := _m.Called(ctx, id) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // Get provides a mock function with given fields: ctx, projectID, options func (_m *Controller) Get(ctx context.Context, projectID int64, options ...project.Option) (*models.Project, error) { _va := make([]interface{}, len(options)) diff --git a/src/testing/pkg/pkg.go b/src/testing/pkg/pkg.go index 3e93d175e..5fc20fc3f 100644 --- a/src/testing/pkg/pkg.go +++ b/src/testing/pkg/pkg.go @@ -16,9 +16,13 @@ package pkg //go:generate mockery --case snake --dir ../../pkg/blob --name Manager --output ./blob --outpkg blob //go:generate mockery --case snake --dir ../../vendor/github.com/docker/distribution --name Manifest --output ./distribution --outpkg distribution +//go:generate mockery --case snake --dir ../../pkg/project --name Manager --output ./project --outpkg project +//go:generate mockery --case snake --dir ../../pkg/project/metadata --name Manager --output ./project/metadata --outpkg metadata //go:generate mockery --case snake --dir ../../pkg/quota --name Manager --output ./quota --outpkg quota //go:generate mockery --case snake --dir ../../pkg/quota/driver --name Driver --output ./quota/driver --outpkg driver +//go:generate mockery --case snake --dir ../../pkg/scan/allowlist --name Manager --output ./scan/allowlist --outpkg allowlist //go:generate mockery --case snake --dir ../../pkg/scan/report --name Manager --output ./scan/report --outpkg report //go:generate mockery --case snake --dir ../../pkg/scan/rest/v1 --all --output ./scan/rest/v1 --outpkg v1 //go:generate mockery --case snake --dir ../../pkg/scan/scanner --all --output ./scan/scanner --outpkg scanner //go:generate mockery --case snake --dir ../../pkg/scheduler --name Scheduler --output ./scheduler --outpkg scheduler +//go:generate mockery --case snake --dir ../../pkg/user --name Manager --output ./user --outpkg user diff --git a/src/testing/pkg/project/manager.go b/src/testing/pkg/project/manager.go index e15bb96e7..14ef4dd50 100644 --- a/src/testing/pkg/project/manager.go +++ b/src/testing/pkg/project/manager.go @@ -1,36 +1,84 @@ -// Copyright Project Harbor Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// Code generated by mockery v2.1.0. DO NOT EDIT. package project import ( - "github.com/goharbor/harbor/src/common/models" - "github.com/stretchr/testify/mock" + context "context" + + models "github.com/goharbor/harbor/src/common/models" + mock "github.com/stretchr/testify/mock" + + q "github.com/goharbor/harbor/src/lib/q" ) -// FakeManager is an autogenerated mock type for the FakeManager type -type FakeManager struct { +// Manager is an autogenerated mock type for the Manager type +type Manager struct { mock.Mock } -// Get provides a mock function with given fields: _a0 -func (_m *FakeManager) Get(_a0 interface{}) (*models.Project, error) { - ret := _m.Called(_a0) +// Count provides a mock function with given fields: ctx, query +func (_m *Manager) Count(ctx context.Context, query *q.Query) (int64, error) { + ret := _m.Called(ctx, query) + + var r0 int64 + if rf, ok := ret.Get(0).(func(context.Context, *q.Query) int64); ok { + r0 = rf(ctx, query) + } else { + r0 = ret.Get(0).(int64) + } + + 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 +} + +// Create provides a mock function with given fields: ctx, _a1 +func (_m *Manager) Create(ctx context.Context, _a1 *models.Project) (int64, error) { + ret := _m.Called(ctx, _a1) + + var r0 int64 + if rf, ok := ret.Get(0).(func(context.Context, *models.Project) int64); ok { + r0 = rf(ctx, _a1) + } else { + r0 = ret.Get(0).(int64) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *models.Project) error); ok { + r1 = rf(ctx, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Delete provides a mock function with given fields: ctx, id +func (_m *Manager) Delete(ctx context.Context, id int64) error { + ret := _m.Called(ctx, id) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Get provides a mock function with given fields: ctx, idOrName +func (_m *Manager) Get(ctx context.Context, idOrName interface{}) (*models.Project, error) { + ret := _m.Called(ctx, idOrName) var r0 *models.Project - if rf, ok := ret.Get(0).(func(interface{}) *models.Project); ok { - r0 = rf(_a0) + if rf, ok := ret.Get(0).(func(context.Context, interface{}) *models.Project); ok { + r0 = rf(ctx, idOrName) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*models.Project) @@ -38,8 +86,8 @@ func (_m *FakeManager) Get(_a0 interface{}) (*models.Project, error) { } var r1 error - if rf, ok := ret.Get(1).(func(interface{}) error); ok { - r1 = rf(_a0) + if rf, ok := ret.Get(1).(func(context.Context, interface{}) error); ok { + r1 = rf(ctx, idOrName) } else { r1 = ret.Error(1) } @@ -47,19 +95,20 @@ func (_m *FakeManager) Get(_a0 interface{}) (*models.Project, error) { return r0, r1 } -// List provides a mock function with given fields: _a0 -func (_m *FakeManager) List(_a0 ...*models.ProjectQueryParam) ([]*models.Project, error) { - _va := make([]interface{}, len(_a0)) - for _i := range _a0 { - _va[_i] = _a0[_i] +// List provides a mock function with given fields: ctx, query +func (_m *Manager) List(ctx context.Context, query ...*models.ProjectQueryParam) ([]*models.Project, error) { + _va := make([]interface{}, len(query)) + for _i := range query { + _va[_i] = query[_i] } var _ca []interface{} + _ca = append(_ca, ctx) _ca = append(_ca, _va...) ret := _m.Called(_ca...) var r0 []*models.Project - if rf, ok := ret.Get(0).(func(...*models.ProjectQueryParam) []*models.Project); ok { - r0 = rf(_a0...) + if rf, ok := ret.Get(0).(func(context.Context, ...*models.ProjectQueryParam) []*models.Project); ok { + r0 = rf(ctx, query...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*models.Project) @@ -67,8 +116,8 @@ func (_m *FakeManager) List(_a0 ...*models.ProjectQueryParam) ([]*models.Project } var r1 error - if rf, ok := ret.Get(1).(func(...*models.ProjectQueryParam) error); ok { - r1 = rf(_a0...) + if rf, ok := ret.Get(1).(func(context.Context, ...*models.ProjectQueryParam) error); ok { + r1 = rf(ctx, query...) } else { r1 = ret.Error(1) } diff --git a/src/testing/pkg/project/metadata/manager.go b/src/testing/pkg/project/metadata/manager.go new file mode 100644 index 000000000..9c51f2823 --- /dev/null +++ b/src/testing/pkg/project/metadata/manager.go @@ -0,0 +1,118 @@ +// Code generated by mockery v2.1.0. DO NOT EDIT. + +package metadata + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + models "github.com/goharbor/harbor/src/common/models" +) + +// Manager is an autogenerated mock type for the Manager type +type Manager struct { + mock.Mock +} + +// Add provides a mock function with given fields: ctx, projectID, meta +func (_m *Manager) Add(ctx context.Context, projectID int64, meta map[string]string) error { + ret := _m.Called(ctx, projectID, meta) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64, map[string]string) error); ok { + r0 = rf(ctx, projectID, meta) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Delete provides a mock function with given fields: ctx, projectID, meta +func (_m *Manager) Delete(ctx context.Context, projectID int64, meta ...string) error { + _va := make([]interface{}, len(meta)) + for _i := range meta { + _va[_i] = meta[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, projectID) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64, ...string) error); ok { + r0 = rf(ctx, projectID, meta...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Get provides a mock function with given fields: ctx, projectID, meta +func (_m *Manager) Get(ctx context.Context, projectID int64, meta ...string) (map[string]string, error) { + _va := make([]interface{}, len(meta)) + for _i := range meta { + _va[_i] = meta[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, projectID) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 map[string]string + if rf, ok := ret.Get(0).(func(context.Context, int64, ...string) map[string]string); ok { + r0 = rf(ctx, projectID, meta...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]string) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int64, ...string) error); ok { + r1 = rf(ctx, projectID, meta...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// List provides a mock function with given fields: ctx, name, value +func (_m *Manager) List(ctx context.Context, name string, value string) ([]*models.ProjectMetadata, error) { + ret := _m.Called(ctx, name, value) + + var r0 []*models.ProjectMetadata + if rf, ok := ret.Get(0).(func(context.Context, string, string) []*models.ProjectMetadata); ok { + r0 = rf(ctx, name, value) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*models.ProjectMetadata) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, name, value) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Update provides a mock function with given fields: ctx, projectID, meta +func (_m *Manager) Update(ctx context.Context, projectID int64, meta map[string]string) error { + ret := _m.Called(ctx, projectID, meta) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64, map[string]string) error); ok { + r0 = rf(ctx, projectID, meta) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/src/testing/pkg/scan/allowlist/manager.go b/src/testing/pkg/scan/allowlist/manager.go new file mode 100644 index 000000000..8dd1af3ee --- /dev/null +++ b/src/testing/pkg/scan/allowlist/manager.go @@ -0,0 +1,101 @@ +// Code generated by mockery v2.1.0. DO NOT EDIT. + +package allowlist + +import ( + models "github.com/goharbor/harbor/src/common/models" + mock "github.com/stretchr/testify/mock" +) + +// Manager is an autogenerated mock type for the Manager type +type Manager struct { + mock.Mock +} + +// CreateEmpty provides a mock function with given fields: projectID +func (_m *Manager) CreateEmpty(projectID int64) error { + ret := _m.Called(projectID) + + var r0 error + if rf, ok := ret.Get(0).(func(int64) error); ok { + r0 = rf(projectID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Get provides a mock function with given fields: projectID +func (_m *Manager) Get(projectID int64) (*models.CVEAllowlist, error) { + ret := _m.Called(projectID) + + var r0 *models.CVEAllowlist + if rf, ok := ret.Get(0).(func(int64) *models.CVEAllowlist); ok { + r0 = rf(projectID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.CVEAllowlist) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(int64) error); ok { + r1 = rf(projectID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetSys provides a mock function with given fields: +func (_m *Manager) GetSys() (*models.CVEAllowlist, error) { + ret := _m.Called() + + var r0 *models.CVEAllowlist + if rf, ok := ret.Get(0).(func() *models.CVEAllowlist); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.CVEAllowlist) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Set provides a mock function with given fields: projectID, list +func (_m *Manager) Set(projectID int64, list models.CVEAllowlist) error { + ret := _m.Called(projectID, list) + + var r0 error + if rf, ok := ret.Get(0).(func(int64, models.CVEAllowlist) error); ok { + r0 = rf(projectID, list) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SetSys provides a mock function with given fields: list +func (_m *Manager) SetSys(list models.CVEAllowlist) error { + ret := _m.Called(list) + + var r0 error + if rf, ok := ret.Get(0).(func(models.CVEAllowlist) error); ok { + r0 = rf(list) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/src/testing/pkg/user/manager.go b/src/testing/pkg/user/manager.go new file mode 100644 index 000000000..141a182a2 --- /dev/null +++ b/src/testing/pkg/user/manager.go @@ -0,0 +1,40 @@ +// Code generated by mockery v2.1.0. DO NOT EDIT. + +package user + +import ( + context "context" + + models "github.com/goharbor/harbor/src/pkg/user/models" + mock "github.com/stretchr/testify/mock" + + q "github.com/goharbor/harbor/src/lib/q" +) + +// Manager is an autogenerated mock type for the Manager type +type Manager struct { + mock.Mock +} + +// List provides a mock function with given fields: ctx, query +func (_m *Manager) List(ctx context.Context, query *q.Query) (models.Users, error) { + ret := _m.Called(ctx, query) + + var r0 models.Users + if rf, ok := ret.Get(0).(func(context.Context, *q.Query) models.Users); ok { + r0 = rf(ctx, query) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(models.Users) + } + } + + 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 +}