Merge branch 'main' into fix/invalid-repo-artifacts-404

This commit is contained in:
Vadim Bauer 2024-08-27 14:24:26 +02:00 committed by GitHub
commit 2651e236d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
103 changed files with 2414 additions and 895 deletions

View File

@ -165,7 +165,6 @@ GOIMAGEBUILD_CORE=$(GOIMAGEBUILDCMD) $(GOFLAGS) ${GOTAGS} --ldflags "-w -s $(COR
GOBUILDPATH_CORE=$(GOBUILDPATHINCONTAINER)/src/core
GOBUILDPATH_JOBSERVICE=$(GOBUILDPATHINCONTAINER)/src/jobservice
GOBUILDPATH_REGISTRYCTL=$(GOBUILDPATHINCONTAINER)/src/registryctl
GOBUILDPATH_MIGRATEPATCH=$(GOBUILDPATHINCONTAINER)/src/cmd/migrate-patch
GOBUILDPATH_STANDALONE_DB_MIGRATOR=$(GOBUILDPATHINCONTAINER)/src/cmd/standalone-db-migrator
GOBUILDPATH_EXPORTER=$(GOBUILDPATHINCONTAINER)/src/cmd/exporter
GOBUILDMAKEPATH=make
@ -182,7 +181,6 @@ JOBSERVICEBINARYPATH=$(BUILDPATH)/$(GOBUILDMAKEPATH_JOBSERVICE)
JOBSERVICEBINARYNAME=harbor_jobservice
REGISTRYCTLBINARYPATH=$(BUILDPATH)/$(GOBUILDMAKEPATH_REGISTRYCTL)
REGISTRYCTLBINARYNAME=harbor_registryctl
MIGRATEPATCHBINARYNAME=migrate-patch
STANDALONE_DB_MIGRATOR_BINARYPATH=$(BUILDPATH)/$(GOBUILDMAKEPATH_STANDALONE_DB_MIGRATOR)
STANDALONE_DB_MIGRATOR_BINARYNAME=migrate
@ -548,7 +546,6 @@ cleanbinary:
if [ -f $(CORE_BINARYPATH)/$(CORE_BINARYNAME) ] ; then rm $(CORE_BINARYPATH)/$(CORE_BINARYNAME) ; fi
if [ -f $(JOBSERVICEBINARYPATH)/$(JOBSERVICEBINARYNAME) ] ; then rm $(JOBSERVICEBINARYPATH)/$(JOBSERVICEBINARYNAME) ; fi
if [ -f $(REGISTRYCTLBINARYPATH)/$(REGISTRYCTLBINARYNAME) ] ; then rm $(REGISTRYCTLBINARYPATH)/$(REGISTRYCTLBINARYNAME) ; fi
if [ -f $(MIGRATEPATCHBINARYPATH)/$(MIGRATEPATCHBINARYNAME) ] ; then rm $(MIGRATEPATCHBINARYPATH)/$(MIGRATEPATCHBINARYNAME) ; fi
rm -rf make/photon/*/binary/
cleanbaseimage:

View File

@ -109,7 +109,7 @@ paths:
operationId: searchLdapUser
summary: Search available ldap users.
description: |
This endpoint searches the available ldap users based on related configuration parameters. Support searched by input ladp configuration, load configuration from the system and specific filter.
This endpoint searches the available ldap users based on related configuration parameters. Support searched by input ldap configuration, load configuration from the system and specific filter.
parameters:
- $ref: '#/parameters/requestId'
- name: username
@ -1548,6 +1548,88 @@ paths:
$ref: '#/responses/409'
'500':
$ref: '#/responses/500'
/projects/{project_name_or_id}/artifacts:
get:
summary: List artifacts
description: List artifacts of the specified project
tags:
- project
operationId: listArtifactsOfProject
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/isResourceName'
- $ref: '#/parameters/projectNameOrId'
- $ref: '#/parameters/query'
- $ref: '#/parameters/sort'
- $ref: '#/parameters/page'
- $ref: '#/parameters/pageSize'
- $ref: '#/parameters/acceptVulnerabilities'
- name: with_tag
in: query
description: Specify whether the tags are included inside the returning artifacts
type: boolean
required: false
default: true
- name: with_label
in: query
description: Specify whether the labels are included inside the returning artifacts
type: boolean
required: false
default: false
- name: with_scan_overview
in: query
description: Specify whether the scan overview is included inside the returning artifacts
type: boolean
required: false
default: false
- name: with_sbom_overview
in: query
description: Specify whether the SBOM overview is included in returning artifacts, when this option is true, the SBOM overview will be included in the response
type: boolean
required: false
default: false
- name: with_immutable_status
in: query
description: Specify whether the immutable status is included inside the tags of the returning artifacts. Only works when setting "with_immutable_status=true"
type: boolean
required: false
default: false
- name: with_accessory
in: query
description: Specify whether the accessories are included of the returning artifacts. Only works when setting "with_accessory=true"
type: boolean
required: false
default: false
- name: latest_in_repository
in: query
description: Specify whether only the latest pushed artifact of each repository is included inside the returning artifacts. Only works when either artifact_type or media_type is included in the query.
type: boolean
required: false
default: false
responses:
'200':
description: Success
headers:
X-Total-Count:
description: The total count of artifacts
type: integer
Link:
description: Link refers to the previous page and next page
type: string
schema:
type: array
items:
$ref: '#/definitions/Artifact'
'400':
$ref: '#/responses/400'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
'/projects/{project_name_or_id}/scanner':
get:
summary: Get project level scanner
@ -6586,6 +6668,9 @@ definitions:
manifest_media_type:
type: string
description: The manifest media type of the artifact
artifact_type:
type: string
description: The artifact_type in the manifest of the artifact
project_id:
type: integer
format: int64
@ -6594,6 +6679,9 @@ definitions:
type: integer
format: int64
description: The ID of the repository that the artifact belongs to
repository_name:
type: string
description: The name of the repository that the artifact belongs to
digest:
type: string
description: The digest of the artifact
@ -7252,6 +7340,10 @@ definitions:
type: string
description: 'The ID of the tag retention policy for the project'
x-nullable: true
proxy_speed_kb:
type: string
description: 'The bandwidth limit of proxy cache, in Kbps (kilobits per second). It limits the communication between Harbor and the upstream registry, not the client and the Harbor.'
x-nullable: true
ProjectSummary:
type: object
properties:
@ -7754,6 +7846,9 @@ definitions:
type: array
items:
$ref: '#/definitions/RobotPermission'
creator:
type: string
description: The creator of the robot
creation_time:
type: string
format: date-time
@ -8897,6 +8992,9 @@ definitions:
ldap_group_search_scope:
$ref: '#/definitions/IntegerConfigItem'
description: The scope to search ldap group. ''0-LDAP_SCOPE_BASE, 1-LDAP_SCOPE_ONELEVEL, 2-LDAP_SCOPE_SUBTREE''
ldap_group_attach_parallel:
$ref: '#/definitions/BoolConfigItem'
description: Attach LDAP user group information in parallel.
ldap_scope:
$ref: '#/definitions/IntegerConfigItem'
description: The scope to search ldap users,'0-LDAP_SCOPE_BASE, 1-LDAP_SCOPE_ONELEVEL, 2-LDAP_SCOPE_SUBTREE'
@ -9087,6 +9185,11 @@ definitions:
description: The scope to search ldap group. ''0-LDAP_SCOPE_BASE, 1-LDAP_SCOPE_ONELEVEL, 2-LDAP_SCOPE_SUBTREE''
x-omitempty: true
x-isnullable: true
ldap_group_attach_parallel:
type: boolean
description: Attach LDAP user group information in parallel, the parallel worker count is 5
x-omitempty: true
x-isnullable: true
ldap_scope:
type: integer
description: The scope to search ldap users,'0-LDAP_SCOPE_BASE, 1-LDAP_SCOPE_ONELEVEL, 2-LDAP_SCOPE_SUBTREE'

View File

@ -0,0 +1,5 @@
/*
Add new column creator for robot table to add a new column to record the creator of the robot
*/
ALTER TABLE robot ADD COLUMN IF NOT EXISTS creator varchar(255);
UPDATE robot SET creator = 'unknown' WHERE creator IS NULL;

View File

@ -1,4 +1,3 @@
version: '2.3'
services:
log:
image: goharbor/harbor-log:{{version}}

View File

@ -101,6 +101,9 @@ http {
proxy_buffering off;
proxy_request_buffering off;
proxy_send_timeout 900;
proxy_read_timeout 900;
}
location /api/ {

View File

@ -9,6 +9,17 @@ packages:
Controller:
config:
dir: testing/controller/artifact
github.com/goharbor/harbor/src/controller/artifact/processor:
interfaces:
Processor:
config:
dir: testing/pkg/processor
github.com/goharbor/harbor/src/controller/artifact/annotation:
interfaces:
Parser:
config:
dir: testing/pkg/parser
outpkg: parser
github.com/goharbor/harbor/src/controller/blob:
interfaces:
Controller:
@ -188,6 +199,11 @@ packages:
Manager:
config:
dir: testing/pkg/artifact
github.com/goharbor/harbor/src/pkg/artifactrash:
interfaces:
Manager:
config:
dir: testing/pkg/artifactrash
github.com/goharbor/harbor/src/pkg/blob:
interfaces:
Manager:
@ -218,6 +234,11 @@ packages:
Handler:
config:
dir: testing/pkg/scan
github.com/goharbor/harbor/src/pkg/scan/postprocessors:
interfaces:
NativeScanReportConverter:
config:
dir: testing/pkg/scan/postprocessors
github.com/goharbor/harbor/src/pkg/scan/report:
interfaces:
Manager:
@ -238,7 +259,7 @@ packages:
dir: pkg/scheduler
outpkg: scheduler
mockname: mockDAO
filename: mock_dao_test.go
filename: mock_dao_test.go
inpackage: True
Scheduler:
config:
@ -342,6 +363,14 @@ packages:
DAO:
config:
dir: testing/pkg/immutable/dao
github.com/goharbor/harbor/src/pkg/immutable/match:
interfaces:
ImmutableTagMatcher:
config:
dir: testing/pkg/immutable
filename: matcher.go
outpkg: immutable
mockname: FakeMatcher
github.com/goharbor/harbor/src/pkg/ldap:
interfaces:
Manager:
@ -505,20 +534,36 @@ packages:
Manager:
config:
dir: testing/pkg/securityhub
github.com/goharbor/harbor/src/pkg/tag:
interfaces:
Manager:
config:
dir: testing/pkg/tag
github.com/goharbor/harbor/src/pkg/p2p/preheat/policy:
interfaces:
Manager:
config:
dir: testing/pkg/p2p/preheat/policy
github.com/goharbor/harbor/src/pkg/p2p/preheat/instance:
interfaces:
Manager:
config:
dir: testing/pkg/p2p/preheat/instance
github.com/goharbor/harbor/src/pkg/chart:
interfaces:
Operator:
config:
dir: testing/pkg/chart
# registryctl related mocks
github.com/goharbor/harbor/src/registryctl/client:
interfaces:
Client:
config:
dir: testing/registryctl
outpkg: registryctl
# remote interfaces
github.com/docker/distribution:
interfaces:
Manifest:
config:
dir: testing/pkg/distribution

View File

@ -1,6 +0,0 @@
# Migrate Patch
This is a simple program to fix the breakage that was introduced by migrate in notary.
## Usage
```sh
patch -database <db_url>
```

View File

@ -1,88 +0,0 @@
// 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 main
import (
"database/sql"
"flag"
"log"
"strings"
"time"
_ "github.com/jackc/pgx/v4/stdlib" // registry pgx driver
)
var dbURL string
const pgSQLAlterStmt string = `ALTER TABLE schema_migrations ADD COLUMN "dirty" boolean NOT NULL DEFAULT false`
const pgSQLCheckColStmt string = `SELECT T1.C1, T2.C2 FROM
(SELECT COUNT(*) AS C1 FROM information_schema.tables WHERE table_name='schema_migrations') T1,
(SELECT COUNT(*) AS C2 FROM information_schema.columns WHERE table_name='schema_migrations' and column_name='dirty') T2`
const pgSQLDelRows string = `DELETE FROM schema_migrations t WHERE t.version < ( SELECT MAX(version) FROM schema_migrations )`
func init() {
urlUsage := `The URL to the target database (driver://url). Currently it only supports postgres`
flag.StringVar(&dbURL, "database", "", urlUsage)
}
func main() {
flag.Parse()
log.Printf("Updating database.")
if !strings.HasPrefix(dbURL, "postgres://") {
log.Fatalf("Invalid URL: '%s'\n", dbURL)
}
db, err := sql.Open("pgx", dbURL)
if err != nil {
log.Fatalf("Failed to connect to Database, error: %v\n", err)
}
defer db.Close()
c := make(chan struct{})
go func() {
defer close(c)
err := db.Ping()
for ; err != nil; err = db.Ping() {
log.Println("Failed to Ping DB, sleep for 1 second.")
time.Sleep(1 * time.Second)
}
}()
select {
case <-c:
case <-time.After(30 * time.Second):
log.Fatal("Failed to connect DB after 30 seconds, time out. \n")
}
row := db.QueryRow(pgSQLCheckColStmt)
var tblCount, colCount int
if err := row.Scan(&tblCount, &colCount); err != nil {
log.Fatalf("Failed to check schema_migrations table, error: %v \n", err)
}
if tblCount == 0 {
log.Println("schema_migrations table does not exist, skip.")
return
}
if colCount > 0 {
log.Println("schema_migrations table does not require update, skip.")
return
}
if _, err := db.Exec(pgSQLDelRows); err != nil {
log.Fatalf("Failed to clean up table, error: %v", err)
}
if _, err := db.Exec(pgSQLAlterStmt); err != nil {
log.Fatalf("Failed to update database, error: %v \n", err)
}
log.Println("Done updating database.")
}

View File

@ -134,6 +134,7 @@ const (
OIDCGroupType = 3
LDAPGroupAdminDn = "ldap_group_admin_dn"
LDAPGroupMembershipAttribute = "ldap_group_membership_attribute"
LDAPGroupAttachParallel = "ldap_group_attach_parallel"
DefaultRegistryControllerEndpoint = "http://registryctl:8080"
DefaultPortalURL = "http://portal:8080"
DefaultRegistryCtlURL = "http://registryctl:8080"

View File

@ -118,6 +118,8 @@ type Controller interface {
Walk(ctx context.Context, root *Artifact, walkFn func(*Artifact) error, option *Option) error
// HasUnscannableLayer check artifact with digest if has unscannable layer
HasUnscannableLayer(ctx context.Context, dgst string) (bool, error)
// ListWithLatest list the artifacts when the latest_in_repository in the query was set
ListWithLatest(ctx context.Context, query *q.Query, option *Option) (artifacts []*Artifact, err error)
}
// NewController creates an instance of the default artifact controller
@ -171,16 +173,18 @@ func (c *controller) Ensure(ctx context.Context, repository, digest string, opti
}
}
}
// fire event
e := &metadata.PushArtifactEventMetadata{
Ctx: ctx,
Artifact: artifact,
}
if created {
// fire event for create
e := &metadata.PushArtifactEventMetadata{
Ctx: ctx,
Artifact: artifact,
}
if option != nil && len(option.Tags) > 0 {
e.Tag = option.Tags[0]
if option != nil && len(option.Tags) > 0 {
e.Tag = option.Tags[0]
}
notification.AddEvent(ctx, e)
}
notification.AddEvent(ctx, e)
return created, artifact.ID, nil
}
@ -782,3 +786,16 @@ func (c *controller) HasUnscannableLayer(ctx context.Context, dgst string) (bool
}
return false, nil
}
// ListWithLatest ...
func (c *controller) ListWithLatest(ctx context.Context, query *q.Query, option *Option) (artifacts []*Artifact, err error) {
arts, err := c.artMgr.ListWithLatest(ctx, query)
if err != nil {
return nil, err
}
var res []*Artifact
for _, art := range arts {
res = append(res, c.assembleArtifact(ctx, art, option))
}
return res, nil
}

View File

@ -67,7 +67,7 @@ type controllerTestSuite struct {
ctl *controller
repoMgr *repotesting.Manager
artMgr *arttesting.Manager
artrashMgr *artrashtesting.FakeManager
artrashMgr *artrashtesting.Manager
blobMgr *blob.Manager
tagCtl *tagtesting.FakeController
labelMgr *label.Manager
@ -80,7 +80,7 @@ type controllerTestSuite struct {
func (c *controllerTestSuite) SetupTest() {
c.repoMgr = &repotesting.Manager{}
c.artMgr = &arttesting.Manager{}
c.artrashMgr = &artrashtesting.FakeManager{}
c.artrashMgr = &artrashtesting.Manager{}
c.blobMgr = &blob.Manager{}
c.tagCtl = &tagtesting.FakeController{}
c.labelMgr = &label.Manager{}
@ -323,6 +323,44 @@ func (c *controllerTestSuite) TestList() {
c.Equal(0, len(artifacts[0].Accessories))
}
func (c *controllerTestSuite) TestListWithLatest() {
query := &q.Query{}
option := &Option{
WithTag: true,
WithAccessory: true,
}
c.artMgr.On("ListWithLatest", mock.Anything, mock.Anything).Return([]*artifact.Artifact{
{
ID: 1,
RepositoryID: 1,
},
}, nil)
c.tagCtl.On("List").Return([]*tag.Tag{
{
Tag: model_tag.Tag{
ID: 1,
RepositoryID: 1,
ArtifactID: 1,
Name: "latest",
},
},
}, nil)
c.repoMgr.On("Get", mock.Anything, mock.Anything).Return(&repomodel.RepoRecord{
Name: "library/hello-world",
}, nil)
c.repoMgr.On("List", mock.Anything, mock.Anything).Return([]*repomodel.RepoRecord{
{RepositoryID: 1, Name: "library/hello-world"},
}, nil)
c.accMgr.On("List", mock.Anything, mock.Anything).Return([]accessorymodel.Accessory{}, nil)
artifacts, err := c.ctl.ListWithLatest(nil, query, option)
c.Require().Nil(err)
c.Require().Len(artifacts, 1)
c.Equal(int64(1), artifacts[0].ID)
c.Require().Len(artifacts[0].Tags, 1)
c.Equal(int64(1), artifacts[0].Tags[0].ID)
c.Equal(0, len(artifacts[0].Accessories))
}
func (c *controllerTestSuite) TestGet() {
c.artMgr.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&artifact.Artifact{
ID: 1,
@ -476,7 +514,7 @@ func (c *controllerTestSuite) TestDeleteDeeply() {
},
}, nil)
c.repoMgr.On("Get", mock.Anything, mock.Anything).Return(&repomodel.RepoRecord{}, nil)
c.artrashMgr.On("Create").Return(0, nil)
c.artrashMgr.On("Create", mock.Anything, mock.Anything).Return(int64(0), nil)
c.accMgr.On("List", mock.Anything, mock.Anything).Return([]accessorymodel.Accessory{}, nil)
err = c.ctl.deleteDeeply(orm.NewContext(nil, &ormtesting.FakeOrmer{}), 1, false, false)
c.Require().Nil(err)
@ -534,7 +572,7 @@ func (c *controllerTestSuite) TestDeleteDeeply() {
c.blobMgr.On("List", mock.Anything, mock.Anything).Return(nil, nil)
c.blobMgr.On("CleanupAssociationsForProject", mock.Anything, mock.Anything, mock.Anything).Return(nil)
c.repoMgr.On("Get", mock.Anything, mock.Anything).Return(&repomodel.RepoRecord{}, nil)
c.artrashMgr.On("Create").Return(0, nil)
c.artrashMgr.On("Create", mock.Anything, mock.Anything).Return(int64(0), nil)
err = c.ctl.deleteDeeply(orm.NewContext(nil, &ormtesting.FakeOrmer{}), 1, true, true)
c.Require().Nil(err)

View File

@ -102,8 +102,9 @@ type AdditionLink struct {
// Option is used to specify the properties returned when listing/getting artifacts
type Option struct {
WithTag bool
TagOption *tag.Option // only works when WithTag is set to true
WithLabel bool
WithAccessory bool
WithTag bool
TagOption *tag.Option // only works when WithTag is set to true
WithLabel bool
WithAccessory bool
LatestInRepository bool
}

View File

@ -64,12 +64,12 @@ type processorTestSuite struct {
suite.Suite
processor *processor
regCli *registry.Client
chartOptr *chart.FakeOpertaor
chartOptr *chart.Operator
}
func (p *processorTestSuite) SetupTest() {
p.regCli = &registry.Client{}
p.chartOptr = &chart.FakeOpertaor{}
p.chartOptr = &chart.Operator{}
p.processor = &processor{
chartOperator: p.chartOptr,
}
@ -106,7 +106,7 @@ func (p *processorTestSuite) TestAbstractAddition() {
p.Require().Nil(err)
p.regCli.On("PullManifest", mock.Anything, mock.Anything).Return(manifest, "", nil)
p.regCli.On("PullBlob", mock.Anything, mock.Anything).Return(int64(0), io.NopCloser(strings.NewReader(chartYaml)), nil)
p.chartOptr.On("GetDetails").Return(chartDetails, nil)
p.chartOptr.On("GetDetails", mock.Anything).Return(chartDetails, nil)
// values.yaml
addition, err := p.processor.AbstractAddition(nil, artifact, AdditionTypeValues)

View File

@ -45,7 +45,8 @@ func (h *Handler) Handle(ctx context.Context, value interface{}) error {
switch v := value.(type) {
case *event.PushArtifactEvent, *event.DeleteArtifactEvent,
*event.DeleteRepositoryEvent, *event.CreateProjectEvent, *event.DeleteProjectEvent,
*event.DeleteTagEvent, *event.CreateTagEvent:
*event.DeleteTagEvent, *event.CreateTagEvent,
*event.CreateRobotEvent, *event.DeleteRobotEvent:
addAuditLog = true
case *event.PullArtifactEvent:
addAuditLog = !config.PullAuditLogDisable(ctx)

View File

@ -65,6 +65,8 @@ func init() {
_ = notifier.Subscribe(event.TopicDeleteRepository, &auditlog.Handler{})
_ = notifier.Subscribe(event.TopicCreateTag, &auditlog.Handler{})
_ = notifier.Subscribe(event.TopicDeleteTag, &auditlog.Handler{})
_ = notifier.Subscribe(event.TopicCreateRobot, &auditlog.Handler{})
_ = notifier.Subscribe(event.TopicDeleteRobot, &auditlog.Handler{})
// internal
_ = notifier.Subscribe(event.TopicPullArtifact, &internal.ArtifactEventHandler{})

View File

@ -0,0 +1,73 @@
// 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"
"fmt"
"time"
"github.com/goharbor/harbor/src/common/security"
event2 "github.com/goharbor/harbor/src/controller/event"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/pkg/notifier/event"
"github.com/goharbor/harbor/src/pkg/robot/model"
)
// CreateRobotEventMetadata is the metadata from which the create robot event can be resolved
type CreateRobotEventMetadata struct {
Ctx context.Context
Robot *model.Robot
}
// Resolve to the event from the metadata
func (c *CreateRobotEventMetadata) Resolve(event *event.Event) error {
data := &event2.CreateRobotEvent{
EventType: event2.TopicCreateRobot,
Robot: c.Robot,
OccurAt: time.Now(),
}
cx, exist := security.FromContext(c.Ctx)
if exist {
data.Operator = cx.GetUsername()
}
data.Robot.Name = fmt.Sprintf("%s%s", config.RobotPrefix(c.Ctx), data.Robot.Name)
event.Topic = event2.TopicCreateRobot
event.Data = data
return nil
}
// DeleteRobotEventMetadata is the metadata from which the delete robot event can be resolved
type DeleteRobotEventMetadata struct {
Ctx context.Context
Robot *model.Robot
}
// Resolve to the event from the metadata
func (d *DeleteRobotEventMetadata) Resolve(event *event.Event) error {
data := &event2.DeleteRobotEvent{
EventType: event2.TopicDeleteRobot,
Robot: d.Robot,
OccurAt: time.Now(),
}
cx, exist := security.FromContext(d.Ctx)
if exist {
data.Operator = cx.GetUsername()
}
data.Robot.Name = fmt.Sprintf("%s%s", config.RobotPrefix(d.Ctx), data.Robot.Name)
event.Topic = event2.TopicDeleteRobot
event.Data = data
return nil
}

View File

@ -0,0 +1,83 @@
// 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"
"testing"
"github.com/stretchr/testify/suite"
"github.com/goharbor/harbor/src/common"
event2 "github.com/goharbor/harbor/src/controller/event"
"github.com/goharbor/harbor/src/lib/config"
_ "github.com/goharbor/harbor/src/pkg/config/inmemory"
"github.com/goharbor/harbor/src/pkg/notifier/event"
"github.com/goharbor/harbor/src/pkg/robot/model"
)
type robotEventTestSuite struct {
suite.Suite
}
func (t *tagEventTestSuite) TestResolveOfCreateRobotEventMetadata() {
cfg := map[string]interface{}{
common.RobotPrefix: "robot$",
}
config.InitWithSettings(cfg)
e := &event.Event{}
metadata := &CreateRobotEventMetadata{
Ctx: context.Background(),
Robot: &model.Robot{
ID: 1,
Name: "test",
},
}
err := metadata.Resolve(e)
t.Require().Nil(err)
t.Equal(event2.TopicCreateRobot, e.Topic)
t.Require().NotNil(e.Data)
data, ok := e.Data.(*event2.CreateRobotEvent)
t.Require().True(ok)
t.Equal(int64(1), data.Robot.ID)
t.Equal("robot$test", data.Robot.Name)
}
func (t *tagEventTestSuite) TestResolveOfDeleteRobotEventMetadata() {
cfg := map[string]interface{}{
common.RobotPrefix: "robot$",
}
config.InitWithSettings(cfg)
e := &event.Event{}
metadata := &DeleteRobotEventMetadata{
Ctx: context.Background(),
Robot: &model.Robot{
ID: 1,
},
}
err := metadata.Resolve(e)
t.Require().Nil(err)
t.Equal(event2.TopicDeleteRobot, e.Topic)
t.Require().NotNil(e.Data)
data, ok := e.Data.(*event2.DeleteRobotEvent)
t.Require().True(ok)
t.Equal(int64(1), data.Robot.ID)
}
func TestRobotEventTestSuite(t *testing.T) {
suite.Run(t, &robotEventTestSuite{})
}

View File

@ -23,6 +23,7 @@ import (
"github.com/goharbor/harbor/src/pkg/artifact"
"github.com/goharbor/harbor/src/pkg/audit/model"
proModels "github.com/goharbor/harbor/src/pkg/project/models"
robotModel "github.com/goharbor/harbor/src/pkg/robot/model"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
)
@ -47,6 +48,8 @@ const (
TopicReplication = "REPLICATION"
TopicArtifactLabeled = "ARTIFACT_LABELED"
TopicTagRetention = "TAG_RETENTION"
TopicCreateRobot = "CREATE_ROBOT"
TopicDeleteRobot = "DELETE_ROBOT"
)
// CreateProjectEvent is the creating project event
@ -369,3 +372,53 @@ func (r *RetentionEvent) String() string {
return fmt.Sprintf("TaskID-%d Status-%s Deleted-%s OccurAt-%s",
r.TaskID, r.Status, candidates, r.OccurAt.Format("2006-01-02 15:04:05"))
}
// CreateRobotEvent is the creating robot event
type CreateRobotEvent struct {
EventType string
Robot *robotModel.Robot
Operator string
OccurAt time.Time
}
// ResolveToAuditLog ...
func (c *CreateRobotEvent) ResolveToAuditLog() (*model.AuditLog, error) {
auditLog := &model.AuditLog{
ProjectID: c.Robot.ProjectID,
OpTime: c.OccurAt,
Operation: rbac.ActionCreate.String(),
Username: c.Operator,
ResourceType: "robot",
Resource: c.Robot.Name}
return auditLog, nil
}
func (c *CreateRobotEvent) String() string {
return fmt.Sprintf("Name-%s Operator-%s OccurAt-%s",
c.Robot.Name, c.Operator, c.OccurAt.Format("2006-01-02 15:04:05"))
}
// DeleteRobotEvent is the deleting robot event
type DeleteRobotEvent struct {
EventType string
Robot *robotModel.Robot
Operator string
OccurAt time.Time
}
// ResolveToAuditLog ...
func (c *DeleteRobotEvent) ResolveToAuditLog() (*model.AuditLog, error) {
auditLog := &model.AuditLog{
ProjectID: c.Robot.ProjectID,
OpTime: c.OccurAt,
Operation: rbac.ActionDelete.String(),
Username: c.Operator,
ResourceType: "robot",
Resource: c.Robot.Name}
return auditLog, nil
}
func (c *DeleteRobotEvent) String() string {
return fmt.Sprintf("Name-%s Operator-%s OccurAt-%s",
c.Robot.Name, c.Operator, c.OccurAt.Format("2006-01-02 15:04:05"))
}

View File

@ -31,8 +31,8 @@ type preheatSuite struct {
suite.Suite
ctx context.Context
controller Controller
fakeInstanceMgr *instance.FakeManager
fakePolicyMgr *pmocks.FakeManager
fakeInstanceMgr *instance.Manager
fakePolicyMgr *pmocks.Manager
fakeScheduler *smocks.Scheduler
mockInstanceServer *httptest.Server
fakeExecutionMgr *tmocks.ExecutionManager
@ -40,8 +40,8 @@ type preheatSuite struct {
func TestPreheatSuite(t *testing.T) {
t.Log("Start TestPreheatSuite")
fakeInstanceMgr := &instance.FakeManager{}
fakePolicyMgr := &pmocks.FakeManager{}
fakeInstanceMgr := &instance.Manager{}
fakePolicyMgr := &pmocks.Manager{}
fakeScheduler := &smocks.Scheduler{}
fakeExecutionMgr := &tmocks.ExecutionManager{}

View File

@ -70,7 +70,7 @@ func (suite *EnforcerTestSuite) SetupSuite() {
suite.server.StartTLS()
fakePolicies := mockPolicies()
fakePolicyManager := &policy.FakeManager{}
fakePolicyManager := &policy.Manager{}
fakePolicyManager.On("Get",
context.TODO(),
mock.AnythingOfType("int64")).
@ -130,7 +130,7 @@ func (suite *EnforcerTestSuite) SetupSuite() {
},
}, nil)
fakeInstanceMgr := &instance.FakeManager{}
fakeInstanceMgr := &instance.Manager{}
fakeInstanceMgr.On("Get",
context.TODO(),
mock.AnythingOfType("int64"),

View File

@ -264,7 +264,7 @@ func (c *controller) HeadManifest(_ context.Context, art lib.ArtifactInfo, remot
func (c *controller) ProxyBlob(ctx context.Context, p *proModels.Project, art lib.ArtifactInfo) (int64, io.ReadCloser, error) {
remoteRepo := getRemoteRepo(art)
log.Debugf("The blob doesn't exist, proxy the request to the target server, url:%v", remoteRepo)
rHelper, err := NewRemoteHelper(ctx, p.RegistryID)
rHelper, err := NewRemoteHelper(ctx, p.RegistryID, WithSpeed(p.ProxyCacheSpeed()))
if err != nil {
return 0, nil, err
}

View File

@ -0,0 +1,37 @@
// 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 proxy
type Option func(*Options)
type Options struct {
// Speed is the data transfer speed for proxy cache from Harbor to upstream registry, no limit by default.
Speed int32
}
func NewOptions(opts ...Option) *Options {
o := &Options{}
for _, opt := range opts {
opt(o)
}
return o
}
func WithSpeed(speed int32) Option {
return func(o *Options) {
o.Speed = speed
}
}

View File

@ -0,0 +1,33 @@
// 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 proxy
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewOptions(t *testing.T) {
// test default options
o := NewOptions()
assert.Equal(t, int32(0), o.Speed)
// test with options
// with speed
withSpeed := WithSpeed(1024)
o = NewOptions(withSpeed)
assert.Equal(t, int32(1024), o.Speed)
}

View File

@ -21,6 +21,7 @@ import (
"github.com/docker/distribution"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/pkg/reg"
"github.com/goharbor/harbor/src/pkg/reg/adapter"
"github.com/goharbor/harbor/src/pkg/reg/model"
@ -43,13 +44,16 @@ type remoteHelper struct {
regID int64
registry adapter.ArtifactRegistry
registryMgr reg.Manager
opts *Options
}
// NewRemoteHelper create a remote interface
func NewRemoteHelper(ctx context.Context, regID int64) (RemoteInterface, error) {
func NewRemoteHelper(ctx context.Context, regID int64, opts ...Option) (RemoteInterface, error) {
r := &remoteHelper{
regID: regID,
registryMgr: reg.Mgr}
registryMgr: reg.Mgr,
opts: NewOptions(opts...),
}
if err := r.init(ctx); err != nil {
return nil, err
}
@ -83,7 +87,14 @@ func (r *remoteHelper) init(ctx context.Context) error {
}
func (r *remoteHelper) BlobReader(repo, dig string) (int64, io.ReadCloser, error) {
return r.registry.PullBlob(repo, dig)
sz, bReader, err := r.registry.PullBlob(repo, dig)
if err != nil {
return 0, nil, err
}
if r.opts != nil && r.opts.Speed > 0 {
bReader = lib.NewReader(bReader, r.opts.Speed)
}
return sz, bReader, err
}
func (r *remoteHelper) Manifest(repo string, ref string) (distribution.Manifest, string, error) {

View File

@ -30,6 +30,7 @@ import (
common_http "github.com/goharbor/harbor/src/common/http"
trans "github.com/goharbor/harbor/src/controller/replication/transfer"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/reg/adapter"
"github.com/goharbor/harbor/src/pkg/reg/model"
@ -380,7 +381,7 @@ func (t *transfer) copyBlobByMonolithic(srcRepo, dstRepo, digest string, sizeFro
return err
}
if speed > 0 {
data = trans.NewReader(data, speed)
data = lib.NewReader(data, speed)
}
defer data.Close()
// get size 0 from PullBlob, use size from distribution.Descriptor instead.
@ -435,7 +436,7 @@ func (t *transfer) copyBlobByChunk(srcRepo, dstRepo, digest string, sizeFromDesc
}
if speed > 0 {
data = trans.NewReader(data, speed)
data = lib.NewReader(data, speed)
}
// failureEnd will only be used for adjusting content range when issue happened during push the chunk.
var failureEnd int64

View File

@ -23,12 +23,14 @@ import (
rbac_project "github.com/goharbor/harbor/src/common/rbac/project"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/controller/event/metadata"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/lib/retry"
"github.com/goharbor/harbor/src/pkg"
"github.com/goharbor/harbor/src/pkg/notification"
"github.com/goharbor/harbor/src/pkg/permission/types"
"github.com/goharbor/harbor/src/pkg/project"
"github.com/goharbor/harbor/src/pkg/rbac"
@ -121,7 +123,8 @@ func (d *controller) Create(ctx context.Context, r *Robot) (int64, string, error
if r.Level == LEVELPROJECT {
name = fmt.Sprintf("%s+%s", r.ProjectName, r.Name)
}
robotID, err := d.robotMgr.Create(ctx, &model.Robot{
rCreate := &model.Robot{
Name: name,
Description: r.Description,
ProjectID: r.ProjectID,
@ -130,7 +133,9 @@ func (d *controller) Create(ctx context.Context, r *Robot) (int64, string, error
Duration: r.Duration,
Salt: salt,
Visible: r.Visible,
})
Creator: r.Creator,
}
robotID, err := d.robotMgr.Create(ctx, rCreate)
if err != nil {
return 0, "", err
}
@ -138,17 +143,31 @@ func (d *controller) Create(ctx context.Context, r *Robot) (int64, string, error
if err := d.createPermission(ctx, r); err != nil {
return 0, "", err
}
// fire event
notification.AddEvent(ctx, &metadata.CreateRobotEventMetadata{
Ctx: ctx,
Robot: rCreate,
})
return robotID, pwd, nil
}
// Delete ...
func (d *controller) Delete(ctx context.Context, id int64) error {
rDelete, err := d.robotMgr.Get(ctx, id)
if err != nil {
return err
}
if err := d.robotMgr.Delete(ctx, id); err != nil {
return err
}
if err := d.rbacMgr.DeletePermissionsByRole(ctx, ROBOTTYPE, id); err != nil {
return err
}
// fire event
notification.AddEvent(ctx, &metadata.DeleteRobotEventMetadata{
Ctx: ctx,
Robot: rDelete,
})
return nil
}

View File

@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/suite"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/security"
"github.com/goharbor/harbor/src/common/utils/test"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/lib/q"
@ -18,6 +19,7 @@ import (
rbac_model "github.com/goharbor/harbor/src/pkg/rbac/model"
"github.com/goharbor/harbor/src/pkg/robot/model"
htesting "github.com/goharbor/harbor/src/testing"
testsec "github.com/goharbor/harbor/src/testing/common/security"
"github.com/goharbor/harbor/src/testing/mock"
"github.com/goharbor/harbor/src/testing/pkg/project"
"github.com/goharbor/harbor/src/testing/pkg/rbac"
@ -102,7 +104,9 @@ func (suite *ControllerTestSuite) TestCreate() {
robotMgr := &robot.Manager{}
c := controller{robotMgr: robotMgr, rbacMgr: rbacMgr, proMgr: projectMgr}
ctx := context.TODO()
secCtx := &testsec.Context{}
secCtx.On("GetUsername").Return("security-context-user")
ctx := security.NewContext(context.Background(), secCtx)
projectMgr.On("Get", mock.Anything, mock.Anything).Return(&proModels.Project{ProjectID: 1, Name: "library"}, nil)
robotMgr.On("Create", mock.Anything, mock.Anything).Return(int64(1), nil)
rbacMgr.On("CreateRbacPolicy", mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil)
@ -145,6 +149,12 @@ func (suite *ControllerTestSuite) TestDelete() {
c := controller{robotMgr: robotMgr, rbacMgr: rbacMgr, proMgr: projectMgr}
ctx := context.TODO()
robotMgr.On("Get", mock.Anything, mock.Anything).Return(&model.Robot{
Name: "library+test",
Description: "test get method",
ProjectID: 1,
Secret: utils.RandStringBytes(10),
}, nil)
robotMgr.On("Delete", mock.Anything, mock.Anything).Return(nil)
rbacMgr.On("DeletePermissionsByRole", mock.Anything, mock.Anything, mock.Anything).Return(nil)

View File

@ -864,6 +864,7 @@ func (bc *basicController) makeRobotAccount(ctx context.Context, projectID int64
Description: "for scan",
ProjectID: projectID,
Duration: -1,
Creator: "harbor-core-for-scan-all",
},
Level: robot.LEVELPROJECT,
Permissions: []*robot.Permission{

View File

@ -82,7 +82,7 @@ type ControllerTestSuite struct {
reportMgr *reporttesting.Manager
ar artifact.Controller
c *basicController
reportConverter *postprocessorstesting.ScanReportV1ToV2Converter
reportConverter *postprocessorstesting.NativeScanReportConverter
cache *mockcache.Cache
}
@ -235,6 +235,7 @@ func (suite *ControllerTestSuite) SetupSuite() {
Description: "for scan",
ProjectID: suite.artifact.ProjectID,
Duration: -1,
Creator: "harbor-core-for-scan-all",
},
Level: robot.LEVELPROJECT,
Permissions: []*robot.Permission{
@ -266,6 +267,7 @@ func (suite *ControllerTestSuite) SetupSuite() {
Description: "for scan",
ProjectID: suite.artifact.ProjectID,
Duration: -1,
Creator: "harbor-core-for-scan-all",
},
Level: "project",
}, nil)
@ -339,7 +341,7 @@ func (suite *ControllerTestSuite) SetupSuite() {
execMgr: suite.execMgr,
taskMgr: suite.taskMgr,
reportConverter: &postprocessorstesting.ScanReportV1ToV2Converter{},
reportConverter: &postprocessorstesting.NativeScanReportConverter{},
cache: func() cache.Cache { return suite.cache },
}
mock.OnAnything(suite.scanHandler, "JobVendorType").Return("IMAGE_SCAN")
@ -486,6 +488,7 @@ func (suite *ControllerTestSuite) TestScanControllerGetReport() {
{ExtraAttrs: suite.makeExtraAttrs(int64(1), "rp-uuid-001")},
}, nil).Once()
mock.OnAnything(suite.accessoryMgr, "List").Return(nil, nil)
mock.OnAnything(suite.c.reportConverter, "FromRelationalSchema").Return("", nil)
rep, err := suite.c.GetReport(ctx, suite.artifact, []string{v1.MimeTypeNativeReport})
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 1, len(rep))

View File

@ -51,7 +51,7 @@ type CallbackTestSuite struct {
scanCtl Controller
taskMgr *tasktesting.Manager
reportConverter *postprocessorstesting.ScanReportV1ToV2Converter
reportConverter *postprocessorstesting.NativeScanReportConverter
}
func (suite *CallbackTestSuite) SetupSuite() {
@ -69,7 +69,7 @@ func (suite *CallbackTestSuite) SetupSuite() {
suite.taskMgr = &tasktesting.Manager{}
taskMgr = suite.taskMgr
suite.reportConverter = &postprocessorstesting.ScanReportV1ToV2Converter{}
suite.reportConverter = &postprocessorstesting.NativeScanReportConverter{}
suite.scanCtl = &basicController{
makeCtx: context.TODO,

View File

@ -44,7 +44,7 @@ type ControllerTestSuite struct {
c *controller
scannerMgr *scannerMock.Manager
secHubMgr *securityMock.Manager
tagMgr *tagMock.FakeManager
tagMgr *tagMock.Manager
}
// TestController is the entry of controller test suite
@ -56,7 +56,7 @@ func TestController(t *testing.T) {
func (suite *ControllerTestSuite) SetupTest() {
suite.secHubMgr = &securityMock.Manager{}
suite.scannerMgr = &scannerMock.Manager{}
suite.tagMgr = &tagMock.FakeManager{}
suite.tagMgr = &tagMock.Manager{}
suite.c = &controller{
secHubMgr: suite.secHubMgr,

View File

@ -18,7 +18,6 @@ import (
"testing"
"time"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
"github.com/goharbor/harbor/src/lib/errors"
@ -27,6 +26,7 @@ import (
_ "github.com/goharbor/harbor/src/pkg/config/inmemory"
"github.com/goharbor/harbor/src/pkg/tag/model/tag"
ormtesting "github.com/goharbor/harbor/src/testing/lib/orm"
"github.com/goharbor/harbor/src/testing/mock"
"github.com/goharbor/harbor/src/testing/pkg/artifact"
"github.com/goharbor/harbor/src/testing/pkg/immutable"
"github.com/goharbor/harbor/src/testing/pkg/repository"
@ -38,14 +38,14 @@ type controllerTestSuite struct {
ctl *controller
repoMgr *repository.Manager
artMgr *artifact.Manager
tagMgr *tagtesting.FakeManager
tagMgr *tagtesting.Manager
immutableMtr *immutable.FakeMatcher
}
func (c *controllerTestSuite) SetupTest() {
c.repoMgr = &repository.Manager{}
c.artMgr = &artifact.Manager{}
c.tagMgr = &tagtesting.FakeManager{}
c.tagMgr = &tagtesting.Manager{}
c.immutableMtr = &immutable.FakeMatcher{}
c.ctl = &controller{
tagMgr: c.tagMgr,
@ -56,7 +56,7 @@ func (c *controllerTestSuite) SetupTest() {
func (c *controllerTestSuite) TestEnsureTag() {
// the tag already exists under the repository and is attached to the artifact
c.tagMgr.On("List").Return([]*tag.Tag{
c.tagMgr.On("List", mock.Anything, mock.Anything).Return([]*tag.Tag{
{
ID: 1,
RepositoryID: 1,
@ -67,7 +67,7 @@ func (c *controllerTestSuite) TestEnsureTag() {
c.artMgr.On("Get", mock.Anything, mock.Anything).Return(&pkg_artifact.Artifact{
ID: 1,
}, nil)
c.immutableMtr.On("Match").Return(false, nil)
mock.OnAnything(c.immutableMtr, "Match").Return(false, nil)
_, err := c.ctl.Ensure(orm.NewContext(nil, &ormtesting.FakeOrmer{}), 1, 1, "latest")
c.Require().Nil(err)
c.tagMgr.AssertExpectations(c.T())
@ -76,7 +76,7 @@ func (c *controllerTestSuite) TestEnsureTag() {
c.SetupTest()
// the tag exists under the repository, but it is attached to other artifact
c.tagMgr.On("List").Return([]*tag.Tag{
c.tagMgr.On("List", mock.Anything, mock.Anything).Return([]*tag.Tag{
{
ID: 1,
RepositoryID: 1,
@ -84,11 +84,11 @@ func (c *controllerTestSuite) TestEnsureTag() {
Name: "latest",
},
}, nil)
c.tagMgr.On("Update").Return(nil)
c.tagMgr.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
c.artMgr.On("Get", mock.Anything, mock.Anything).Return(&pkg_artifact.Artifact{
ID: 1,
}, nil)
c.immutableMtr.On("Match").Return(false, nil)
mock.OnAnything(c.immutableMtr, "Match").Return(false, nil)
_, err = c.ctl.Ensure(orm.NewContext(nil, &ormtesting.FakeOrmer{}), 1, 1, "latest")
c.Require().Nil(err)
c.tagMgr.AssertExpectations(c.T())
@ -97,26 +97,26 @@ func (c *controllerTestSuite) TestEnsureTag() {
c.SetupTest()
// the tag doesn't exist under the repository, create it
c.tagMgr.On("List").Return([]*tag.Tag{}, nil)
c.tagMgr.On("Create").Return(1, nil)
c.tagMgr.On("List", mock.Anything, mock.Anything).Return([]*tag.Tag{}, nil)
c.tagMgr.On("Create", mock.Anything, mock.Anything).Return(int64(1), nil)
c.artMgr.On("Get", mock.Anything, mock.Anything).Return(&pkg_artifact.Artifact{
ID: 1,
}, nil)
c.immutableMtr.On("Match").Return(false, nil)
mock.OnAnything(c.immutableMtr, "Match").Return(false, nil)
_, err = c.ctl.Ensure(orm.NewContext(nil, &ormtesting.FakeOrmer{}), 1, 1, "latest")
c.Require().Nil(err)
c.tagMgr.AssertExpectations(c.T())
}
func (c *controllerTestSuite) TestCount() {
c.tagMgr.On("Count").Return(1, nil)
c.tagMgr.On("Count", mock.Anything, mock.Anything).Return(int64(1), nil)
total, err := c.ctl.Count(nil, nil)
c.Require().Nil(err)
c.Equal(int64(1), total)
}
func (c *controllerTestSuite) TestList() {
c.tagMgr.On("List").Return([]*tag.Tag{
c.tagMgr.On("List", mock.Anything, mock.Anything).Return([]*tag.Tag{
{
RepositoryID: 1,
Name: "testlist",
@ -134,7 +134,7 @@ func (c *controllerTestSuite) TestGet() {
getTest.RepositoryID = 1
getTest.Name = "testget"
c.tagMgr.On("Get").Return(getTest, nil)
c.tagMgr.On("Get", mock.Anything, mock.Anything).Return(getTest, nil)
tag, err := c.ctl.Get(nil, 1, nil)
c.Require().Nil(err)
c.tagMgr.AssertExpectations(c.T())
@ -143,36 +143,36 @@ func (c *controllerTestSuite) TestGet() {
}
func (c *controllerTestSuite) TestDelete() {
c.tagMgr.On("Get").Return(&tag.Tag{
c.tagMgr.On("Get", mock.Anything, mock.Anything).Return(&tag.Tag{
RepositoryID: 1,
Name: "test",
}, nil)
c.artMgr.On("Get", mock.Anything, mock.Anything).Return(&pkg_artifact.Artifact{
ID: 1,
}, nil)
c.immutableMtr.On("Match").Return(false, nil)
c.tagMgr.On("Delete").Return(nil)
mock.OnAnything(c.immutableMtr, "Match").Return(false, nil)
c.tagMgr.On("Delete", mock.Anything, mock.Anything).Return(nil)
err := c.ctl.Delete(nil, 1)
c.Require().Nil(err)
}
func (c *controllerTestSuite) TestDeleteImmutable() {
c.tagMgr.On("Get").Return(&tag.Tag{
c.tagMgr.On("Get", mock.Anything, mock.Anything).Return(&tag.Tag{
RepositoryID: 1,
Name: "test",
}, nil)
c.artMgr.On("Get", mock.Anything, mock.Anything).Return(&pkg_artifact.Artifact{
ID: 1,
}, nil)
c.immutableMtr.On("Match").Return(true, nil)
c.tagMgr.On("Delete").Return(nil)
mock.OnAnything(c.immutableMtr, "Match").Return(true, nil)
c.tagMgr.On("Delete", mock.Anything, mock.Anything).Return(nil)
err := c.ctl.Delete(nil, 1)
c.Require().NotNil(err)
c.True(errors.IsErr(err, errors.PreconditionCode))
}
func (c *controllerTestSuite) TestUpdate() {
c.tagMgr.On("Update").Return(nil)
mock.OnAnything(c.tagMgr, "Update").Return(nil)
err := c.ctl.Update(nil, &Tag{
Tag: tag.Tag{
RepositoryID: 1,
@ -184,14 +184,14 @@ func (c *controllerTestSuite) TestUpdate() {
}
func (c *controllerTestSuite) TestDeleteTags() {
c.tagMgr.On("Get").Return(&tag.Tag{
c.tagMgr.On("Get", mock.Anything, mock.Anything).Return(&tag.Tag{
RepositoryID: 1,
}, nil)
c.artMgr.On("Get", mock.Anything, mock.Anything).Return(&pkg_artifact.Artifact{
ID: 1,
}, nil)
c.immutableMtr.On("Match").Return(false, nil)
c.tagMgr.On("Delete").Return(nil)
mock.OnAnything(c.immutableMtr, "Match").Return(false, nil)
c.tagMgr.On("Delete", mock.Anything, mock.Anything).Return(nil)
ids := []int64{1, 2, 3, 4}
err := c.ctl.DeleteTags(nil, ids)
c.Require().Nil(err)
@ -218,7 +218,7 @@ func (c *controllerTestSuite) TestAssembleTag() {
}
c.artMgr.On("Get", mock.Anything, mock.Anything).Return(art, nil)
c.immutableMtr.On("Match").Return(true, nil)
mock.OnAnything(c.immutableMtr, "Match").Return(true, nil)
tag := c.ctl.assembleTag(nil, tg, option)
c.Require().NotNil(tag)
c.Equal(tag.ID, tg.ID)

View File

@ -21,6 +21,7 @@ import (
"strings"
goldap "github.com/go-ldap/ldap/v3"
"golang.org/x/sync/errgroup"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/models"
@ -38,6 +39,10 @@ import (
ugModel "github.com/goharbor/harbor/src/pkg/usergroup/model"
)
const (
workerCount = 5
)
// Auth implements AuthenticateHelper interface to authenticate against LDAP
type Auth struct {
auth.DefaultAuthenticateHelper
@ -117,24 +122,94 @@ func (l *Auth) attachLDAPGroup(ctx context.Context, ldapUsers []model.User, u *m
return
}
userGroups := make([]ugModel.UserGroup, 0)
if groupCfg.AttachParallel {
log.Debug("Attach LDAP group in parallel")
l.attachGroupParallel(ctx, ldapUsers, u)
return
}
// Attach LDAP group sequencially
for _, dn := range ldapUsers[0].GroupDNList {
lGroups, err := sess.SearchGroupByDN(dn)
if err != nil {
log.Warningf("Can not get the ldap group name with DN %v, error %v", dn, err)
continue
if lgroup, exist := verifyGroupInLDAP(dn, sess); exist {
userGroups = append(userGroups, ugModel.UserGroup{GroupName: lgroup.Name, LdapGroupDN: dn, GroupType: common.LDAPGroupType})
}
if len(lGroups) == 0 {
log.Warningf("Can not get the ldap group name with DN %v", dn)
continue
}
userGroups = append(userGroups, ugModel.UserGroup{GroupName: lGroups[0].Name, LdapGroupDN: dn, GroupType: common.LDAPGroupType})
}
u.GroupIDs, err = ugCtl.Ctl.Populate(ctx, userGroups)
if err != nil {
log.Warningf("Failed to fetch ldap group configuration:%v", err)
log.Warningf("Failed to populate ldap group, error: %v", err)
}
}
func (l *Auth) attachGroupParallel(ctx context.Context, ldapUsers []model.User, u *models.User) {
userGroupsList := make([][]ugModel.UserGroup, workerCount)
gdsList := make([][]string, workerCount)
// Divide the groupDNs into workerCount parts
for index, dn := range ldapUsers[0].GroupDNList {
idx := index % workerCount
gdsList[idx] = append(gdsList[idx], dn)
}
g := new(errgroup.Group)
g.SetLimit(workerCount)
for i := 0; i < workerCount; i++ {
curIndex := i
g.Go(func() error {
userGroups := make([]ugModel.UserGroup, 0)
groups := gdsList[curIndex]
if len(groups) == 0 {
return nil
}
// use different ldap session for each go routine
ldapSession, err := ldapCtl.Ctl.Session(ctx)
if err != nil {
return err
}
if err = ldapSession.Open(); err != nil {
return err
}
defer ldapSession.Close()
log.Debugf("Current worker index is %v", curIndex)
// verify and populate group
for _, dn := range groups {
if lgroup, exist := verifyGroupInLDAP(dn, ldapSession); exist {
userGroups = append(userGroups, ugModel.UserGroup{GroupName: lgroup.Name, LdapGroupDN: dn, GroupType: common.LDAPGroupType})
}
}
userGroupsList[curIndex] = userGroups
return nil
})
}
if err := g.Wait(); err != nil {
log.Warningf("failed to verify and populate ldap group parallel, error %v", err)
}
ugs := make([]ugModel.UserGroup, 0)
for _, userGroups := range userGroupsList {
ugs = append(ugs, userGroups...)
}
groupIDsList, err := ugCtl.Ctl.Populate(ctx, ugs)
if err != nil {
log.Warningf("Failed to populate user groups :%v", err)
}
u.GroupIDs = groupIDsList
}
func verifyGroupInLDAP(groupDN string, sess *ldap.Session) (*model.Group, bool) {
if _, err := goldap.ParseDN(groupDN); err != nil {
return nil, false
}
lGroups, err := sess.SearchGroupByDN(groupDN)
if err != nil {
log.Warningf("Can not get the ldap group name with DN %v, error %v", groupDN, err)
return nil, false
}
if len(lGroups) == 0 {
log.Warningf("Can not get the ldap group name with DN %v", groupDN)
return nil, false
}
return &lGroups[0], true
}
func (l *Auth) syncUserInfoFromDB(ctx context.Context, u *models.User) {
// Retrieve SysAdminFlag from DB so that it transfer to session
dbUser, err := l.userMgr.GetByName(ctx, u.Username)

View File

@ -21,19 +21,19 @@ require (
github.com/go-asn1-ber/asn1-ber v1.5.7
github.com/go-ldap/ldap/v3 v3.4.6
github.com/go-openapi/errors v0.22.0
github.com/go-openapi/loads v0.21.2 // indirect
github.com/go-openapi/loads v0.21.2
github.com/go-openapi/runtime v0.26.2
github.com/go-openapi/spec v0.20.11 // indirect
github.com/go-openapi/spec v0.20.11
github.com/go-openapi/strfmt v0.23.0
github.com/go-openapi/swag v0.23.0
github.com/go-openapi/validate v0.22.3 // indirect
github.com/go-openapi/validate v0.22.3
github.com/go-redis/redis/v8 v8.11.4
github.com/gocarina/gocsv v0.0.0-20210516172204-ca9e8a8ddea8
github.com/gocraft/work v0.5.1
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/golang-migrate/migrate/v4 v4.17.1
github.com/gomodule/redigo v2.0.0+incompatible
github.com/google/go-containerregistry v0.19.2
github.com/google/go-containerregistry v0.20.2
github.com/google/uuid v1.6.0
github.com/gorilla/csrf v1.7.2
github.com/gorilla/handlers v1.5.2
@ -63,7 +63,7 @@ require (
go.opentelemetry.io/otel/sdk v1.27.0
go.opentelemetry.io/otel/trace v1.28.0
go.uber.org/ratelimit v0.3.1
golang.org/x/crypto v0.24.0
golang.org/x/crypto v0.25.0
golang.org/x/net v0.26.0
golang.org/x/oauth2 v0.21.0
golang.org/x/sync v0.7.0
@ -71,10 +71,10 @@ require (
golang.org/x/time v0.5.0
gopkg.in/h2non/gock.v1 v1.1.2
gopkg.in/yaml.v2 v2.4.0
helm.sh/helm/v3 v3.15.2
k8s.io/api v0.30.2
k8s.io/apimachinery v0.30.2
k8s.io/client-go v0.30.0
helm.sh/helm/v3 v3.15.4
k8s.io/api v0.30.3
k8s.io/apimachinery v0.30.3
k8s.io/client-go v0.30.3
sigs.k8s.io/yaml v1.4.0
)
@ -100,8 +100,7 @@ require (
github.com/denverdino/aliyungo v0.0.0-20191227032621-df38c6fa730c // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dnaeon/go-vcr v1.2.0 // indirect
github.com/docker/cli v25.0.1+incompatible // indirect
github.com/docker/docker v25.0.5+incompatible // indirect
github.com/docker/cli v27.1.1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.7.0 // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
@ -173,8 +172,8 @@ require (
go.uber.org/multierr v1.9.0 // indirect
go.uber.org/zap v1.21.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/term v0.21.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/term v0.22.0 // indirect
google.golang.org/api v0.171.0 // indirect
google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect

View File

@ -117,10 +117,10 @@ github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/docker/cli v25.0.1+incompatible h1:mFpqnrS6Hsm3v1k7Wa/BO23oz0k121MTbTO1lpcGSkU=
github.com/docker/cli v25.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE=
github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2oNn0GkeZE=
github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v25.0.6+incompatible h1:5cPwbwriIcsua2REJe8HqQV+6WlWc1byg2QSXzBxBGg=
github.com/docker/docker v25.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
@ -250,8 +250,8 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-containerregistry v0.19.2 h1:TannFKE1QSajsP6hPWb5oJNgKe1IKjHukIKDUmvsV6w=
github.com/google/go-containerregistry v0.19.2/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI=
github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo=
github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@ -627,8 +627,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
@ -668,8 +668,6 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -716,16 +714,16 @@ golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@ -783,8 +781,6 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291/go.
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
@ -834,17 +830,17 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
helm.sh/helm/v3 v3.15.2 h1:/3XINUFinJOBjQplGnjw92eLGpgXXp1L8chWPkCkDuw=
helm.sh/helm/v3 v3.15.2/go.mod h1:FzSIP8jDQaa6WAVg9F+OkKz7J0ZmAga4MABtTbsb9WQ=
helm.sh/helm/v3 v3.15.4 h1:UFHd6oZ1IN3FsUZ7XNhOQDyQ2QYknBNWRHH57e9cbHY=
helm.sh/helm/v3 v3.15.4/go.mod h1:phOwlxqGSgppCY/ysWBNRhG3MtnpsttOzxaTK+Mt40E=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
k8s.io/api v0.30.2 h1:+ZhRj+28QT4UOH+BKznu4CBgPWgkXO7XAvMcMl0qKvI=
k8s.io/api v0.30.2/go.mod h1:ULg5g9JvOev2dG0u2hig4Z7tQ2hHIuS+m8MNZ+X6EmI=
k8s.io/apimachinery v0.30.2 h1:fEMcnBj6qkzzPGSVsAZtQThU62SmQ4ZymlXRC5yFSCg=
k8s.io/apimachinery v0.30.2/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc=
k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ=
k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY=
k8s.io/api v0.30.3 h1:ImHwK9DCsPA9uoU3rVh4QHAHHK5dTSv1nxJUapx8hoQ=
k8s.io/api v0.30.3/go.mod h1:GPc8jlzoe5JG3pb0KJCSLX5oAFIW3/qNJITlDj8BH04=
k8s.io/apimachinery v0.30.3 h1:q1laaWCmrszyQuSQCfNB8cFgCuDAoPszKY4ucAjDwHc=
k8s.io/apimachinery v0.30.3/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc=
k8s.io/client-go v0.30.3 h1:bHrJu3xQZNXIi8/MoxYtZBBWQQXwy16zqJwloXXfD3k=
k8s.io/client-go v0.30.3/go.mod h1:8d4pf8vYu665/kUbsxWAQ/JDBNWqfFeZnvFiVdmx89U=
k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=

View File

@ -318,18 +318,16 @@ func (gc *GarbageCollector) sweep(ctx job.Context) error {
return errGcStop
}
atomic.AddInt64(&index, 1)
index := atomic.LoadInt64(&index)
localIndex := atomic.AddInt64(&index, 1)
// set the status firstly, if the blob is updated by any HEAD/PUT request, it should be fail and skip.
blob.Status = blobModels.StatusDeleting
count, err := gc.blobMgr.UpdateBlobStatus(ctx.SystemContext(), blob)
if err != nil {
gc.logger.Errorf("[%s][%d/%d] failed to mark gc candidate deleting, skip: %s, %s", uid, index, total, blob.Digest, blob.Status)
gc.logger.Errorf("[%s][%d/%d] failed to mark gc candidate deleting, skip: %s, %s", uid, localIndex, total, blob.Digest, blob.Status)
continue
}
if count == 0 {
gc.logger.Warningf("[%s][%d/%d] no blob found to mark gc candidate deleting, ID:%d, digest:%s", uid, index, total, blob.ID, blob.Digest)
gc.logger.Warningf("[%s][%d/%d] no blob found to mark gc candidate deleting, ID:%d, digest:%s", uid, localIndex, total, blob.ID, blob.Digest)
continue
}
@ -339,7 +337,7 @@ func (gc *GarbageCollector) sweep(ctx job.Context) error {
for _, art := range gc.trashedArts[blob.Digest] {
// Harbor cannot know the existing tags in the backend from its database, so let the v2 DELETE manifest to remove all of them.
gc.logger.Infof("[%s][%d/%d] delete the manifest with registry v2 API: %s, %s, %s",
uid, index, total, art.RepositoryName, blob.ContentType, blob.Digest)
uid, localIndex, total, art.RepositoryName, blob.ContentType, blob.Digest)
if err := retry.Retry(func() error {
return ignoreNotFound(func() error {
err := v2DeleteManifest(art.RepositoryName, blob.Digest)
@ -350,13 +348,13 @@ func (gc *GarbageCollector) sweep(ctx job.Context) error {
return err
})
}, retry.Callback(func(err error, sleep time.Duration) {
gc.logger.Infof("[%s][%d/%d] failed to exec v2DeleteManifest, error: %v, will retry again after: %s", uid, index, total, err, sleep)
gc.logger.Infof("[%s][%d/%d] failed to exec v2DeleteManifest, error: %v, will retry again after: %s", uid, localIndex, total, err, sleep)
})); err != nil {
gc.logger.Errorf("[%s][%d/%d] failed to delete manifest with v2 API, %s, %s, %v", uid, index, total, art.RepositoryName, blob.Digest, err)
gc.logger.Errorf("[%s][%d/%d] failed to delete manifest with v2 API, %s, %s, %v", uid, localIndex, total, art.RepositoryName, blob.Digest, err)
if err := ignoreNotFound(func() error {
return gc.markDeleteFailed(ctx, blob)
}); err != nil {
gc.logger.Errorf("[%s][%d/%d] failed to call gc.markDeleteFailed() after v2DeleteManifest() error out: %s, %v", uid, index, total, blob.Digest, err)
gc.logger.Errorf("[%s][%d/%d] failed to call gc.markDeleteFailed() after v2DeleteManifest() error out: %s, %v", uid, localIndex, total, blob.Digest, err)
return err
}
// if the system is set to read-only mode, return directly
@ -367,7 +365,7 @@ func (gc *GarbageCollector) sweep(ctx job.Context) error {
continue
}
// for manifest, it has to delete the revisions folder of each repository
gc.logger.Infof("[%s][%d/%d] delete manifest from storage: %s", uid, index, total, blob.Digest)
gc.logger.Infof("[%s][%d/%d] delete manifest from storage: %s", uid, localIndex, total, blob.Digest)
if err := retry.Retry(func() error {
return ignoreNotFound(func() error {
err := gc.registryCtlClient.DeleteManifest(art.RepositoryName, blob.Digest)
@ -378,13 +376,13 @@ func (gc *GarbageCollector) sweep(ctx job.Context) error {
return err
})
}, retry.Callback(func(err error, sleep time.Duration) {
gc.logger.Infof("[%s][%d/%d] failed to exec DeleteManifest, error: %v, will retry again after: %s", uid, index, total, err, sleep)
gc.logger.Infof("[%s][%d/%d] failed to exec DeleteManifest, error: %v, will retry again after: %s", uid, localIndex, total, err, sleep)
})); err != nil {
gc.logger.Errorf("[%s][%d/%d] failed to remove manifest from storage: %s, %s, errMsg=%v", uid, index, total, art.RepositoryName, blob.Digest, err)
gc.logger.Errorf("[%s][%d/%d] failed to remove manifest from storage: %s, %s, errMsg=%v", uid, localIndex, total, art.RepositoryName, blob.Digest, err)
if err := ignoreNotFound(func() error {
return gc.markDeleteFailed(ctx, blob)
}); err != nil {
gc.logger.Errorf("[%s][%d/%d] failed to call gc.markDeleteFailed() after gc.registryCtlClient.DeleteManifest() error out: %s, %s, %v", uid, index, total, art.RepositoryName, blob.Digest, err)
gc.logger.Errorf("[%s][%d/%d] failed to call gc.markDeleteFailed() after gc.registryCtlClient.DeleteManifest() error out: %s, %s, %v", uid, localIndex, total, art.RepositoryName, blob.Digest, err)
return err
}
// if the system is set to read-only mode, return directly
@ -395,19 +393,19 @@ func (gc *GarbageCollector) sweep(ctx job.Context) error {
continue
}
gc.logger.Infof("[%s][%d/%d] delete artifact blob record from database: %d, %s, %s", uid, index, total, art.ID, art.RepositoryName, art.Digest)
gc.logger.Infof("[%s][%d/%d] delete artifact blob record from database: %d, %s, %s", uid, localIndex, total, art.ID, art.RepositoryName, art.Digest)
if err := ignoreNotFound(func() error {
return gc.blobMgr.CleanupAssociationsForArtifact(ctx.SystemContext(), art.Digest)
}); err != nil {
gc.logger.Errorf("[%s][%d/%d] failed to call gc.blobMgr.CleanupAssociationsForArtifact(): %v, errMsg=%v", uid, index, total, art.Digest, err)
gc.logger.Errorf("[%s][%d/%d] failed to call gc.blobMgr.CleanupAssociationsForArtifact(): %v, errMsg=%v", uid, localIndex, total, art.Digest, err)
return err
}
gc.logger.Infof("[%s][%d/%d] delete artifact trash record from database: %d, %s, %s", uid, index, total, art.ID, art.RepositoryName, art.Digest)
gc.logger.Infof("[%s][%d/%d] delete artifact trash record from database: %d, %s, %s", uid, localIndex, total, art.ID, art.RepositoryName, art.Digest)
if err := ignoreNotFound(func() error {
return gc.artrashMgr.Delete(ctx.SystemContext(), art.ID)
}); err != nil {
gc.logger.Errorf("[%s][%d/%d] failed to call gc.artrashMgr.Delete(): %v, errMsg=%v", uid, index, total, art.ID, err)
gc.logger.Errorf("[%s][%d/%d] failed to call gc.artrashMgr.Delete(): %v, errMsg=%v", uid, localIndex, total, art.ID, err)
return err
}
}
@ -421,7 +419,7 @@ func (gc *GarbageCollector) sweep(ctx job.Context) error {
// delete all the blobs, which include config, layer and manifest
// for the foreign layer, as it's not stored in the storage, no need to call the delete api and count size, but still have to delete the DB record.
if !blob.IsForeignLayer() {
gc.logger.Infof("[%s][%d/%d] delete blob from storage: %s", uid, index, total, blob.Digest)
gc.logger.Infof("[%s][%d/%d] delete blob from storage: %s", uid, localIndex, total, blob.Digest)
if err := retry.Retry(func() error {
return ignoreNotFound(func() error {
err := gc.registryCtlClient.DeleteBlob(blob.Digest)
@ -432,13 +430,13 @@ func (gc *GarbageCollector) sweep(ctx job.Context) error {
return err
})
}, retry.Callback(func(err error, sleep time.Duration) {
gc.logger.Infof("[%s][%d/%d] failed to exec DeleteBlob, error: %v, will retry again after: %s", uid, index, total, err, sleep)
gc.logger.Infof("[%s][%d/%d] failed to exec DeleteBlob, error: %v, will retry again after: %s", uid, localIndex, total, err, sleep)
})); err != nil {
gc.logger.Errorf("[%s][%d/%d] failed to delete blob from storage: %s, %s, errMsg=%v", uid, index, total, blob.Digest, blob.Status, err)
gc.logger.Errorf("[%s][%d/%d] failed to delete blob from storage: %s, %s, errMsg=%v", uid, localIndex, total, blob.Digest, blob.Status, err)
if err := ignoreNotFound(func() error {
return gc.markDeleteFailed(ctx, blob)
}); err != nil {
gc.logger.Errorf("[%s][%d/%d] failed to call gc.markDeleteFailed() after gc.registryCtlClient.DeleteBlob() error out: %s, %v", uid, index, total, blob.Digest, err)
gc.logger.Errorf("[%s][%d/%d] failed to call gc.markDeleteFailed() after gc.registryCtlClient.DeleteBlob() error out: %s, %v", uid, localIndex, total, blob.Digest, err)
return err
}
// if the system is set to read-only mode, return directly
@ -450,15 +448,15 @@ func (gc *GarbageCollector) sweep(ctx job.Context) error {
atomic.AddInt64(&sweepSize, blob.Size)
}
gc.logger.Infof("[%s][%d/%d] delete blob record from database: %d, %s", uid, index, total, blob.ID, blob.Digest)
gc.logger.Infof("[%s][%d/%d] delete blob record from database: %d, %s", uid, localIndex, total, blob.ID, blob.Digest)
if err := ignoreNotFound(func() error {
return gc.blobMgr.Delete(ctx.SystemContext(), blob.ID)
}); err != nil {
gc.logger.Errorf("[%s][%d/%d] failed to delete blob from database: %s, %s, errMsg=%v", uid, index, total, blob.Digest, blob.Status, err)
gc.logger.Errorf("[%s][%d/%d] failed to delete blob from database: %s, %s, errMsg=%v", uid, localIndex, total, blob.Digest, blob.Status, err)
if err := ignoreNotFound(func() error {
return gc.markDeleteFailed(ctx, blob)
}); err != nil {
gc.logger.Errorf("[%s][%d/%d] failed to call gc.markDeleteFailed() after gc.blobMgr.Delete() error out, %d, %s %v", uid, index, total, blob.ID, blob.Digest, err)
gc.logger.Errorf("[%s][%d/%d] failed to call gc.markDeleteFailed() after gc.blobMgr.Delete() error out, %d, %s %v", uid, localIndex, total, blob.ID, blob.Digest, err)
return err
}
return err

View File

@ -42,8 +42,8 @@ import (
type gcTestSuite struct {
htesting.Suite
artifactCtl *artifacttesting.Controller
artrashMgr *trashtesting.FakeManager
registryCtlClient *registryctl.Mockclient
artrashMgr *trashtesting.Manager
registryCtlClient *registryctl.Client
projectCtl *projecttesting.Controller
blobMgr *blob.Manager
@ -54,8 +54,8 @@ type gcTestSuite struct {
func (suite *gcTestSuite) SetupTest() {
suite.artifactCtl = &artifacttesting.Controller{}
suite.artrashMgr = &trashtesting.FakeManager{}
suite.registryCtlClient = &registryctl.Mockclient{}
suite.artrashMgr = &trashtesting.Manager{}
suite.registryCtlClient = &registryctl.Client{}
suite.blobMgr = &blob.Manager{}
suite.projectCtl = &projecttesting.Controller{}
@ -98,7 +98,7 @@ func (suite *gcTestSuite) TestDeletedArt() {
},
}, nil)
suite.artifactCtl.On("Delete").Return(nil)
suite.artrashMgr.On("Filter").Return([]model.ArtifactTrash{
mock.OnAnything(suite.artrashMgr, "Filter").Return([]model.ArtifactTrash{
{
ID: 1,
Digest: suite.DigestString(),
@ -163,6 +163,8 @@ func (suite *gcTestSuite) TestInit() {
"time_window": 1,
"workers": float64(3),
}
mock.OnAnything(gc.registryCtlClient, "Health").Return(nil)
suite.Nil(gc.init(ctx, params))
suite.True(gc.deleteUntagged)
suite.Equal(3, gc.workers)
@ -230,7 +232,7 @@ func (suite *gcTestSuite) TestRun() {
},
}, nil)
suite.artifactCtl.On("Delete").Return(nil)
suite.artrashMgr.On("Filter").Return([]model.ArtifactTrash{}, nil)
mock.OnAnything(suite.artrashMgr, "Filter").Return([]model.ArtifactTrash{}, nil)
mock.OnAnything(suite.projectCtl, "List").Return([]*proModels.Project{
{
@ -271,6 +273,8 @@ func (suite *gcTestSuite) TestRun() {
mock.OnAnything(suite.blobMgr, "Delete").Return(nil)
mock.OnAnything(suite.registryCtlClient, "Health").Return(nil)
gc := &GarbageCollector{
artCtl: suite.artifactCtl,
artrashMgr: suite.artrashMgr,
@ -284,6 +288,7 @@ func (suite *gcTestSuite) TestRun() {
"workers": 3,
}
mock.OnAnything(gc.registryCtlClient, "DeleteBlob").Return(nil)
suite.Nil(gc.Run(ctx, params))
}
@ -302,7 +307,7 @@ func (suite *gcTestSuite) TestMark() {
},
}, nil)
suite.artifactCtl.On("Delete").Return(nil)
suite.artrashMgr.On("Filter").Return([]model.ArtifactTrash{
mock.OnAnything(suite.artrashMgr, "Filter").Return([]model.ArtifactTrash{
{
ID: 1,
Digest: suite.DigestString(),
@ -381,6 +386,7 @@ func (suite *gcTestSuite) TestSweep() {
workers: 3,
}
mock.OnAnything(gc.registryCtlClient, "DeleteBlob").Return(nil)
suite.Nil(gc.sweep(ctx))
}

View File

@ -96,6 +96,7 @@ var (
{Name: common.LDAPURL, Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_URL", DefaultValue: "", ItemType: &NonEmptyStringType{}, Editable: false, Description: `The URL of LDAP server`},
{Name: common.LDAPVerifyCert, Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_VERIFY_CERT", DefaultValue: "true", ItemType: &BoolType{}, Editable: false, Description: `Whether verify your OIDC server certificate, disable it if your OIDC server is hosted via self-hosted certificate.`},
{Name: common.LDAPGroupMembershipAttribute, Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_GROUP_MEMBERSHIP_ATTRIBUTE", DefaultValue: "memberof", ItemType: &StringType{}, Editable: true, Description: `The user attribute to identify the group membership`},
{Name: common.LDAPGroupAttachParallel, Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_GROUP_ATTACH_PARALLEL", DefaultValue: "false", ItemType: &BoolType{}, Editable: true, Description: `Attach LDAP group information to Harbor in parallel`},
{Name: common.MaxJobWorkers, Scope: SystemScope, Group: BasicGroup, EnvKey: "MAX_JOB_WORKERS", DefaultValue: "10", ItemType: &IntType{}, Editable: false},
{Name: common.ScanAllPolicy, Scope: UserScope, Group: BasicGroup, EnvKey: "", DefaultValue: "", ItemType: &MapType{}, Editable: false, Description: `The policy to scan images`},

View File

@ -94,6 +94,7 @@ type GroupConf struct {
SearchScope int `json:"ldap_group_search_scope"`
AdminDN string `json:"ldap_group_admin_dn,omitempty"`
MembershipAttribute string `json:"ldap_group_membership_attribute,omitempty"`
AttachParallel bool `json:"ldap_group_attach_parallel,omitempty"`
}
type GDPRSetting struct {

View File

@ -81,6 +81,7 @@ func LDAPGroupConf(ctx context.Context) (*cfgModels.GroupConf, error) {
SearchScope: mgr.Get(ctx, common.LDAPGroupSearchScope).GetInt(),
AdminDN: mgr.Get(ctx, common.LDAPGroupAdminDn).GetString(),
MembershipAttribute: mgr.Get(ctx, common.LDAPGroupMembershipAttribute).GetString(),
AttachParallel: mgr.Get(ctx, common.LDAPGroupAttachParallel).GetBool(),
}, nil
}

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package transfer
package lib
import (
"fmt"

View File

@ -54,6 +54,8 @@ type DAO interface {
DeleteReference(ctx context.Context, id int64) (err error)
// DeleteReferences deletes the references referenced by the artifact specified by parent ID
DeleteReferences(ctx context.Context, parentID int64) (err error)
// ListWithLatest ...
ListWithLatest(ctx context.Context, query *q.Query) (artifacts []*Artifact, err error)
}
const (
@ -282,6 +284,53 @@ func (d *dao) DeleteReferences(ctx context.Context, parentID int64) error {
return err
}
func (d *dao) ListWithLatest(ctx context.Context, query *q.Query) (artifacts []*Artifact, err error) {
ormer, err := orm.FromContext(ctx)
if err != nil {
return nil, err
}
sql := `SELECT a.*
FROM artifact a
JOIN (
SELECT repository_name, MAX(push_time) AS latest_push_time
FROM artifact
WHERE project_id = ? and %s = ?
GROUP BY repository_name
) latest ON a.repository_name = latest.repository_name AND a.push_time = latest.latest_push_time`
queryParam := make([]interface{}, 0)
var ok bool
var pid interface{}
if pid, ok = query.Keywords["ProjectID"]; !ok {
return nil, errors.New(nil).WithCode(errors.BadRequestCode).
WithMessage(`the value of "ProjectID" must be set`)
}
queryParam = append(queryParam, pid)
var attributionValue interface{}
if attributionValue, ok = query.Keywords["media_type"]; ok {
sql = fmt.Sprintf(sql, "media_type")
} else if attributionValue, ok = query.Keywords["artifact_type"]; ok {
sql = fmt.Sprintf(sql, "artifact_type")
}
if attributionValue == "" {
return nil, errors.New(nil).WithCode(errors.BadRequestCode).
WithMessage(`the value of "media_type" or "artifact_type" must be set`)
}
queryParam = append(queryParam, attributionValue)
sql, queryParam = orm.PaginationOnRawSQL(query, sql, queryParam)
arts := []*Artifact{}
_, err = ormer.Raw(sql, queryParam...).QueryRows(&arts)
if err != nil {
return nil, err
}
return arts, nil
}
func querySetter(ctx context.Context, query *q.Query, options ...orm.Option) (beegoorm.QuerySeter, error) {
qs, err := orm.QuerySetter(ctx, &Artifact{}, query, options...)
if err != nil {

View File

@ -472,6 +472,75 @@ func (d *daoTestSuite) TestDeleteReferences() {
d.True(errors.IsErr(err, errors.NotFoundCode))
}
func (d *daoTestSuite) TestListWithLatest() {
now := time.Now()
art := &Artifact{
Type: "IMAGE",
MediaType: v1.MediaTypeImageConfig,
ManifestMediaType: v1.MediaTypeImageIndex,
ProjectID: 1234,
RepositoryID: 1234,
RepositoryName: "library2/hello-world1",
Digest: "digest",
PushTime: now,
PullTime: now,
Annotations: `{"anno1":"value1"}`,
}
id, err := d.dao.Create(d.ctx, art)
d.Require().Nil(err)
time.Sleep(1 * time.Second)
now = time.Now()
art2 := &Artifact{
Type: "IMAGE",
MediaType: v1.MediaTypeImageConfig,
ManifestMediaType: v1.MediaTypeImageIndex,
ProjectID: 1234,
RepositoryID: 1235,
RepositoryName: "library2/hello-world2",
Digest: "digest",
PushTime: now,
PullTime: now,
Annotations: `{"anno1":"value1"}`,
}
id1, err := d.dao.Create(d.ctx, art2)
d.Require().Nil(err)
time.Sleep(1 * time.Second)
now = time.Now()
art3 := &Artifact{
Type: "IMAGE",
MediaType: v1.MediaTypeImageConfig,
ManifestMediaType: v1.MediaTypeImageIndex,
ProjectID: 1234,
RepositoryID: 1235,
RepositoryName: "library2/hello-world2",
Digest: "digest2",
PushTime: now,
PullTime: now,
Annotations: `{"anno1":"value1"}`,
}
id2, err := d.dao.Create(d.ctx, art3)
d.Require().Nil(err)
latest, err := d.dao.ListWithLatest(d.ctx, &q.Query{
Keywords: map[string]interface{}{
"ProjectID": 1234,
"media_type": v1.MediaTypeImageConfig,
},
})
d.Require().Nil(err)
d.Require().Equal(2, len(latest))
d.Equal("library2/hello-world1", latest[0].RepositoryName)
defer d.dao.Delete(d.ctx, id)
defer d.dao.Delete(d.ctx, id1)
defer d.dao.Delete(d.ctx, id2)
}
func TestDaoTestSuite(t *testing.T) {
suite.Run(t, &daoTestSuite{})
}

View File

@ -48,6 +48,8 @@ type Manager interface {
ListReferences(ctx context.Context, query *q.Query) (references []*Reference, err error)
// DeleteReference specified by ID
DeleteReference(ctx context.Context, id int64) (err error)
// ListWithLatest list the artifacts when the latest_in_repository in the query was set
ListWithLatest(ctx context.Context, query *q.Query) (artifacts []*Artifact, err error)
}
// NewManager returns an instance of the default manager
@ -147,6 +149,22 @@ func (m *manager) DeleteReference(ctx context.Context, id int64) error {
return m.dao.DeleteReference(ctx, id)
}
func (m *manager) ListWithLatest(ctx context.Context, query *q.Query) ([]*Artifact, error) {
arts, err := m.dao.ListWithLatest(ctx, query)
if err != nil {
return nil, err
}
var artifacts []*Artifact
for _, art := range arts {
artifact, err := m.assemble(ctx, art)
if err != nil {
return nil, err
}
artifacts = append(artifacts, artifact)
}
return artifacts, nil
}
// assemble the artifact with references populated
func (m *manager) assemble(ctx context.Context, art *dao.Artifact) (*Artifact, error) {
artifact := &Artifact{}

View File

@ -80,6 +80,11 @@ func (f *fakeDao) DeleteReferences(ctx context.Context, parentID int64) error {
return args.Error(0)
}
func (f *fakeDao) ListWithLatest(ctx context.Context, query *q.Query) ([]*dao.Artifact, error) {
args := f.Called()
return args.Get(0).([]*dao.Artifact), args.Error(1)
}
type managerTestSuite struct {
suite.Suite
mgr *manager
@ -135,6 +140,28 @@ func (m *managerTestSuite) TestAssemble() {
m.Equal(2, len(artifact.References))
}
func (m *managerTestSuite) TestListWithLatest() {
art := &dao.Artifact{
ID: 1,
Type: "IMAGE",
MediaType: "application/vnd.oci.image.config.v1+json",
ManifestMediaType: "application/vnd.oci.image.manifest.v1+json",
ProjectID: 1,
RepositoryID: 1,
Digest: "sha256:418fb88ec412e340cdbef913b8ca1bbe8f9e8dc705f9617414c1f2c8db980180",
Size: 1024,
PushTime: time.Now(),
PullTime: time.Now(),
ExtraAttrs: `{"attr1":"value1"}`,
Annotations: `{"anno1":"value1"}`,
}
m.dao.On("ListWithLatest", mock.Anything).Return([]*dao.Artifact{art}, nil)
artifacts, err := m.mgr.ListWithLatest(nil, nil)
m.Require().Nil(err)
m.Equal(1, len(artifacts))
m.Equal(art.ID, artifacts[0].ID)
}
func (m *managerTestSuite) TestList() {
art := &dao.Artifact{
ID: 1,

View File

@ -65,6 +65,10 @@ func (m *Manager) List(ctx context.Context, query *q.Query) ([]*artifact.Artifac
return m.delegator.List(ctx, query)
}
func (m *Manager) ListWithLatest(ctx context.Context, query *q.Query) ([]*artifact.Artifact, error) {
return m.delegator.ListWithLatest(ctx, query)
}
func (m *Manager) Create(ctx context.Context, artifact *artifact.Artifact) (int64, error) {
return m.delegator.Create(ctx, artifact)
}

View File

@ -24,4 +24,5 @@ const (
ProMetaAutoScan = "auto_scan"
ProMetaReuseSysCVEAllowlist = "reuse_sys_cve_allowlist"
ProMetaAutoSBOMGen = "auto_sbom_generation"
ProMetaProxySpeed = "proxy_speed_kb"
)

View File

@ -156,6 +156,19 @@ func (p *Project) AutoSBOMGen() bool {
return isTrue(auto)
}
// ProxyCacheSpeed ...
func (p *Project) ProxyCacheSpeed() int32 {
speed, exist := p.GetMetadata(ProMetaProxySpeed)
if !exist {
return 0
}
speedInt, err := strconv.ParseInt(speed, 10, 32)
if err != nil {
return 0
}
return int32(speedInt)
}
// FilterByPublic returns orm.QuerySeter with public filter
func (p *Project) FilterByPublic(_ context.Context, qs orm.QuerySeter, _ string, value interface{}) orm.QuerySeter {
subQuery := `SELECT project_id FROM project_metadata WHERE name = 'public' AND value = '%s'`

View File

@ -29,7 +29,7 @@ import (
)
const (
ecrPattern = "https://(?:api|(\\d+)\\.dkr)\\.ecr\\.([\\w\\-]+)\\.amazonaws\\.com"
ecrPattern = "https://(?:api|(\\d+)\\.dkr)\\.ecr(\\-fips)?\\.([\\w\\-]+)\\.(amazonaws\\.com(\\.cn)?|sc2s\\.sgov\\.gov|c2s\\.ic\\.gov)"
)
var (
@ -64,10 +64,10 @@ func newAdapter(registry *model.Registry) (*adapter, error) {
func parseAccountRegion(url string) (string, string, error) {
rs := ecrRegexp.FindStringSubmatch(url)
if rs == nil {
if rs == nil || len(rs) < 4 {
return "", "", errors.New("bad aws url")
}
return rs[1], rs[2], nil
return rs[1], rs[3], nil
}
type factory struct {

View File

@ -83,6 +83,38 @@ func TestAdapter_NewAdapter(t *testing.T) {
assert.Nil(t, adapter)
assert.NotNil(t, err)
adapter, err = newAdapter(&model.Registry{
Type: model.RegistryTypeAwsEcr,
Credential: &model.Credential{
AccessKey: "xxx",
AccessSecret: "ppp",
},
URL: "https://123456.dkr.ecr-fips.test-region.amazonaws.com",
})
assert.Nil(t, err)
assert.NotNil(t, adapter)
adapter, err = newAdapter(&model.Registry{
Type: model.RegistryTypeAwsEcr,
Credential: &model.Credential{
AccessKey: "xxx",
AccessSecret: "ppp",
},
URL: "https://123456.dkr.ecr.us-isob-east-1.sc2s.sgov.gov",
})
assert.Nil(t, err)
assert.NotNil(t, adapter)
adapter, err = newAdapter(&model.Registry{
Type: model.RegistryTypeAwsEcr,
Credential: &model.Credential{
AccessKey: "xxx",
AccessSecret: "ppp",
},
URL: "https://123456.dkr.ecr.us-iso-east-1.c2s.ic.gov",
})
assert.Nil(t, err)
assert.NotNil(t, adapter)
}
func getMockAdapter(t *testing.T, hasCred, health bool) (*adapter, *httptest.Server) {

View File

@ -52,6 +52,7 @@ func (suite *DaoTestSuite) robots() {
Description: "test3 description",
ProjectID: 1,
Secret: suite.RandString(10),
Creator: "tester",
})
suite.Nil(err)
@ -120,6 +121,7 @@ func (suite *DaoTestSuite) TestGet() {
r, err := suite.dao.Get(orm.Context(), suite.robotID3)
suite.Nil(err)
suite.Equal("test3", r.Name)
suite.Equal("tester", r.Creator)
}
func (suite *DaoTestSuite) TestCount() {

View File

@ -39,6 +39,7 @@ type Robot struct {
ExpiresAt int64 `orm:"column(expiresat)" json:"expires_at"`
Disabled bool `orm:"column(disabled)" json:"disabled"`
Visible bool `orm:"column(visible)" json:"-"`
Creator string `orm:"column(creator)" json:"creator"`
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"`
}

View File

@ -62,12 +62,12 @@ func TestPostScan(t *testing.T) {
origRp := &scan.Report{}
rawReport := ""
mocker := &postprocessorstesting.ScanReportV1ToV2Converter{}
mocker.On("ToRelationalSchema", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, "original report", nil)
mocker := &postprocessorstesting.NativeScanReportConverter{}
mocker.On("ToRelationalSchema", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("", "original report", nil)
postprocessors.Converter = mocker
sr := &v1.ScanRequest{Artifact: artifact}
refreshedReport, err := v.PostScan(ctx, sr, origRp, rawReport, time.Now(), &model.Robot{})
assert.Equal(t, "", refreshedReport, "PostScan should return the refreshed report")
assert.Equal(t, "original report", refreshedReport, "PostScan should return the refreshed report")
assert.Nil(t, err, "PostScan should not return an error")
}
@ -209,6 +209,7 @@ func (suite *VulHandlerTestSuite) TestMakeReportPlaceHolder() {
mock.OnAnything(suite.reportMgr, "Create").Return("uuid", nil).Once()
mock.OnAnything(suite.reportMgr, "Delete").Return(nil).Once()
mock.OnAnything(suite.taskMgr, "ListScanTasksByReportUUID").Return([]*task.Task{{Status: "Success"}}, nil)
mock.OnAnything(suite.handler.reportConverter, "FromRelationalSchema").Return("", nil)
rps, err := suite.handler.MakePlaceHolder(ctx, art, r)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 1, len(rps))

View File

@ -498,6 +498,37 @@
</option>
</select>
</clr-select-container>
<clr-checkbox-container>
<label for="ldapGroupAttachParallel">
{{ 'CONFIG.LDAP.GROUP_ATTACH_PARALLEL' | translate }}
<clr-tooltip>
<clr-icon
clrTooltipTrigger
shape="info-circle"
size="24"></clr-icon>
<clr-tooltip-content
*clrIfOpen
clrPosition="top-right"
clrSize="lg">
<span>{{
'CONFIG.LDAP.GROUP_ATTACH_PARALLEL_INFO' | translate
}}</span>
</clr-tooltip-content>
</clr-tooltip>
</label>
<clr-checkbox-wrapper>
<input
(ngModelChange)="setLdapGroupAttachParallelValue($event)"
[disabled]="
disabled(currentConfig.ldap_group_attach_parallel)
"
[ngModel]="currentConfig.ldap_group_attach_parallel.value"
clrCheckbox
id="ldapGroupAttachParallel"
name="ldapGroupAttachParallel"
type="checkbox" />
</clr-checkbox-wrapper>
</clr-checkbox-container>
</section>
<clr-checkbox-container *ngIf="showSelfReg">
<label for="selfReg"

View File

@ -11,15 +11,15 @@
// 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.
import { Component, ViewChild, OnInit } from '@angular/core';
import { Component, OnInit, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { MessageHandlerService } from '../../../../shared/services/message-handler.service';
import { AppConfigService } from '../../../../services/app-config.service';
import { ConfigurationService } from '../../../../services/config.service';
import { SystemInfoService } from '../../../../shared/services';
import {
isEmpty,
getChanges as getChangesFunc,
isEmpty,
} from '../../../../shared/units/utils';
import { CONFIG_AUTH_MODE } from '../../../../shared/entities/shared.const';
import { errorHandler } from '../../../../shared/units/shared.utils';
@ -132,6 +132,9 @@ export class ConfigurationAuthComponent implements OnInit {
this.currentConfig.ldap_verify_cert.value = $event;
}
setLdapGroupAttachParallelValue($event: any) {
this.currentConfig.ldap_group_attach_parallel.value = $event;
}
public pingTestServer(): void {
if (this.testingOnGoing) {
return; // Should not come here

View File

@ -72,6 +72,7 @@ export class Configuration {
ldap_group_search_scope: NumberValueItem;
ldap_group_membership_attribute: StringValueItem;
ldap_group_admin_dn: StringValueItem;
ldap_group_attach_parallel: BoolValueItem;
uaa_client_id: StringValueItem;
uaa_client_secret?: StringValueItem;
uaa_endpoint: StringValueItem;

View File

@ -139,6 +139,7 @@
}}</label>
<app-label-selector
[usedInDropdown]="true"
[dropdownOpened]="true"
[width]="200"
[ownedLabels]="
selectedRow[0]?.labels

View File

@ -928,7 +928,9 @@
"LDAP_GROUP_MEMBERSHIP": "LDAP Group Membership",
"LDAP_GROUP_MEMBERSHIP_INFO": "The attribute indicates the membership of LDAP group, default value is memberof, in some LDAP server it could be \"ismemberof\". This field cannot be empty if you need to enable the LDAP group related feature.",
"GROUP_SCOPE": "LDAP Group Search Scope",
"GROUP_SCOPE_INFO": "The scope to search for groups, select Subtree by default."
"GROUP_SCOPE_INFO": "The scope to search for groups, select Subtree by default.",
"GROUP_ATTACH_PARALLEL": "LDAP Group Attached In Parallel",
"GROUP_ATTACH_PARALLEL_INFO": "Enable this option to attach group in parallel to avoid timeout when there are too many groups. If disabled, the LDAP group information will be attached sequentially."
},
"UAA": {

View File

@ -926,7 +926,10 @@
"LDAP_GROUP_MEMBERSHIP": "LDAP 组成员",
"LDAP_GROUP_MEMBERSHIP_INFO": "LDAP组成员的membership属性默认为 memberof, 在某些LDAP服务器会变为 ismemberof。如果要开启LDAP组功能则此项必填",
"GROUP_SCOPE": "LDAP组搜索范围",
"GROUP_SCOPE_INFO": "搜索组的范围,默认值为\"子树\""
"GROUP_SCOPE_INFO": "搜索组的范围,默认值为\"子树\"",
"GROUP_ATTACH_PARALLEL": "LDAP组并行同步",
"GROUP_ATTACH_PARALLEL_INFO": "打开这个选项时LDAP组的信息是并行同步到Harbor, 这样可以防止用户组太多时造成的登录超时如果关闭这个选项LDAP组信息是顺序同步到Harbor"
},
"UAA": {
"ENDPOINT": "UAA Endpoint",

View File

@ -60,7 +60,7 @@ func BlobGetMiddleware() func(http.Handler) http.Handler {
func handleBlob(w http.ResponseWriter, r *http.Request, next http.Handler) error {
ctx := r.Context()
art, p, proxyCtl, err := preCheck(ctx)
art, p, proxyCtl, err := preCheck(ctx, true)
if err != nil {
return err
}
@ -96,14 +96,14 @@ func handleBlob(w http.ResponseWriter, r *http.Request, next http.Handler) error
return nil
}
func preCheck(ctx context.Context) (art lib.ArtifactInfo, p *proModels.Project, ctl proxy.Controller, err error) {
func preCheck(ctx context.Context, withProjectMetadata bool) (art lib.ArtifactInfo, p *proModels.Project, ctl proxy.Controller, err error) {
none := lib.ArtifactInfo{}
art = lib.GetArtifactInfo(ctx)
if art == none {
return none, nil, nil, errors.New("artifactinfo is not found").WithCode(errors.NotFoundCode)
}
ctl = proxy.ControllerInstance()
p, err = project.Ctl.GetByName(ctx, art.ProjectName, project.Metadata(false))
p, err = project.Ctl.GetByName(ctx, art.ProjectName, project.Metadata(withProjectMetadata))
return
}
@ -155,7 +155,7 @@ func defaultBlobURL(projectName string, name string, digest string) string {
func handleManifest(w http.ResponseWriter, r *http.Request, next http.Handler) error {
ctx := r.Context()
art, p, proxyCtl, err := preCheck(ctx)
art, p, proxyCtl, err := preCheck(ctx, true)
if err != nil {
return err
}
@ -174,7 +174,7 @@ func handleManifest(w http.ResponseWriter, r *http.Request, next http.Handler) e
next.ServeHTTP(w, r)
return nil
}
remote, err := proxy.NewRemoteHelper(r.Context(), p.RegistryID)
remote, err := proxy.NewRemoteHelper(r.Context(), p.RegistryID, proxy.WithSpeed(p.ProxyCacheSpeed()))
if err != nil {
return err
}

View File

@ -35,7 +35,7 @@ func TagsListMiddleware() func(http.Handler) http.Handler {
return middleware.New(func(w http.ResponseWriter, r *http.Request, next http.Handler) {
ctx := r.Context()
art, p, _, err := preCheck(ctx)
art, p, _, err := preCheck(ctx, false)
if err != nil {
libhttp.SendError(w, err)
return
@ -69,7 +69,7 @@ func TagsListMiddleware() func(http.Handler) http.Handler {
util.SendListTagsResponse(w, r, tags)
}()
remote, err := proxy.NewRemoteHelper(ctx, p.RegistryID)
remote, err := proxy.NewRemoteHelper(ctx, p.RegistryID, proxy.WithSpeed(p.ProxyCacheSpeed()))
if err != nil {
logger.Warningf("failed to get remote interface, error: %v, fallback to local tags", err)
return

View File

@ -104,7 +104,7 @@ func (a *artifactAPI) ListArtifacts(ctx context.Context, params operation.ListAr
// set option
option := option(params.WithTag, params.WithImmutableStatus,
params.WithLabel, params.WithAccessory)
params.WithLabel, params.WithAccessory, nil)
// get the total count of artifacts
total, err := a.artCtl.Count(ctx, query)
@ -138,7 +138,7 @@ func (a *artifactAPI) GetArtifact(ctx context.Context, params operation.GetArtif
}
// set option
option := option(params.WithTag, params.WithImmutableStatus,
params.WithLabel, params.WithAccessory)
params.WithLabel, params.WithAccessory, nil)
// get the artifact
artifact, err := a.artCtl.GetByReference(ctx, fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName), params.Reference, option)
@ -510,11 +510,12 @@ func (a *artifactAPI) RequireLabelInProject(ctx context.Context, projectID, labe
return nil
}
func option(withTag, withImmutableStatus, withLabel, withAccessory *bool) *artifact.Option {
func option(withTag, withImmutableStatus, withLabel, withAccessory *bool, latestInRepository *bool) *artifact.Option {
option := &artifact.Option{
WithTag: true, // return the tag by default
WithLabel: lib.BoolValue(withLabel),
WithAccessory: true, // return the accessory by default
WithTag: true, // return the tag by default
WithLabel: lib.BoolValue(withLabel),
WithAccessory: true, // return the accessory by default
LatestInRepository: lib.BoolValue(latestInRepository),
}
if withTag != nil {

View File

@ -49,6 +49,8 @@ func (a *Artifact) ToSwagger() *models.Artifact {
PushTime: strfmt.DateTime(a.PushTime),
ExtraAttrs: a.ExtraAttrs,
Annotations: a.Annotations,
ArtifactType: a.ArtifactType,
RepositoryName: a.RepositoryName,
}
for _, reference := range a.References {

View File

@ -48,6 +48,7 @@ func (r *Robot) ToSwagger() *models.Robot {
Level: r.Level,
Disable: r.Disabled,
Editable: r.Editable,
Creator: r.Creator,
CreationTime: strfmt.DateTime(r.CreationTime),
UpdateTime: strfmt.DateTime(r.UpdateTime),
Permissions: perms,

View File

@ -29,6 +29,7 @@ import (
"github.com/goharbor/harbor/src/common/security"
"github.com/goharbor/harbor/src/common/security/local"
robotSec "github.com/goharbor/harbor/src/common/security/robot"
"github.com/goharbor/harbor/src/controller/artifact"
"github.com/goharbor/harbor/src/controller/p2p/preheat"
"github.com/goharbor/harbor/src/controller/project"
"github.com/goharbor/harbor/src/controller/quota"
@ -52,6 +53,7 @@ import (
"github.com/goharbor/harbor/src/pkg/retention/policy"
"github.com/goharbor/harbor/src/pkg/robot"
userModels "github.com/goharbor/harbor/src/pkg/user/models"
"github.com/goharbor/harbor/src/server/v2.0/handler/assembler"
"github.com/goharbor/harbor/src/server/v2.0/handler/model"
"github.com/goharbor/harbor/src/server/v2.0/models"
operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/project"
@ -63,6 +65,7 @@ const defaultDaysToRetentionForProxyCacheProject = 7
func newProjectAPI() *projectAPI {
return &projectAPI{
auditMgr: audit.Mgr,
artCtl: artifact.Ctl,
metadataMgr: pkg.ProjectMetaMgr,
userCtl: user.Ctl,
repositoryCtl: repository.Ctl,
@ -79,6 +82,7 @@ func newProjectAPI() *projectAPI {
type projectAPI struct {
BaseAPI
auditMgr audit.Manager
artCtl artifact.Controller
metadataMgr metadata.Manager
userCtl user.Controller
repositoryCtl repository.Controller
@ -155,6 +159,11 @@ func (a *projectAPI) CreateProject(ctx context.Context, params operation.CreateP
}
}
// ignore metadata.proxy_speed_kb for non-proxy-cache project
if req.RegistryID == nil {
req.Metadata.ProxySpeedKb = nil
}
// ignore enable_content_trust metadata for proxy cache project
// see https://github.com/goharbor/harbor/issues/12940 to get more info
if req.RegistryID != nil {
@ -547,6 +556,11 @@ func (a *projectAPI) UpdateProject(ctx context.Context, params operation.UpdateP
}
}
// ignore metadata.proxy_speed_kb for non-proxy-cache project
if params.Project.Metadata != nil && !p.IsProxy() {
params.Project.Metadata.ProxySpeedKb = nil
}
// ignore enable_content_trust metadata for proxy cache project
// see https://github.com/goharbor/harbor/issues/12940 to get more info
if params.Project.Metadata != nil && p.IsProxy() {
@ -660,6 +674,82 @@ func (a *projectAPI) SetScannerOfProject(ctx context.Context, params operation.S
return operation.NewSetScannerOfProjectOK()
}
func (a *projectAPI) ListArtifactsOfProject(ctx context.Context, params operation.ListArtifactsOfProjectParams) middleware.Responder {
if err := a.RequireAuthenticated(ctx); err != nil {
return a.SendError(ctx, err)
}
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
if err := a.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionList, rbac.ResourceArtifact); err != nil {
return a.SendError(ctx, err)
}
// set query
pro, err := a.projectCtl.Get(ctx, projectNameOrID)
if err != nil {
return a.SendError(ctx, err)
}
query, err := a.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
if err != nil {
return a.SendError(ctx, err)
}
query.Keywords["ProjectID"] = pro.ProjectID
// set option
option := option(params.WithTag, params.WithImmutableStatus,
params.WithLabel, params.WithAccessory, params.LatestInRepository)
var total int64
// list artifacts according to the query and option
var arts []*artifact.Artifact
if option.LatestInRepository {
// ignore page & page_size
_, hasMediaType := query.Keywords["media_type"]
_, hasArtifactType := query.Keywords["artifact_type"]
if hasMediaType == hasArtifactType {
return a.SendError(ctx, errors.BadRequestError(fmt.Errorf("either 'media_type' or 'artifact_type' must be specified, but not both, when querying with latest_in_repository")))
}
getCount := func() (int64, error) {
var countQ *q.Query
if query != nil {
countQ = q.New(query.Keywords)
}
allArts, err := a.artCtl.ListWithLatest(ctx, countQ, nil)
if err != nil {
return int64(0), err
}
return int64(len(allArts)), nil
}
total, err = getCount()
if err != nil {
return a.SendError(ctx, err)
}
arts, err = a.artCtl.ListWithLatest(ctx, query, option)
} else {
total, err = a.artCtl.Count(ctx, query)
if err != nil {
return a.SendError(ctx, err)
}
arts, err = a.artCtl.List(ctx, query, option)
}
if err != nil {
return a.SendError(ctx, err)
}
overviewOpts := model.NewOverviewOptions(model.WithSBOM(lib.BoolValue(params.WithSbomOverview)), model.WithVuln(lib.BoolValue(params.WithScanOverview)))
assembler := assembler.NewScanReportAssembler(overviewOpts, parseScanReportMimeTypes(params.XAcceptVulnerabilities))
var artifacts []*models.Artifact
for _, art := range arts {
artifact := &model.Artifact{}
artifact.Artifact = *art
_ = assembler.WithArtifacts(artifact).Assemble(ctx)
artifacts = append(artifacts, artifact.ToSwagger())
}
return operation.NewListArtifactsOfProjectOK().
WithXTotalCount(total).
WithLink(a.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String()).
WithPayload(artifacts)
}
func (a *projectAPI) deletable(ctx context.Context, projectNameOrID interface{}) (*project.Project, *models.ProjectDeletable, error) {
p, err := a.getProject(ctx, projectNameOrID)
if err != nil {
@ -712,6 +802,13 @@ func (a *projectAPI) validateProjectReq(ctx context.Context, req *models.Project
if !permitted {
return errors.BadRequestError(fmt.Errorf("unsupported registry type %s", string(registry.Type)))
}
// validate metadata.proxy_speed_kb. It should be an int32
if ps := req.Metadata.ProxySpeedKb; ps != nil {
if _, err := strconv.ParseInt(*ps, 10, 32); err != nil {
return errors.BadRequestError(nil).WithMessage(fmt.Sprintf("metadata.proxy_speed_kb should by an int32, but got: '%s', err: %s", *ps, err))
}
}
}
if req.StorageLimit != nil {

View File

@ -155,6 +155,12 @@ func (p *projectMetadataAPI) validate(metas map[string]string) (map[string]strin
return nil, errors.New(nil).WithCode(errors.BadRequestCode).WithMessage("invalid value: %s", value)
}
metas[proModels.ProMetaSeverity] = strings.ToLower(severity.String())
case proModels.ProMetaProxySpeed:
v, err := strconv.ParseInt(value, 10, 32)
if err != nil {
return nil, errors.New(nil).WithCode(errors.BadRequestCode).WithMessage("invalid value: %s", value)
}
metas[proModels.ProMetaProxySpeed] = strconv.FormatInt(v, 10)
default:
return nil, errors.New(nil).WithCode(errors.BadRequestCode).WithMessage("invalid key: %s", key)
}

View File

@ -62,12 +62,18 @@ func (rAPI *robotAPI) CreateRobot(ctx context.Context, params operation.CreateRo
return rAPI.SendError(ctx, err)
}
sc, err := rAPI.GetSecurityContext(ctx)
if err != nil {
return rAPI.SendError(ctx, err)
}
r := &robot.Robot{
Robot: pkg.Robot{
Name: params.Robot.Name,
Description: params.Robot.Description,
Duration: params.Robot.Duration,
Visible: true,
Creator: sc.GetUsername(),
},
Level: params.Robot.Level,
}

View File

@ -296,6 +296,36 @@ func (_m *Controller) List(ctx context.Context, query *q.Query, option *artifact
return r0, r1
}
// ListWithLatest provides a mock function with given fields: ctx, query, option
func (_m *Controller) ListWithLatest(ctx context.Context, query *q.Query, option *artifact.Option) ([]*artifact.Artifact, error) {
ret := _m.Called(ctx, query, option)
if len(ret) == 0 {
panic("no return value specified for ListWithLatest")
}
var r0 []*artifact.Artifact
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *q.Query, *artifact.Option) ([]*artifact.Artifact, error)); ok {
return rf(ctx, query, option)
}
if rf, ok := ret.Get(0).(func(context.Context, *q.Query, *artifact.Option) []*artifact.Artifact); ok {
r0 = rf(ctx, query, option)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*artifact.Artifact)
}
}
if rf, ok := ret.Get(1).(func(context.Context, *q.Query, *artifact.Option) error); ok {
r1 = rf(ctx, query, option)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RemoveLabel provides a mock function with given fields: ctx, artifactID, labelID
func (_m *Controller) RemoveLabel(ctx context.Context, artifactID int64, labelID int64) error {
ret := _m.Called(ctx, artifactID, labelID)

View File

@ -1,136 +0,0 @@
// Code generated by mockery v2.22.1. DO NOT EDIT.
package libcache
import (
context "context"
cache "github.com/goharbor/harbor/src/lib/cache"
mock "github.com/stretchr/testify/mock"
time "time"
)
// Cache is an autogenerated mock type for the Cache type
type Cache struct {
mock.Mock
}
// Contains provides a mock function with given fields: ctx, key
func (_m *Cache) Contains(ctx context.Context, key string) bool {
ret := _m.Called(ctx, key)
var r0 bool
if rf, ok := ret.Get(0).(func(context.Context, string) bool); ok {
r0 = rf(ctx, key)
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// Delete provides a mock function with given fields: ctx, key
func (_m *Cache) Delete(ctx context.Context, key string) error {
ret := _m.Called(ctx, key)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
r0 = rf(ctx, key)
} else {
r0 = ret.Error(0)
}
return r0
}
// Fetch provides a mock function with given fields: ctx, key, value
func (_m *Cache) Fetch(ctx context.Context, key string, value interface{}) error {
ret := _m.Called(ctx, key, value)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, interface{}) error); ok {
r0 = rf(ctx, key, value)
} else {
r0 = ret.Error(0)
}
return r0
}
// Ping provides a mock function with given fields: ctx
func (_m *Cache) Ping(ctx context.Context) error {
ret := _m.Called(ctx)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
r0 = rf(ctx)
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: ctx, key, value, expiration
func (_m *Cache) Save(ctx context.Context, key string, value interface{}, expiration ...time.Duration) error {
_va := make([]interface{}, len(expiration))
for _i := range expiration {
_va[_i] = expiration[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, key, value)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, interface{}, ...time.Duration) error); ok {
r0 = rf(ctx, key, value, expiration...)
} else {
r0 = ret.Error(0)
}
return r0
}
// Scan provides a mock function with given fields: ctx, match
func (_m *Cache) Scan(ctx context.Context, match string) (cache.Iterator, error) {
ret := _m.Called(ctx, match)
var r0 cache.Iterator
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) (cache.Iterator, error)); ok {
return rf(ctx, match)
}
if rf, ok := ret.Get(0).(func(context.Context, string) cache.Iterator); ok {
r0 = rf(ctx, match)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(cache.Iterator)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, match)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
type mockConstructorTestingTNewCache interface {
mock.TestingT
Cleanup(func())
}
// NewCache creates a new instance of Cache. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewCache(t mockConstructorTestingTNewCache) *Cache {
mock := &Cache{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -231,6 +231,36 @@ func (_m *Manager) ListReferences(ctx context.Context, query *q.Query) ([]*artif
return r0, r1
}
// ListWithLatest provides a mock function with given fields: ctx, query
func (_m *Manager) ListWithLatest(ctx context.Context, query *q.Query) ([]*artifact.Artifact, error) {
ret := _m.Called(ctx, query)
if len(ret) == 0 {
panic("no return value specified for ListWithLatest")
}
var r0 []*artifact.Artifact
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) ([]*artifact.Artifact, error)); ok {
return rf(ctx, query)
}
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*artifact.Artifact); ok {
r0 = rf(ctx, query)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*artifact.Artifact)
}
}
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
}
// Update provides a mock function with given fields: ctx, _a1, props
func (_m *Manager) Update(ctx context.Context, _a1 *artifact.Artifact, props ...string) error {
_va := make([]interface{}, len(props))

View File

@ -1,38 +1,123 @@
// Code generated by mockery v2.43.2. DO NOT EDIT.
package artifactrash
import (
"context"
context "context"
"github.com/stretchr/testify/mock"
"github.com/goharbor/harbor/src/pkg/artifactrash/model"
model "github.com/goharbor/harbor/src/pkg/artifactrash/model"
mock "github.com/stretchr/testify/mock"
)
// FakeManager is a fake tag manager that implement the src/pkg/tag.Manager interface
type FakeManager struct {
// Manager is an autogenerated mock type for the Manager type
type Manager struct {
mock.Mock
}
// Create ...
func (f *FakeManager) Create(ctx context.Context, artifactrsh *model.ArtifactTrash) (id int64, err error) {
args := f.Called()
return int64(args.Int(0)), args.Error(1)
// Create provides a mock function with given fields: ctx, artifactrsh
func (_m *Manager) Create(ctx context.Context, artifactrsh *model.ArtifactTrash) (int64, error) {
ret := _m.Called(ctx, artifactrsh)
if len(ret) == 0 {
panic("no return value specified for Create")
}
var r0 int64
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *model.ArtifactTrash) (int64, error)); ok {
return rf(ctx, artifactrsh)
}
if rf, ok := ret.Get(0).(func(context.Context, *model.ArtifactTrash) int64); ok {
r0 = rf(ctx, artifactrsh)
} else {
r0 = ret.Get(0).(int64)
}
if rf, ok := ret.Get(1).(func(context.Context, *model.ArtifactTrash) error); ok {
r1 = rf(ctx, artifactrsh)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete ...
func (f *FakeManager) Delete(ctx context.Context, id int64) error {
args := f.Called()
return args.Error(0)
// 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)
if len(ret) == 0 {
panic("no return value specified for Delete")
}
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
}
// Filter ...
func (f *FakeManager) Filter(ctx context.Context, timeWindow int64) (arts []model.ArtifactTrash, err error) {
args := f.Called()
return args.Get(0).([]model.ArtifactTrash), args.Error(1)
// Filter provides a mock function with given fields: ctx, timeWindow
func (_m *Manager) Filter(ctx context.Context, timeWindow int64) ([]model.ArtifactTrash, error) {
ret := _m.Called(ctx, timeWindow)
if len(ret) == 0 {
panic("no return value specified for Filter")
}
var r0 []model.ArtifactTrash
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64) ([]model.ArtifactTrash, error)); ok {
return rf(ctx, timeWindow)
}
if rf, ok := ret.Get(0).(func(context.Context, int64) []model.ArtifactTrash); ok {
r0 = rf(ctx, timeWindow)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]model.ArtifactTrash)
}
}
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
r1 = rf(ctx, timeWindow)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Flush ...
func (f *FakeManager) Flush(ctx context.Context, timeWindow int64) (err error) {
args := f.Called()
return args.Error(0)
// Flush provides a mock function with given fields: ctx, timeWindow
func (_m *Manager) Flush(ctx context.Context, timeWindow int64) error {
ret := _m.Called(ctx, timeWindow)
if len(ret) == 0 {
panic("no return value specified for Flush")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
r0 = rf(ctx, timeWindow)
} else {
r0 = ret.Error(0)
}
return r0
}
// NewManager creates a new instance of Manager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewManager(t interface {
mock.TestingT
Cleanup(func())
}) *Manager {
mock := &Manager{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -1,33 +1,89 @@
// Code generated by mockery v2.43.2. DO NOT EDIT.
package chart
import (
"github.com/stretchr/testify/mock"
helm_chart "helm.sh/helm/v3/pkg/chart"
mock "github.com/stretchr/testify/mock"
chart "helm.sh/helm/v3/pkg/chart"
chartserver "github.com/goharbor/harbor/src/pkg/chart"
pkgchart "github.com/goharbor/harbor/src/pkg/chart"
)
// FakeOpertaor ...
type FakeOpertaor struct {
// Operator is an autogenerated mock type for the Operator type
type Operator struct {
mock.Mock
}
// GetDetails ...
func (f *FakeOpertaor) GetDetails(content []byte) (*chartserver.VersionDetails, error) {
args := f.Called()
var chartDetails *chartserver.VersionDetails
if args.Get(0) != nil {
chartDetails = args.Get(0).(*chartserver.VersionDetails)
// GetData provides a mock function with given fields: content
func (_m *Operator) GetData(content []byte) (*chart.Chart, error) {
ret := _m.Called(content)
if len(ret) == 0 {
panic("no return value specified for GetData")
}
return chartDetails, args.Error(1)
var r0 *chart.Chart
var r1 error
if rf, ok := ret.Get(0).(func([]byte) (*chart.Chart, error)); ok {
return rf(content)
}
if rf, ok := ret.Get(0).(func([]byte) *chart.Chart); ok {
r0 = rf(content)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*chart.Chart)
}
}
if rf, ok := ret.Get(1).(func([]byte) error); ok {
r1 = rf(content)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetData ...
func (f *FakeOpertaor) GetData(content []byte) (*helm_chart.Chart, error) {
args := f.Called()
var chartData *helm_chart.Chart
if args.Get(0) != nil {
chartData = args.Get(0).(*helm_chart.Chart)
// GetDetails provides a mock function with given fields: content
func (_m *Operator) GetDetails(content []byte) (*pkgchart.VersionDetails, error) {
ret := _m.Called(content)
if len(ret) == 0 {
panic("no return value specified for GetDetails")
}
return chartData, args.Error(1)
var r0 *pkgchart.VersionDetails
var r1 error
if rf, ok := ret.Get(0).(func([]byte) (*pkgchart.VersionDetails, error)); ok {
return rf(content)
}
if rf, ok := ret.Get(0).(func([]byte) *pkgchart.VersionDetails); ok {
r0 = rf(content)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*pkgchart.VersionDetails)
}
}
if rf, ok := ret.Get(1).(func([]byte) error); ok {
r1 = rf(content)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// NewOperator creates a new instance of Operator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewOperator(t interface {
mock.TestingT
Cleanup(func())
}) *Operator {
mock := &Operator{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -1,4 +1,4 @@
// Code generated by mockery v2.22.1. DO NOT EDIT.
// Code generated by mockery v2.43.2. DO NOT EDIT.
package distribution
@ -16,6 +16,10 @@ type Manifest struct {
func (_m *Manifest) Payload() (string, []byte, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Payload")
}
var r0 string
var r1 []byte
var r2 error
@ -49,6 +53,10 @@ func (_m *Manifest) Payload() (string, []byte, error) {
func (_m *Manifest) References() []distribution.Descriptor {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for References")
}
var r0 []distribution.Descriptor
if rf, ok := ret.Get(0).(func() []distribution.Descriptor); ok {
r0 = rf()
@ -61,13 +69,12 @@ func (_m *Manifest) References() []distribution.Descriptor {
return r0
}
type mockConstructorTestingTNewManifest interface {
// NewManifest creates a new instance of Manifest. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewManifest(t interface {
mock.TestingT
Cleanup(func())
}
// NewManifest creates a new instance of Manifest. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewManifest(t mockConstructorTestingTNewManifest) *Manifest {
}) *Manifest {
mock := &Manifest{}
mock.Mock.Test(t)

View File

@ -1,20 +1,58 @@
// Code generated by mockery v2.43.2. DO NOT EDIT.
package immutable
import (
"context"
context "context"
"github.com/stretchr/testify/mock"
mock "github.com/stretchr/testify/mock"
"github.com/goharbor/harbor/src/lib/selector"
selector "github.com/goharbor/harbor/src/lib/selector"
)
// FakeMatcher ...
// FakeMatcher is an autogenerated mock type for the ImmutableTagMatcher type
type FakeMatcher struct {
mock.Mock
}
// Match ...
func (f *FakeMatcher) Match(ctx context.Context, pid int64, c selector.Candidate) (bool, error) {
args := f.Called()
return args.Bool(0), args.Error(1)
// Match provides a mock function with given fields: ctx, pid, c
func (_m *FakeMatcher) Match(ctx context.Context, pid int64, c selector.Candidate) (bool, error) {
ret := _m.Called(ctx, pid, c)
if len(ret) == 0 {
panic("no return value specified for Match")
}
var r0 bool
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64, selector.Candidate) (bool, error)); ok {
return rf(ctx, pid, c)
}
if rf, ok := ret.Get(0).(func(context.Context, int64, selector.Candidate) bool); ok {
r0 = rf(ctx, pid, c)
} else {
r0 = ret.Get(0).(bool)
}
if rf, ok := ret.Get(1).(func(context.Context, int64, selector.Candidate) error); ok {
r1 = rf(ctx, pid, c)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// NewFakeMatcher creates a new instance of FakeMatcher. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewFakeMatcher(t interface {
mock.TestingT
Cleanup(func())
}) *FakeMatcher {
mock := &FakeMatcher{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Code generated by mockery v2.43.2. DO NOT EDIT.
package instance
@ -7,27 +7,35 @@ import (
mock "github.com/stretchr/testify/mock"
q "github.com/goharbor/harbor/src/lib/q"
provider "github.com/goharbor/harbor/src/pkg/p2p/preheat/models/provider"
q "github.com/goharbor/harbor/src/lib/q"
)
// FakeManager is an autogenerated mock type for the Manager type
type FakeManager struct {
// Manager is an autogenerated mock type for the Manager type
type Manager struct {
mock.Mock
}
// Count provides a mock function with given fields: ctx, query
func (_m *FakeManager) Count(ctx context.Context, query *q.Query) (int64, error) {
func (_m *Manager) Count(ctx context.Context, query *q.Query) (int64, error) {
ret := _m.Called(ctx, query)
if len(ret) == 0 {
panic("no return value specified for Count")
}
var r0 int64
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) (int64, error)); ok {
return rf(ctx, query)
}
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 {
@ -38,9 +46,13 @@ func (_m *FakeManager) Count(ctx context.Context, query *q.Query) (int64, error)
}
// Delete provides a mock function with given fields: ctx, id
func (_m *FakeManager) Delete(ctx context.Context, id int64) error {
func (_m *Manager) Delete(ctx context.Context, id int64) error {
ret := _m.Called(ctx, id)
if len(ret) == 0 {
panic("no return value specified for Delete")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
r0 = rf(ctx, id)
@ -52,10 +64,18 @@ func (_m *FakeManager) Delete(ctx context.Context, id int64) error {
}
// Get provides a mock function with given fields: ctx, id
func (_m *FakeManager) Get(ctx context.Context, id int64) (*provider.Instance, error) {
func (_m *Manager) Get(ctx context.Context, id int64) (*provider.Instance, error) {
ret := _m.Called(ctx, id)
if len(ret) == 0 {
panic("no return value specified for Get")
}
var r0 *provider.Instance
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64) (*provider.Instance, error)); ok {
return rf(ctx, id)
}
if rf, ok := ret.Get(0).(func(context.Context, int64) *provider.Instance); ok {
r0 = rf(ctx, id)
} else {
@ -64,7 +84,6 @@ func (_m *FakeManager) Get(ctx context.Context, id int64) (*provider.Instance, e
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
r1 = rf(ctx, id)
} else {
@ -75,10 +94,18 @@ func (_m *FakeManager) Get(ctx context.Context, id int64) (*provider.Instance, e
}
// GetByName provides a mock function with given fields: ctx, name
func (_m *FakeManager) GetByName(ctx context.Context, name string) (*provider.Instance, error) {
func (_m *Manager) GetByName(ctx context.Context, name string) (*provider.Instance, error) {
ret := _m.Called(ctx, name)
if len(ret) == 0 {
panic("no return value specified for GetByName")
}
var r0 *provider.Instance
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) (*provider.Instance, error)); ok {
return rf(ctx, name)
}
if rf, ok := ret.Get(0).(func(context.Context, string) *provider.Instance); ok {
r0 = rf(ctx, name)
} else {
@ -87,7 +114,6 @@ func (_m *FakeManager) GetByName(ctx context.Context, name string) (*provider.In
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, name)
} else {
@ -98,10 +124,18 @@ func (_m *FakeManager) GetByName(ctx context.Context, name string) (*provider.In
}
// List provides a mock function with given fields: ctx, query
func (_m *FakeManager) List(ctx context.Context, query *q.Query) ([]*provider.Instance, error) {
func (_m *Manager) List(ctx context.Context, query *q.Query) ([]*provider.Instance, error) {
ret := _m.Called(ctx, query)
if len(ret) == 0 {
panic("no return value specified for List")
}
var r0 []*provider.Instance
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) ([]*provider.Instance, error)); ok {
return rf(ctx, query)
}
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*provider.Instance); ok {
r0 = rf(ctx, query)
} else {
@ -110,7 +144,6 @@ func (_m *FakeManager) List(ctx context.Context, query *q.Query) ([]*provider.In
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
r1 = rf(ctx, query)
} else {
@ -121,17 +154,24 @@ func (_m *FakeManager) List(ctx context.Context, query *q.Query) ([]*provider.In
}
// Save provides a mock function with given fields: ctx, inst
func (_m *FakeManager) Save(ctx context.Context, inst *provider.Instance) (int64, error) {
func (_m *Manager) Save(ctx context.Context, inst *provider.Instance) (int64, error) {
ret := _m.Called(ctx, inst)
if len(ret) == 0 {
panic("no return value specified for Save")
}
var r0 int64
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *provider.Instance) (int64, error)); ok {
return rf(ctx, inst)
}
if rf, ok := ret.Get(0).(func(context.Context, *provider.Instance) int64); ok {
r0 = rf(ctx, inst)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *provider.Instance) error); ok {
r1 = rf(ctx, inst)
} else {
@ -142,7 +182,7 @@ func (_m *FakeManager) Save(ctx context.Context, inst *provider.Instance) (int64
}
// Update provides a mock function with given fields: ctx, inst, props
func (_m *FakeManager) Update(ctx context.Context, inst *provider.Instance, props ...string) error {
func (_m *Manager) Update(ctx context.Context, inst *provider.Instance, props ...string) error {
_va := make([]interface{}, len(props))
for _i := range props {
_va[_i] = props[_i]
@ -152,6 +192,10 @@ func (_m *FakeManager) Update(ctx context.Context, inst *provider.Instance, prop
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
if len(ret) == 0 {
panic("no return value specified for Update")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *provider.Instance, ...string) error); ok {
r0 = rf(ctx, inst, props...)
@ -161,3 +205,17 @@ func (_m *FakeManager) Update(ctx context.Context, inst *provider.Instance, prop
return r0
}
// NewManager creates a new instance of Manager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewManager(t interface {
mock.TestingT
Cleanup(func())
}) *Manager {
mock := &Manager{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -1,33 +1,40 @@
// Code generated by mockery v2.0.3. DO NOT EDIT.
// Code generated by mockery v2.43.2. DO NOT EDIT.
package policy
import (
context "context"
modelspolicy "github.com/goharbor/harbor/src/pkg/p2p/preheat/models/policy"
mock "github.com/stretchr/testify/mock"
q "github.com/goharbor/harbor/src/lib/q"
modelspolicy "github.com/goharbor/harbor/src/pkg/p2p/preheat/models/policy"
)
// FakeManager is an autogenerated mock type for the Manager type
type FakeManager struct {
// Manager is an autogenerated mock type for the Manager type
type Manager struct {
mock.Mock
}
// Count provides a mock function with given fields: ctx, query
func (_m *FakeManager) Count(ctx context.Context, query *q.Query) (int64, error) {
func (_m *Manager) Count(ctx context.Context, query *q.Query) (int64, error) {
ret := _m.Called(ctx, query)
if len(ret) == 0 {
panic("no return value specified for Count")
}
var r0 int64
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) (int64, error)); ok {
return rf(ctx, query)
}
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 {
@ -38,17 +45,24 @@ func (_m *FakeManager) Count(ctx context.Context, query *q.Query) (int64, error)
}
// Create provides a mock function with given fields: ctx, schema
func (_m *FakeManager) Create(ctx context.Context, schema *modelspolicy.Schema) (int64, error) {
func (_m *Manager) Create(ctx context.Context, schema *modelspolicy.Schema) (int64, error) {
ret := _m.Called(ctx, schema)
if len(ret) == 0 {
panic("no return value specified for Create")
}
var r0 int64
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *modelspolicy.Schema) (int64, error)); ok {
return rf(ctx, schema)
}
if rf, ok := ret.Get(0).(func(context.Context, *modelspolicy.Schema) int64); ok {
r0 = rf(ctx, schema)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *modelspolicy.Schema) error); ok {
r1 = rf(ctx, schema)
} else {
@ -59,9 +73,13 @@ func (_m *FakeManager) Create(ctx context.Context, schema *modelspolicy.Schema)
}
// Delete provides a mock function with given fields: ctx, id
func (_m *FakeManager) Delete(ctx context.Context, id int64) error {
func (_m *Manager) Delete(ctx context.Context, id int64) error {
ret := _m.Called(ctx, id)
if len(ret) == 0 {
panic("no return value specified for Delete")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
r0 = rf(ctx, id)
@ -73,10 +91,18 @@ func (_m *FakeManager) Delete(ctx context.Context, id int64) error {
}
// Get provides a mock function with given fields: ctx, id
func (_m *FakeManager) Get(ctx context.Context, id int64) (*modelspolicy.Schema, error) {
func (_m *Manager) Get(ctx context.Context, id int64) (*modelspolicy.Schema, error) {
ret := _m.Called(ctx, id)
if len(ret) == 0 {
panic("no return value specified for Get")
}
var r0 *modelspolicy.Schema
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64) (*modelspolicy.Schema, error)); ok {
return rf(ctx, id)
}
if rf, ok := ret.Get(0).(func(context.Context, int64) *modelspolicy.Schema); ok {
r0 = rf(ctx, id)
} else {
@ -85,7 +111,6 @@ func (_m *FakeManager) Get(ctx context.Context, id int64) (*modelspolicy.Schema,
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
r1 = rf(ctx, id)
} else {
@ -95,22 +120,29 @@ func (_m *FakeManager) Get(ctx context.Context, id int64) (*modelspolicy.Schema,
return r0, r1
}
// GetByName provides a mock function with given fields: ctx, projectId, name
func (_m *FakeManager) GetByName(ctx context.Context, projectId int64, name string) (*modelspolicy.Schema, error) {
ret := _m.Called(ctx, projectId, name)
// GetByName provides a mock function with given fields: ctx, projectID, name
func (_m *Manager) GetByName(ctx context.Context, projectID int64, name string) (*modelspolicy.Schema, error) {
ret := _m.Called(ctx, projectID, name)
if len(ret) == 0 {
panic("no return value specified for GetByName")
}
var r0 *modelspolicy.Schema
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64, string) (*modelspolicy.Schema, error)); ok {
return rf(ctx, projectID, name)
}
if rf, ok := ret.Get(0).(func(context.Context, int64, string) *modelspolicy.Schema); ok {
r0 = rf(ctx, projectId, name)
r0 = rf(ctx, projectID, name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*modelspolicy.Schema)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok {
r1 = rf(ctx, projectId, name)
r1 = rf(ctx, projectID, name)
} else {
r1 = ret.Error(1)
}
@ -119,10 +151,18 @@ func (_m *FakeManager) GetByName(ctx context.Context, projectId int64, name stri
}
// ListPolicies provides a mock function with given fields: ctx, query
func (_m *FakeManager) ListPolicies(ctx context.Context, query *q.Query) ([]*modelspolicy.Schema, error) {
func (_m *Manager) ListPolicies(ctx context.Context, query *q.Query) ([]*modelspolicy.Schema, error) {
ret := _m.Called(ctx, query)
if len(ret) == 0 {
panic("no return value specified for ListPolicies")
}
var r0 []*modelspolicy.Schema
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) ([]*modelspolicy.Schema, error)); ok {
return rf(ctx, query)
}
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*modelspolicy.Schema); ok {
r0 = rf(ctx, query)
} else {
@ -131,7 +171,6 @@ func (_m *FakeManager) ListPolicies(ctx context.Context, query *q.Query) ([]*mod
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
r1 = rf(ctx, query)
} else {
@ -142,10 +181,18 @@ func (_m *FakeManager) ListPolicies(ctx context.Context, query *q.Query) ([]*mod
}
// ListPoliciesByProject provides a mock function with given fields: ctx, project, query
func (_m *FakeManager) ListPoliciesByProject(ctx context.Context, project int64, query *q.Query) ([]*modelspolicy.Schema, error) {
func (_m *Manager) ListPoliciesByProject(ctx context.Context, project int64, query *q.Query) ([]*modelspolicy.Schema, error) {
ret := _m.Called(ctx, project, query)
if len(ret) == 0 {
panic("no return value specified for ListPoliciesByProject")
}
var r0 []*modelspolicy.Schema
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64, *q.Query) ([]*modelspolicy.Schema, error)); ok {
return rf(ctx, project, query)
}
if rf, ok := ret.Get(0).(func(context.Context, int64, *q.Query) []*modelspolicy.Schema); ok {
r0 = rf(ctx, project, query)
} else {
@ -154,7 +201,6 @@ func (_m *FakeManager) ListPoliciesByProject(ctx context.Context, project int64,
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int64, *q.Query) error); ok {
r1 = rf(ctx, project, query)
} else {
@ -165,7 +211,7 @@ func (_m *FakeManager) ListPoliciesByProject(ctx context.Context, project int64,
}
// Update provides a mock function with given fields: ctx, schema, props
func (_m *FakeManager) Update(ctx context.Context, schema *modelspolicy.Schema, props ...string) error {
func (_m *Manager) Update(ctx context.Context, schema *modelspolicy.Schema, props ...string) error {
_va := make([]interface{}, len(props))
for _i := range props {
_va[_i] = props[_i]
@ -175,6 +221,10 @@ func (_m *FakeManager) Update(ctx context.Context, schema *modelspolicy.Schema,
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
if len(ret) == 0 {
panic("no return value specified for Update")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *modelspolicy.Schema, ...string) error); ok {
r0 = rf(ctx, schema, props...)
@ -184,3 +234,17 @@ func (_m *FakeManager) Update(ctx context.Context, schema *modelspolicy.Schema,
return r0
}
// NewManager creates a new instance of Manager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewManager(t interface {
mock.TestingT
Cleanup(func())
}) *Manager {
mock := &Manager{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -1,13 +1,13 @@
// Code generated by mockery v2.1.0. DO NOT EDIT.
// Code generated by mockery v2.43.2. DO NOT EDIT.
package parser
import (
context "context"
mock "github.com/stretchr/testify/mock"
artifact "github.com/goharbor/harbor/src/pkg/artifact"
mock "github.com/stretchr/testify/mock"
)
// Parser is an autogenerated mock type for the Parser type
@ -19,6 +19,10 @@ type Parser struct {
func (_m *Parser) Parse(ctx context.Context, _a1 *artifact.Artifact, manifest []byte) error {
ret := _m.Called(ctx, _a1, manifest)
if len(ret) == 0 {
panic("no return value specified for Parse")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, []byte) error); ok {
r0 = rf(ctx, _a1, manifest)
@ -28,3 +32,17 @@ func (_m *Parser) Parse(ctx context.Context, _a1 *artifact.Artifact, manifest []
return r0
}
// NewParser creates a new instance of Parser. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewParser(t interface {
mock.TestingT
Cleanup(func())
}) *Parser {
mock := &Parser{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -1,14 +1,15 @@
// Code generated by mockery v2.1.0. DO NOT EDIT.
// Code generated by mockery v2.43.2. DO NOT EDIT.
package processor
import (
context "context"
artifact "github.com/goharbor/harbor/src/pkg/artifact"
mock "github.com/stretchr/testify/mock"
processor "github.com/goharbor/harbor/src/controller/artifact/processor"
artifact "github.com/goharbor/harbor/src/pkg/artifact"
)
// Processor is an autogenerated mock type for the Processor type
@ -20,7 +21,15 @@ type Processor struct {
func (_m *Processor) AbstractAddition(ctx context.Context, _a1 *artifact.Artifact, additionType string) (*processor.Addition, error) {
ret := _m.Called(ctx, _a1, additionType)
if len(ret) == 0 {
panic("no return value specified for AbstractAddition")
}
var r0 *processor.Addition
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, string) (*processor.Addition, error)); ok {
return rf(ctx, _a1, additionType)
}
if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, string) *processor.Addition); ok {
r0 = rf(ctx, _a1, additionType)
} else {
@ -29,7 +38,6 @@ func (_m *Processor) AbstractAddition(ctx context.Context, _a1 *artifact.Artifac
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *artifact.Artifact, string) error); ok {
r1 = rf(ctx, _a1, additionType)
} else {
@ -43,6 +51,10 @@ func (_m *Processor) AbstractAddition(ctx context.Context, _a1 *artifact.Artifac
func (_m *Processor) AbstractMetadata(ctx context.Context, _a1 *artifact.Artifact, manifest []byte) error {
ret := _m.Called(ctx, _a1, manifest)
if len(ret) == 0 {
panic("no return value specified for AbstractMetadata")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, []byte) error); ok {
r0 = rf(ctx, _a1, manifest)
@ -57,6 +69,10 @@ func (_m *Processor) AbstractMetadata(ctx context.Context, _a1 *artifact.Artifac
func (_m *Processor) GetArtifactType(ctx context.Context, _a1 *artifact.Artifact) string {
ret := _m.Called(ctx, _a1)
if len(ret) == 0 {
panic("no return value specified for GetArtifactType")
}
var r0 string
if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact) string); ok {
r0 = rf(ctx, _a1)
@ -71,6 +87,10 @@ func (_m *Processor) GetArtifactType(ctx context.Context, _a1 *artifact.Artifact
func (_m *Processor) ListAdditionTypes(ctx context.Context, _a1 *artifact.Artifact) []string {
ret := _m.Called(ctx, _a1)
if len(ret) == 0 {
panic("no return value specified for ListAdditionTypes")
}
var r0 []string
if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact) []string); ok {
r0 = rf(ctx, _a1)
@ -82,3 +102,17 @@ func (_m *Processor) ListAdditionTypes(ctx context.Context, _a1 *artifact.Artifa
return r0
}
// NewProcessor creates a new instance of Processor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewProcessor(t interface {
mock.TestingT
Cleanup(func())
}) *Processor {
mock := &Processor{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -1,256 +0,0 @@
// Code generated by mockery v2.1.0. DO NOT EDIT.
package scan
import (
context "context"
mock "github.com/stretchr/testify/mock"
q "github.com/goharbor/harbor/src/lib/q"
scan "github.com/goharbor/harbor/src/pkg/scan/dao/scan"
)
// VulnerabilityRecordDao is an autogenerated mock type for the VulnerabilityRecordDao type
type VulnerabilityRecordDao struct {
mock.Mock
}
// Create provides a mock function with given fields: ctx, vr
func (_m *VulnerabilityRecordDao) Create(ctx context.Context, vr *scan.VulnerabilityRecord) (int64, error) {
ret := _m.Called(ctx, vr)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context, *scan.VulnerabilityRecord) int64); ok {
r0 = rf(ctx, vr)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *scan.VulnerabilityRecord) error); ok {
r1 = rf(ctx, vr)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: ctx, vr
func (_m *VulnerabilityRecordDao) Delete(ctx context.Context, vr *scan.VulnerabilityRecord) error {
ret := _m.Called(ctx, vr)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *scan.VulnerabilityRecord) error); ok {
r0 = rf(ctx, vr)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteForDigests provides a mock function with given fields: ctx, digests
func (_m *VulnerabilityRecordDao) DeleteForDigests(ctx context.Context, digests ...string) (int64, error) {
_va := make([]interface{}, len(digests))
for _i := range digests {
_va[_i] = digests[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context, ...string) int64); ok {
r0 = rf(ctx, digests...)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, ...string) error); ok {
r1 = rf(ctx, digests...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteForReport provides a mock function with given fields: ctx, reportUUID
func (_m *VulnerabilityRecordDao) DeleteForReport(ctx context.Context, reportUUID string) (int64, error) {
ret := _m.Called(ctx, reportUUID)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context, string) int64); ok {
r0 = rf(ctx, reportUUID)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, reportUUID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteForScanner provides a mock function with given fields: ctx, registrationUUID
func (_m *VulnerabilityRecordDao) DeleteForScanner(ctx context.Context, registrationUUID string) (int64, error) {
ret := _m.Called(ctx, registrationUUID)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context, string) int64); ok {
r0 = rf(ctx, registrationUUID)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, registrationUUID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetForReport provides a mock function with given fields: ctx, reportUUID
func (_m *VulnerabilityRecordDao) GetForReport(ctx context.Context, reportUUID string) ([]*scan.VulnerabilityRecord, error) {
ret := _m.Called(ctx, reportUUID)
var r0 []*scan.VulnerabilityRecord
if rf, ok := ret.Get(0).(func(context.Context, string) []*scan.VulnerabilityRecord); ok {
r0 = rf(ctx, reportUUID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*scan.VulnerabilityRecord)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, reportUUID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetForScanner provides a mock function with given fields: ctx, registrationUUID
func (_m *VulnerabilityRecordDao) GetForScanner(ctx context.Context, registrationUUID string) ([]*scan.VulnerabilityRecord, error) {
ret := _m.Called(ctx, registrationUUID)
var r0 []*scan.VulnerabilityRecord
if rf, ok := ret.Get(0).(func(context.Context, string) []*scan.VulnerabilityRecord); ok {
r0 = rf(ctx, registrationUUID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*scan.VulnerabilityRecord)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, registrationUUID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetRecordIdsForScanner provides a mock function with given fields: ctx, registrationUUID
func (_m *VulnerabilityRecordDao) GetRecordIdsForScanner(ctx context.Context, registrationUUID string) ([]int, error) {
ret := _m.Called(ctx, registrationUUID)
var r0 []int
if rf, ok := ret.Get(0).(func(context.Context, string) []int); ok {
r0 = rf(ctx, registrationUUID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]int)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, registrationUUID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// InsertForReport provides a mock function with given fields: ctx, reportUUID, vulnerabilityRecordIDs
func (_m *VulnerabilityRecordDao) InsertForReport(ctx context.Context, reportUUID string, vulnerabilityRecordIDs ...int64) error {
_va := make([]interface{}, len(vulnerabilityRecordIDs))
for _i := range vulnerabilityRecordIDs {
_va[_i] = vulnerabilityRecordIDs[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, reportUUID)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, ...int64) error); ok {
r0 = rf(ctx, reportUUID, vulnerabilityRecordIDs...)
} else {
r0 = ret.Error(0)
}
return r0
}
// List provides a mock function with given fields: ctx, query
func (_m *VulnerabilityRecordDao) List(ctx context.Context, query *q.Query) ([]*scan.VulnerabilityRecord, error) {
ret := _m.Called(ctx, query)
var r0 []*scan.VulnerabilityRecord
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*scan.VulnerabilityRecord); ok {
r0 = rf(ctx, query)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*scan.VulnerabilityRecord)
}
}
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
}
// Update provides a mock function with given fields: ctx, vr, cols
func (_m *VulnerabilityRecordDao) Update(ctx context.Context, vr *scan.VulnerabilityRecord, cols ...string) error {
_va := make([]interface{}, len(cols))
for _i := range cols {
_va[_i] = cols[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, vr)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *scan.VulnerabilityRecord, ...string) error); ok {
r0 = rf(ctx, vr, cols...)
} else {
r0 = ret.Error(0)
}
return r0
}

View File

@ -0,0 +1,91 @@
// Code generated by mockery v2.43.2. DO NOT EDIT.
package postprocessors
import (
context "context"
mock "github.com/stretchr/testify/mock"
)
// NativeScanReportConverter is an autogenerated mock type for the NativeScanReportConverter type
type NativeScanReportConverter struct {
mock.Mock
}
// FromRelationalSchema provides a mock function with given fields: ctx, reportUUID, artifactDigest, reportSummary
func (_m *NativeScanReportConverter) FromRelationalSchema(ctx context.Context, reportUUID string, artifactDigest string, reportSummary string) (string, error) {
ret := _m.Called(ctx, reportUUID, artifactDigest, reportSummary)
if len(ret) == 0 {
panic("no return value specified for FromRelationalSchema")
}
var r0 string
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, string) (string, error)); ok {
return rf(ctx, reportUUID, artifactDigest, reportSummary)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string, string) string); ok {
r0 = rf(ctx, reportUUID, artifactDigest, reportSummary)
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok {
r1 = rf(ctx, reportUUID, artifactDigest, reportSummary)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ToRelationalSchema provides a mock function with given fields: ctx, reportUUID, registrationUUID, digest, reportData
func (_m *NativeScanReportConverter) ToRelationalSchema(ctx context.Context, reportUUID string, registrationUUID string, digest string, reportData string) (string, string, error) {
ret := _m.Called(ctx, reportUUID, registrationUUID, digest, reportData)
if len(ret) == 0 {
panic("no return value specified for ToRelationalSchema")
}
var r0 string
var r1 string
var r2 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) (string, string, error)); ok {
return rf(ctx, reportUUID, registrationUUID, digest, reportData)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) string); ok {
r0 = rf(ctx, reportUUID, registrationUUID, digest, reportData)
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string) string); ok {
r1 = rf(ctx, reportUUID, registrationUUID, digest, reportData)
} else {
r1 = ret.Get(1).(string)
}
if rf, ok := ret.Get(2).(func(context.Context, string, string, string, string) error); ok {
r2 = rf(ctx, reportUUID, registrationUUID, digest, reportData)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// NewNativeScanReportConverter creates a new instance of NativeScanReportConverter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewNativeScanReportConverter(t interface {
mock.TestingT
Cleanup(func())
}) *NativeScanReportConverter {
mock := &NativeScanReportConverter{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -1,23 +0,0 @@
package postprocessors
import (
"context"
mock "github.com/stretchr/testify/mock"
)
// ScanReportV1ToV2Converter is an auto-generated mock type for converting native Harbor report in JSON
// to relational schema
type ScanReportV1ToV2Converter struct {
mock.Mock
}
// ToRelationalSchema is a mock implementation of the scan report conversion
func (_c *ScanReportV1ToV2Converter) ToRelationalSchema(ctx context.Context, reportUUID string, registrationUUID string, digest string, reportData string) (string, string, error) {
return "mockId", reportData, nil
}
// ToRelationalSchema is a mock implementation of the scan report conversion
func (_c *ScanReportV1ToV2Converter) FromRelationalSchema(ctx context.Context, reportUUID string, artifactDigest string, reportData string) (string, error) {
return "mockId", nil
}

View File

@ -1,79 +1,208 @@
// 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.43.2. DO NOT EDIT.
package tag
import (
"context"
context "context"
"github.com/stretchr/testify/mock"
modeltag "github.com/goharbor/harbor/src/pkg/tag/model/tag"
mock "github.com/stretchr/testify/mock"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/tag/model/tag"
q "github.com/goharbor/harbor/src/lib/q"
)
// FakeManager is a fake tag manager that implement the src/pkg/tag.Manager interface
type FakeManager struct {
// Manager is an autogenerated mock type for the Manager type
type Manager struct {
mock.Mock
}
// Count ...
func (f *FakeManager) Count(ctx context.Context, query *q.Query) (int64, error) {
args := f.Called()
return int64(args.Int(0)), args.Error(1)
}
// 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)
// List ...
func (f *FakeManager) List(ctx context.Context, query *q.Query) ([]*tag.Tag, error) {
args := f.Called()
var tags []*tag.Tag
if args.Get(0) != nil {
tags = args.Get(0).([]*tag.Tag)
if len(ret) == 0 {
panic("no return value specified for Count")
}
return tags, args.Error(1)
}
// Get ...
func (f *FakeManager) Get(ctx context.Context, id int64) (*tag.Tag, error) {
args := f.Called()
var tg *tag.Tag
if args.Get(0) != nil {
tg = args.Get(0).(*tag.Tag)
var r0 int64
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) (int64, error)); ok {
return rf(ctx, query)
}
return tg, args.Error(1)
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) int64); ok {
r0 = rf(ctx, query)
} else {
r0 = ret.Get(0).(int64)
}
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 ...
func (f *FakeManager) Create(ctx context.Context, tag *tag.Tag) (int64, error) {
args := f.Called()
return int64(args.Int(0)), args.Error(1)
// Create provides a mock function with given fields: ctx, _a1
func (_m *Manager) Create(ctx context.Context, _a1 *modeltag.Tag) (int64, error) {
ret := _m.Called(ctx, _a1)
if len(ret) == 0 {
panic("no return value specified for Create")
}
var r0 int64
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *modeltag.Tag) (int64, error)); ok {
return rf(ctx, _a1)
}
if rf, ok := ret.Get(0).(func(context.Context, *modeltag.Tag) int64); ok {
r0 = rf(ctx, _a1)
} else {
r0 = ret.Get(0).(int64)
}
if rf, ok := ret.Get(1).(func(context.Context, *modeltag.Tag) error); ok {
r1 = rf(ctx, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update ...
func (f *FakeManager) Update(ctx context.Context, tag *tag.Tag, props ...string) error {
args := f.Called()
return args.Error(0)
// 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)
if len(ret) == 0 {
panic("no return value specified for Delete")
}
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
}
// Delete ...
func (f *FakeManager) Delete(ctx context.Context, id int64) error {
args := f.Called()
return args.Error(0)
// DeleteOfArtifact provides a mock function with given fields: ctx, artifactID
func (_m *Manager) DeleteOfArtifact(ctx context.Context, artifactID int64) error {
ret := _m.Called(ctx, artifactID)
if len(ret) == 0 {
panic("no return value specified for DeleteOfArtifact")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
r0 = rf(ctx, artifactID)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteOfArtifact ...
func (f *FakeManager) DeleteOfArtifact(ctx context.Context, artifactID int64) error {
args := f.Called()
return args.Error(0)
// Get provides a mock function with given fields: ctx, id
func (_m *Manager) Get(ctx context.Context, id int64) (*modeltag.Tag, error) {
ret := _m.Called(ctx, id)
if len(ret) == 0 {
panic("no return value specified for Get")
}
var r0 *modeltag.Tag
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64) (*modeltag.Tag, error)); ok {
return rf(ctx, id)
}
if rf, ok := ret.Get(0).(func(context.Context, int64) *modeltag.Tag); ok {
r0 = rf(ctx, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*modeltag.Tag)
}
}
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
r1 = rf(ctx, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// List provides a mock function with given fields: ctx, query
func (_m *Manager) List(ctx context.Context, query *q.Query) ([]*modeltag.Tag, error) {
ret := _m.Called(ctx, query)
if len(ret) == 0 {
panic("no return value specified for List")
}
var r0 []*modeltag.Tag
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) ([]*modeltag.Tag, error)); ok {
return rf(ctx, query)
}
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*modeltag.Tag); ok {
r0 = rf(ctx, query)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*modeltag.Tag)
}
}
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
}
// Update provides a mock function with given fields: ctx, _a1, props
func (_m *Manager) Update(ctx context.Context, _a1 *modeltag.Tag, props ...string) error {
_va := make([]interface{}, len(props))
for _i := range props {
_va[_i] = props[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, _a1)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
if len(ret) == 0 {
panic("no return value specified for Update")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *modeltag.Tag, ...string) error); ok {
r0 = rf(ctx, _a1, props...)
} else {
r0 = ret.Error(0)
}
return r0
}
// NewManager creates a new instance of Manager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewManager(t interface {
mock.TestingT
Cleanup(func())
}) *Manager {
mock := &Manager{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -1,24 +1,78 @@
// Code generated by mockery v2.43.2. DO NOT EDIT.
package registryctl
import (
"github.com/stretchr/testify/mock"
)
import mock "github.com/stretchr/testify/mock"
type Mockclient struct {
// Client is an autogenerated mock type for the Client type
type Client struct {
mock.Mock
}
// Health ...
func (c *Mockclient) Health() error {
return nil
// DeleteBlob provides a mock function with given fields: reference
func (_m *Client) DeleteBlob(reference string) error {
ret := _m.Called(reference)
if len(ret) == 0 {
panic("no return value specified for DeleteBlob")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(reference)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteBlob ...
func (c *Mockclient) DeleteBlob(reference string) (err error) {
return nil
// DeleteManifest provides a mock function with given fields: repository, reference
func (_m *Client) DeleteManifest(repository string, reference string) error {
ret := _m.Called(repository, reference)
if len(ret) == 0 {
panic("no return value specified for DeleteManifest")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(repository, reference)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteManifest ...
func (c *Mockclient) DeleteManifest(repository, reference string) (err error) {
return nil
// Health provides a mock function with given fields:
func (_m *Client) Health() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Health")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewClient(t interface {
mock.TestingT
Cleanup(func())
}) *Client {
mock := &Client{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -30,6 +30,8 @@ class Artifact(base.Base, object):
if "with_scan_overview" in kwargs:
params["with_scan_overview"] = kwargs["with_scan_overview"]
params["x_accept_vulnerabilities"] = ",".join(report_mime_types)
if "with_sbom_overview" in kwargs:
params["with_sbom_overview"] = kwargs["with_sbom_overview"]
if "with_immutable_status" in kwargs:
params["with_immutable_status"] = kwargs["with_immutable_status"]
if "with_accessory" in kwargs:
@ -140,6 +142,29 @@ class Artifact(base.Base, object):
return
raise Exception("Scan image result is {}, not as expected {}.".format(scan_status, expected_scan_status))
def check_image_sbom_generation_result(self, project_name, repo_name, reference, expected_scan_status = "Success", **kwargs):
timeout_count = 30
scan_status=""
while True:
time.sleep(5)
timeout_count = timeout_count - 1
if (timeout_count == 0):
break
artifact = self.get_reference_info(project_name, repo_name, reference, **kwargs)
if expected_scan_status in ["Not Scanned", "No SBOM Overview"]:
if artifact.sbom_overview is None:
if (timeout_count > 24):
continue
print("artifact SBOM is not generated.")
return
else:
raise Exception("Artifact SBOM should not be generated {}.".format(artifact.sbom_overview))
scan_status = artifact.sbom_overview.scan_status
if scan_status == expected_scan_status:
return
raise Exception("Generate image SBOM result is {}, not as expected {}.".format(scan_status, expected_scan_status))
def check_reference_exist(self, project_name, repo_name, reference, ignore_not_found = False, **kwargs):
artifact = self.get_reference_info( project_name, repo_name, reference, ignore_not_found=ignore_not_found, **kwargs)
return {

View File

@ -21,3 +21,18 @@ class Scan(base.Base, object):
base._assert_status_code(expect_status_code, status_code)
return data
def sbom_generation_of_artifact(self, project_name, repo_name, reference, expect_status_code = 202, expect_response_body = None, **kwargs):
try:
req_param = dict(scan_type = {"scan_type":"sbom"})
data, status_code, _ = self._get_client(**kwargs).scan_artifact_with_http_info(project_name, repo_name, reference, **req_param)
except ApiException as e:
base._assert_status_code(expect_status_code, e.status)
if expect_response_body is not None:
base._assert_status_body(expect_response_body, e.body)
return
base._assert_status_code(expect_status_code, status_code)
return data

View File

@ -22,4 +22,19 @@ class StopScan(base.Base, object):
base._assert_status_code(expect_status_code, status_code)
return data
def stop_sbom_generation_of_artifact(self, project_name, repo_name, reference, expect_status_code = 202, expect_response_body = None, **kwargs):
try:
scanType = v2_swagger_client.ScanType()
scanType.scan_type = "sbom"
data, status_code, _ = self._get_client(**kwargs).stop_scan_artifact_with_http_info(project_name, repo_name, reference, scanType)
except ApiException as e:
base._assert_status_code(expect_status_code, e.status)
if expect_response_body is not None:
base._assert_status_body(expect_response_body, e.body)
return
base._assert_status_code(expect_status_code, status_code)
return data

View File

@ -73,7 +73,7 @@ list_metadata = Permission("{}/projects/{}/metadatas".format(harbor_base_url, pr
read_metadata = Permission("{}/projects/{}/metadatas/auto_scan".format(harbor_base_url, project_id), "GET", 200, metadata_payload)
metadata_payload_for_update = { "auto_scan": "false" }
update_metadata = Permission("{}/projects/{}/metadatas/auto_scan".format(harbor_base_url, project_id), "PUT", 200, metadata_payload_for_update)
delete_metadata = Permission("{}/projects/{}/metadatas/auto_scan".format(harbor_base_url, project_id), "DELETE", 200, metadata_payload)
delete_metadata = Permission("{}/projects/{}/metadatas/auto_scan".format(harbor_base_url, project_id), "DELETE", 200, metadata_payload_for_update)
# 4. Resource: repository actions: ['read', 'list', 'update', 'delete', 'pull', 'push']
# note: pull and push are for docker cli, no API needs them
@ -89,12 +89,17 @@ copy_artifact = Permission("{}/projects/{}/repositories/target_repo/artifacts?fr
delete_artifact = Permission("{}/projects/{}/repositories/target_repo/artifacts/{}".format(harbor_base_url, project_name, source_artifact_tag), "DELETE", 200)
# 6. Resource scan actions: ['read', 'create', 'stop']
stop_scan_payload = {
vulnerability_scan_payload = {
"scan_type": "vulnerability"
}
create_scan = Permission("{}/projects/{}/repositories/{}/artifacts/{}/scan".format(harbor_base_url, project_name, source_artifact_name, source_artifact_tag), "POST", 202)
stop_scan = Permission("{}/projects/{}/repositories/{}/artifacts/{}/scan/stop".format(harbor_base_url, project_name, source_artifact_name, source_artifact_tag), "POST", 202, stop_scan_payload)
create_scan = Permission("{}/projects/{}/repositories/{}/artifacts/{}/scan".format(harbor_base_url, project_name, source_artifact_name, source_artifact_tag), "POST", 202, vulnerability_scan_payload)
stop_scan = Permission("{}/projects/{}/repositories/{}/artifacts/{}/scan/stop".format(harbor_base_url, project_name, source_artifact_name, source_artifact_tag), "POST", 202, vulnerability_scan_payload)
read_scan = Permission("{}/projects/{}/repositories/{}/artifacts/{}/scan/83be44fd-1234-5678-b49f-4b6d6e8f5730/log".format(harbor_base_url, project_name, source_artifact_name, source_artifact_tag), "get", 404)
sbom_gen_payload = {
"scan_type": "sbom"
}
create_sbom_generation = Permission("{}/projects/{}/repositories/{}/artifacts/{}/scan".format(harbor_base_url, project_name, source_artifact_name, source_artifact_tag), "POST", 202, sbom_gen_payload)
stop_sbom_generation = Permission("{}/projects/{}/repositories/{}/artifacts/{}/scan/stop".format(harbor_base_url, project_name, source_artifact_name, source_artifact_tag), "POST", 202, sbom_gen_payload)
# 7. Resource tag actions: ['list', 'create', 'delete']
tag_payload = { "name": "test-{}".format(int(random.randint(1000, 9999))) }
@ -240,7 +245,7 @@ resource_permissions = {
"metadata": [create_metadata, list_metadata, read_metadata, update_metadata, delete_metadata],
"repository": [list_repo, read_repo, update_repo, delete_repo],
"artifact": [list_artifact, read_artifact, copy_artifact, delete_artifact],
"scan": [create_scan, stop_scan, read_scan],
"scan": [create_scan, stop_scan, read_scan, create_sbom_generation, stop_sbom_generation],
"tag": [create_tag, list_tag, delete_tag],
"accessory": [list_accessory],
"artifact-addition": [read_artifact_addition_vul, read_artifact_addition_dependencies],

View File

@ -72,7 +72,7 @@ class TestProxyCache(unittest.TestCase):
index_for_ctr = dict(image = "alpine", tag = "3.12.0")
else:
user_namespace = "nightly"
registry = "https://cicd.harbor.vmwarecna.net"
registry = "https://registry.goharbor.io"
index_for_ctr = dict(image = "busybox", tag = "1.32.0")
registry_id, _ = self.registry.create_registry(registry, name=_random_name(registry_type), registry_type=registry_type, access_key = access_key, access_secret = access_secret, insecure=True, **ADMIN_CLIENT)

View File

@ -0,0 +1,87 @@
from __future__ import absolute_import
import unittest
import sys
from testutils import harbor_server, suppress_urllib3_warning
from testutils import TEARDOWN
from testutils import ADMIN_CLIENT, BASE_IMAGE, BASE_IMAGE_ABS_PATH_NAME
from library.project import Project
from library.user import User
from library.repository import Repository
from library.repository import push_self_build_image_to_project
from library.artifact import Artifact
from library.scan import Scan
class TestSBOMGeneration(unittest.TestCase):
@suppress_urllib3_warning
def setUp(self):
self.project= Project()
self.user= User()
self.artifact = Artifact()
self.repo = Repository()
self.scan = Scan()
self.url = ADMIN_CLIENT["endpoint"]
self.user_password = "Aa123456"
self.project_id, self.project_name, self.user_id, self.user_name, self.repo_name1 = [None] * 5
self.user_id, self.user_name = self.user.create_user(user_password = self.user_password, **ADMIN_CLIENT)
self.USER_CLIENT = dict(with_signature = True, with_immutable_status = True, endpoint = self.url, username = self.user_name, password = self.user_password, with_sbom_overview = True)
#2. Create a new private project(PA) by user(UA);
self.project_id, self.project_name = self.project.create_project(metadata = {"public": "false"}, **ADMIN_CLIENT)
#3. Add user(UA) as a member of project(PA) with project-admin role;
self.project.add_project_members(self.project_id, user_id = self.user_id, **ADMIN_CLIENT)
@unittest.skipIf(TEARDOWN == False, "Test data won't be erased.")
def do_tearDown(self):
#1. Delete repository(RA) by user(UA);
self.repo.delete_repository(self.project_name, self.repo_name1.split('/')[1], **self.USER_CLIENT)
#2. Delete project(PA);
self.project.delete_project(self.project_id, **self.USER_CLIENT)
#3. Delete user(UA);
self.user.delete_user(self.user_id, **ADMIN_CLIENT)
def testGenerateSBOMOfImageArtifact(self):
"""
Test case:
Generate an SBOM of An Image Artifact
Test step and expected result:
1. Create a new user(UA);
2. Create a new private project(PA) by user(UA);
3. Add user(UA) as a member of project(PA) with project-admin role;
4. Get private project of user(UA), user(UA) can see only one private project which is project(PA);
5. Create a new repository(RA) and tag(TA) in project(PA) by user(UA);
6. Send sbom generation of an image command and get tag(TA) information to check sbom generation result, it should be finished;
Tear down:
1. Delete repository(RA) by user(UA);
2. Delete project(PA);
3. Delete user(UA);
"""
#4. Get private project of user(UA), user(UA) can see only one private project which is project(PA);
self.project.projects_should_exist(dict(public=False), expected_count = 1,
expected_project_id = self.project_id, **self.USER_CLIENT)
#Note: Please make sure that this Image has never been pulled before by any other cases,
# so it is a not-scanned image right after repository creation.
image = "docker"
src_tag = "1.13"
#5. Create a new repository(RA) and tag(TA) in project(PA) by user(UA);
self.repo_name1, tag = push_self_build_image_to_project(self.project_name, harbor_server, self.user_name, self.user_password, image, src_tag)
#6. Send sbom generation of an image command and get tag(TA) information to check sbom generation result, it should be finished;
self.scan.sbom_generation_of_artifact(self.project_name, self.repo_name1.split('/')[1], tag, **self.USER_CLIENT)
self.artifact.check_image_sbom_generation_result(self.project_name, image, tag, **self.USER_CLIENT)
self.do_tearDown()
if __name__ == '__main__':
suite = unittest.TestSuite(unittest.makeSuite(TestSBOMGeneration))
result = unittest.TextTestRunner(sys.stdout, verbosity=2, failfast=True).run(suite)
if not result.wasSuccessful():
raise Exception(r"SBOM generation test failed: {}".format(result))

View File

@ -0,0 +1,91 @@
from __future__ import absolute_import
import unittest
import sys
from testutils import harbor_server, suppress_urllib3_warning
from testutils import TEARDOWN
from testutils import ADMIN_CLIENT, BASE_IMAGE, BASE_IMAGE_ABS_PATH_NAME
from library.project import Project
from library.user import User
from library.repository import Repository
from library.repository import push_self_build_image_to_project
from library.artifact import Artifact
from library.scan import Scan
from library.scan_stop import StopScan
class TestStopSBOMGeneration(unittest.TestCase):
@suppress_urllib3_warning
def setUp(self):
self.project= Project()
self.user= User()
self.artifact = Artifact()
self.repo = Repository()
self.scan = Scan()
self.stop_scan = StopScan()
self.url = ADMIN_CLIENT["endpoint"]
self.user_password = "Aa123456"
self.project_id, self.project_name, self.user_id, self.user_name, self.repo_name1 = [None] * 5
self.user_id, self.user_name = self.user.create_user(user_password = self.user_password, **ADMIN_CLIENT)
self.USER_CLIENT = dict(with_signature = True, with_immutable_status = True, endpoint = self.url, username = self.user_name, password = self.user_password, with_sbom_overview = True)
#2. Create a new private project(PA) by user(UA);
self.project_id, self.project_name = self.project.create_project(metadata = {"public": "false"}, **ADMIN_CLIENT)
#3. Add user(UA) as a member of project(PA) with project-admin role;
self.project.add_project_members(self.project_id, user_id = self.user_id, **ADMIN_CLIENT)
@unittest.skipIf(TEARDOWN == False, "Test data won't be erased.")
def do_tearDown(self):
#1. Delete repository(RA) by user(UA);
self.repo.delete_repository(self.project_name, self.repo_name1.split('/')[1], **self.USER_CLIENT)
#2. Delete project(PA);
self.project.delete_project(self.project_id, **self.USER_CLIENT)
#3. Delete user(UA);
self.user.delete_user(self.user_id, **ADMIN_CLIENT)
def testStopSBOMGenerationOfImageArtifact(self):
"""
Test case:
Stop SBOM Generation Of An Image Artifact
Test step and expected result:
1. Create a new user(UA);
2. Create a new private project(PA) by user(UA);
3. Add user(UA) as a member of project(PA) with project-admin role;
4. Get private project of user(UA), user(UA) can see only one private project which is project(PA);
5. Create a new repository(RA) and tag(TA) in project(PA) by user(UA);
6. Send SBOM generation of an image command;
7. Send stop SBOM generation of an image command.
Tear down:
1. Delete repository(RA) by user(UA);
2. Delete project(PA);
3. Delete user(UA);
"""
#4. Get private project of user(UA), user(UA) can see only one private project which is project(PA);
self.project.projects_should_exist(dict(public=False), expected_count = 1,
expected_project_id = self.project_id, **self.USER_CLIENT)
#Note: Please make sure that this Image has never been pulled before by any other cases,
# so it is a not-scanned image right after repository creation.
image = "docker"
src_tag = "1.13"
#5. Create a new repository(RA) and tag(TA) in project(PA) by user(UA);
self.repo_name1, tag = push_self_build_image_to_project(self.project_name, harbor_server, self.user_name, self.user_password, image, src_tag)
#6. Send SBOM generation of an image command;
self.scan.sbom_generation_of_artifact(self.project_name, self.repo_name1.split('/')[1], tag, **self.USER_CLIENT)
#7. Send stop SBOM generation of an image command.
self.stop_scan.stop_sbom_generation_of_artifact(self.project_name, self.repo_name1.split('/')[1], tag, **self.USER_CLIENT)
self.do_tearDown()
if __name__ == '__main__':
suite = unittest.TestSuite(unittest.makeSuite(TestStopSBOMGeneration))
result = unittest.TextTestRunner(sys.stdout, verbosity=2, failfast=True).run(suite)
if not result.wasSuccessful():
raise Exception(r"Stop SBOM generation test failed: {}".format(result))

View File

@ -41,9 +41,20 @@ Stop Scan Artifact
Retry Element Click ${stop_scan_artifact_btn}
Check Scan Artifact Job Status Is Stopped
Wait Until Element Is Visible ${stopped_label}
${job_status}= Get Text ${stopped_label}
Should Be Equal As Strings '${job_status}' 'Scan stopped'
Wait Until Element Is Visible ${scan_stopped_label}
Generate Artifact SBOM
[Arguments] ${project} ${repo} ${label_xpath}=//clr-dg-row//label[contains(@class,'clr-control-label')][1]
Go Into Repo ${project} ${repo}
Retry Element Click ${label_xpath}
Retry Element Click ${gen_artifact_sbom_btn}
Stop Gen Artifact SBOM
Retry Element Click ${artifact_action_xpath}
Retry Element Click ${stop_gen_artifact_sbom_btn}
Check Gen Artifact SBOM Job Status Is Stopped
Wait Until Element Is Visible ${gen_sbom_stopped_label}
Refresh Repositories
Retry Element Click ${refresh_repositories_xpath}

View File

@ -24,5 +24,8 @@ ${build_history_data} //clr-dg-row
${push_image_command_btn} //hbr-push-image-button//button
${scan_artifact_btn} //button[@id='scan-btn']
${stop_scan_artifact_btn} //button[@id='stop-scan']
${stopped_label} //span[@class='label stopped']
${scan_stopped_label} //span[normalize-space()='Scan stopped']
${gen_sbom_stopped_label} //span[normalize-space()='Generation stopped']
${gen_artifact_sbom_btn} //button[@id='generate-sbom-btn']
${stop_gen_artifact_sbom_btn} //button[@id='stop-sbom-btn']
${refresh_repositories_xpath} //hbr-repository-gridview//span[contains(@class,'refresh-btn')]

View File

@ -59,6 +59,41 @@ Enable Scan On Push
Checkbox Should Be Selected //clr-checkbox-wrapper[@id='scan-image-on-push-wrapper']//input
Retry Element Click ${project_config_save_btn}
Generate Repo SBOM
[Arguments] ${tagname} ${status}
Retry Element Click //clr-dg-row[contains(.,'${tagname}')]//label[contains(@class,'clr-control-label')]
Retry Element Click //button[@id='generate-sbom-btn']
Run Keyword If '${status}' == 'Succeed' Wait Until Element Is Visible //a[@title='SBOM details'] 300
Checkout And Review SBOM Details
[Arguments] ${tagname}
Retry Element Click //clr-dg-row[contains(.,'${tagname}')]//a[@title='SBOM details']
# Download SBOM file
Retry Element Click //button[@id='sbom-btn']
${sbom_artifact_short_sha256}= Get Text //span[@class='margin-left-10px']
${sbom_filename_raw}= Get Text //clr-dg-cell[contains(text(),'${sbom_artifact_short_sha256}')]
${sbom_filename}= Replace String ${sbom_filename_raw} : _ count=-1
${sbom_filename}= Replace String ${sbom_filename} / _ count=-1
${sbom_json_path}= Set Variable ${download_directory}/${sbom_filename}.json
Retry File Should Exist ${sbom_json_path}
# Load the downloaded SBOM json file and verify the first N package records
${sbom_json_content}= Load Json From File ${sbom_json_path}
${items}= Get Value From JSON ${sbom_json_content} packages
${items_length}= Get Length ${items}
${first_n_records}= Evaluate min(5, ${items_length})
FOR ${idx} IN RANGE 1 ${first_n_records}
${item}= Get From List ${items} ${idx}
Wait Until Element Is Visible //clr-dg-cell[normalize-space()='${item.name}']
Wait Until Element Is Visible //clr-dg-cell[normalize-space()='${item.versionInfo}']
Wait Until Element Is Visible //clr-dg-cell[normalize-space()='${item.licenseConcluded}']
END
Enable Generating SBOM On Push
Checkbox Should Not Be Selected //clr-checkbox-wrapper[@id='generate-sbom-on-push-wrapper']//input
Retry Element Click //clr-checkbox-wrapper[@id='generate-sbom-on-push-wrapper']//label[contains(@class,'clr-control-label')]
Checkbox Should Be Selected //clr-checkbox-wrapper[@id='generate-sbom-on-push-wrapper']//input
Retry Element Click ${project_config_save_btn}
Vulnerability Not Ready Project Hint
Sleep 2
${element}= Set Variable xpath=//span[contains(@class, 'db-status-warning')]

View File

@ -417,6 +417,49 @@ Stop Scan All
Stop Scan All Artifact
Retry Action Keyword Check Scan All Artifact Job Status Is Stopped
Body Of Generate SBOM of An Image In The Repo
[Arguments] ${image_argument} ${tag_argument}
Init Chrome Driver
${d}= get current date result_format=%m%s
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD}
Create An New Project And Go Into Project project${d}
Push Image ${ip} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} project${d} ${image_argument}:${tag_argument}
Go Into Repo project${d} ${image_argument}
Generate Repo SBOM ${tag_argument} Succeed
Checkout And Review SBOM Details ${tag_argument}
Close Browser
Body Of Generate Image SBOM On Push
Init Chrome Driver
${d}= get current date result_format=%m%s
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD}
Create An New Project And Go Into Project project${d}
Goto Project Config
Enable Generating SBOM On Push
Push Image ${ip} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} project${d} memcached
Go Into Repo project${d} memcached
Checkout And Review SBOM Details latest
Close Browser
Body Of Stop SBOM Manual Generation
Init Chrome Driver
${d}= get current date result_format=%m%s
${repo}= Set Variable goharbor/harbor-e2e-engine
${tag}= Set Variable test-ui
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD}
Create An New Project And Go Into Project project${d}
Push Image With Tag ${ip} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} project${d} ${repo} ${tag} ${tag}
# stop generate sbom of an artifact
Retry Action Keyword Stop SBOM Generation project${d} ${repo}
Close Browser
Stop SBOM Generation
[Arguments] ${project_name} ${repo}
Generate Artifact SBOM ${project_name} ${repo}
Stop Gen Artifact SBOM
Retry Action Keyword Check Gen Artifact SBOM Job Status Is Stopped
Prepare Image Package Test Files
[Arguments] ${files_path}
${rc} ${output}= Run And Return Rc And Output bash tests/robot-cases/Group0-Util/prepare_imgpkg_test_files.sh ${files_path}

View File

@ -120,6 +120,14 @@ Test Case - Stop Scan All Images
[Tags] stop_scan_all
Harbor API Test ./tests/apitests/python/test_system_level_stop_scan_all.py
Test Case - Generate SBOM Of An Image
[Tags] generate_sbom
Harbor API Test ./tests/apitests/python/test_sbom_generation_of_image_artifact.py
Test Case - Stop Generating SBOM Of An Image
[Tags] stop_generating_sbom
Harbor API Test ./tests/apitests/python/test_stop_sbom_generation_of_image_artifact.py
Test Case - Registry API
[Tags] reg_api
Harbor API Test ./tests/apitests/python/test_registry_api.py

View File

@ -244,7 +244,7 @@ Test Case - User View Logs
Create An New Project And Go Into Project project${d}
Logout Harbor
Body Of Replication Of Pull Images from Registry To Self harbor https://cicd.harbor.vmwarecna.net ${null} ${null} nightly/${replication_image} project${d} N Flatten 1 Level @{target_images}
Body Of Replication Of Pull Images from Registry To Self harbor https://${LOCAL_REGISTRY} ${null} ${null} nightly/${replication_image} project${d} N Flatten 1 Level @{target_images}
Push image ${ip} ${user} ${pwd} project${d} ${img}:${tag}
Pull image ${ip} ${user} ${pwd} project${d} ${replication_image}:${replication_tag}
@ -1118,7 +1118,7 @@ Test Case - Job Service Dashboard Workers
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD}
Create An New Project And Go Into Project ${project_name}
Switch to Registries
Create A New Endpoint harbor ${endpoint_name} https://cicd.harbor.vmwarecna.net ${null} ${null}
Create A New Endpoint harbor ${endpoint_name} https://${LOCAL_REGISTRY} ${null} ${null}
Switch To Replication Manage
Create A Rule With Existing Endpoint ${rule_name} pull nightly/test_replication image ${endpoint_name} ${project_name} bandwidth=50 bandwidth_unit=Mbps
Select Rule And Replicate ${rule_name}

View File

@ -119,7 +119,7 @@ Test Case - Ldap Group Admin DN Setting
Logout Harbor
Sign In Harbor ${HARBOR_URL} mike zhu88jie
Switch To Registries
Create A New Endpoint harbor edp1${d} https://cicd.harbor.vmwarecna.net ${null} ${null} Y
Create A New Endpoint harbor edp1${d} https://${LOCAL_REGISTRY} ${null} ${null} Y
Test Case - Run LDAP Group Related API Test

View File

@ -120,7 +120,7 @@ Test Case - OIDC Group User
${pwd}= Set Variable ${admin_pwd}
Sign In Harbor With OIDC User ${HARBOR_URL} username=${admin_user} password=${admin_pwd} login_with_provider=ldap
Switch To Registries
Create A New Endpoint harbor test_oidc_admin https://cicd.harbor.vmwarecna.net ${null} ${null} Y
Create A New Endpoint harbor test_oidc_admin https://${LOCAL_REGISTRY} ${null} ${null} Y
${secret}= Get Secrete By API ${HARBOR_URL} username=${admin_user}
Push image ${ip} ${admin_user} ${secret} library ${image}
Logout Harbor

View File

@ -93,7 +93,7 @@ Test Case - Replication Rule Edit
${cron_str}= Set Variable 0 0 0 * * 0
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD}
Switch To Registries
Create A New Endpoint harbor ${endpoint1} https://cicd.harbor.vmwarecna.net ${null} ${null} Y
Create A New Endpoint harbor ${endpoint1} https://${LOCAL_REGISTRY} ${null} ${null} Y
Create A New Endpoint harbor ${endpoint2} https://${ip} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} Y
Switch To Replication Manage
Create A Rule With Existing Endpoint ${rule_name_old} pull nightly/a* image ${endpoint1} project${d}
@ -312,7 +312,7 @@ Test Case - Replication Of Pull Manifest List and CNAB from Harbor To Self
${image2}= Get From Dictionary ${image2_with_tag} image
${image3}= Get From Dictionary ${image3_with_tag} image
@{target_images}= Create List '&{image1_with_tag}' '&{image2_with_tag}' '&{image3_with_tag}'
Body Of Replication Of Pull Images from Registry To Self harbor https://cicd.harbor.vmwarecna.net ${null} ${null} nightly/{${image1},${image2},${image3}} ${null} Y Flatten 1 Level @{target_images}
Body Of Replication Of Pull Images from Registry To Self harbor https://${LOCAL_REGISTRY} ${null} ${null} nightly/{${image1},${image2},${image3}} ${null} Y Flatten 1 Level @{target_images}
Test Case - Image Namespace Level Flattening
[tags] flattening

View File

@ -28,7 +28,7 @@ ${HARBOR_ADMIN} admin
Test Case - Proxy Cache
[Tags] proxy_cache
${d}= Get Current Date result_format=%m%s
${registry}= Set Variable https://cicd.harbor.vmwarecna.net
${registry}= Set Variable https://${LOCAL_REGISTRY}
${user_namespace}= Set Variable nightly
${image}= Set Variable for_proxy
${tag}= Set Variable 1.0
@ -156,7 +156,7 @@ Test Case - Replication Schedule Job
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD}
Create An New Project And Go Into Project ${project_name}
Switch To Registries
Create A New Endpoint harbor e${d} https://cicd.harbor.vmwarecna.net ${null} ${null} Y
Create A New Endpoint harbor e${d} https://${LOCAL_REGISTRY} ${null} ${null} Y
Switch To Replication Manage
${flag}= Set Variable ${false}
FOR ${i} IN RANGE 999999

Some files were not shown because too many files have changed in this diff Show More