Cosign artifact api

1,update artifact list & delete api to support accessory
2, add list accesories api

Signed-off-by: Wang Yan <wangyan@vmware.com>
This commit is contained in:
Wang Yan 2022-01-05 11:13:40 +08:00 committed by GitHub
parent 3c0a5a936f
commit 2111703d8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 525 additions and 54 deletions

View File

@ -1000,7 +1000,13 @@ paths:
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_tag=true"
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
@ -1091,6 +1097,12 @@ paths:
type: boolean
required: false
default: false
- name: with_accessory
in: query
description: Specify whether the accessories are included of the returning artifacts.
type: boolean
required: false
default: false
# should be in tag level
- name: with_signature
in: query
@ -1100,7 +1112,7 @@ paths:
default: false
- name: with_immutable_status
in: query
description: Specify whether the immutable status is inclued inside the tags of the returning artifacts. Only works when setting "with_tag=true"
description: Specify whether the immutable status is inclued inside the tags of the returning artifacts.
type: boolean
required: false
default: false
@ -1333,6 +1345,46 @@ paths:
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
/projects/{project_name}/repositories/{repository_name}/artifacts/{reference}/accessories:
get:
summary: List accessories
description: List accessories of the specific artifact
tags:
- artifact
operationId: listAccessories
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectName'
- $ref: '#/parameters/repositoryName'
- $ref: '#/parameters/reference'
- $ref: '#/parameters/query'
- $ref: '#/parameters/sort'
- $ref: '#/parameters/page'
- $ref: '#/parameters/pageSize'
responses:
'200':
description: Success
headers:
X-Total-Count:
description: The total count of accessories
type: integer
Link:
description: Link refers to the previous page and next page
type: string
schema:
type: array
items:
$ref: '#/definitions/Accessory'
'400':
$ref: '#/responses/400'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
/projects/{project_name}/repositories/{repository_name}/artifacts/{reference}/additions/vulnerabilities:
get:
summary: Get the vulnerabilities addition of the specific artifact
@ -5487,6 +5539,13 @@ parameters:
required: true
type: integer
format: int64
accessoryId:
name: accessory_id
in: path
description: The ID of the accessory
required: true
type: integer
format: int64
responses:
'200':
@ -5827,6 +5886,11 @@ definitions:
scan_overview:
$ref: '#/definitions/ScanOverview'
description: The overview of the scan result.
accessories:
type: array
items:
$ref: '#/definitions/Accessory'
description: The accessory of the artifact.
Tag:
type: object
properties:
@ -8657,3 +8721,42 @@ definitions:
format: int64
description: The total storage consumption of blobs, only be seen by the system admin
x-omitempty: false
Accessory:
type: object
description: 'The accessory of the artifact'
properties:
id:
type: integer
format: int64
description: The ID of the accessory
artifact_id:
type: integer
format: int64
description: The artifact id of the accessory
x-omitempty: false
subject_artifact_id:
type: integer
format: int64
description: The subject artifact id of the accessory
x-omitempty: false
size:
type: integer
format: int64
description: The artifact size of the accessory
x-omitempty: false
digest:
type: string
description: The artifact digest of the accessory
x-omitempty: false
type:
type: string
description: The artifact size of the accessory
x-omitempty: false
icon:
type: string
description: The icon of the accessory
x-omitempty: false
creation_time:
type: string
format: date-time
description: The creation time of the accessory

View File

@ -55,6 +55,7 @@ const (
ResourceScanner = Resource("scanner")
ResourceArtifact = Resource("artifact")
ResourceTag = Resource("tag")
ResourceAccessory = Resource("accessory")
ResourceArtifactAddition = Resource("artifact-addition")
ResourceArtifactLabel = Resource("artifact-label")
ResourcePreatPolicy = Resource("preheat-policy")

View File

@ -113,6 +113,8 @@ var (
{Resource: rbac.ResourceTag, Action: rbac.ActionCreate},
{Resource: rbac.ResourceTag, Action: rbac.ActionDelete},
{Resource: rbac.ResourceAccessory, Action: rbac.ActionList},
{Resource: rbac.ResourceArtifactLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceArtifactLabel, Action: rbac.ActionDelete},
@ -159,6 +161,8 @@ var (
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionList},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionOperate},
{Resource: rbac.ResourceAccessory, Action: rbac.ActionList},
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionCreate},
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionDelete},
@ -252,6 +256,8 @@ var (
{Resource: rbac.ResourceTag, Action: rbac.ActionList},
{Resource: rbac.ResourceTag, Action: rbac.ActionCreate},
{Resource: rbac.ResourceAccessory, Action: rbac.ActionList},
{Resource: rbac.ResourceArtifactLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceArtifactLabel, Action: rbac.ActionDelete},
},
@ -289,6 +295,7 @@ var (
{Resource: rbac.ResourceScanner, Action: rbac.ActionRead},
{Resource: rbac.ResourceTag, Action: rbac.ActionList},
{Resource: rbac.ResourceAccessory, Action: rbac.ActionList},
{Resource: rbac.ResourceArtifact, Action: rbac.ActionRead},
{Resource: rbac.ResourceArtifact, Action: rbac.ActionList},
@ -316,6 +323,7 @@ var (
{Resource: rbac.ResourceScanner, Action: rbac.ActionRead},
{Resource: rbac.ResourceTag, Action: rbac.ActionList},
{Resource: rbac.ResourceAccessory, Action: rbac.ActionList},
{Resource: rbac.ResourceArtifact, Action: rbac.ActionRead},
{Resource: rbac.ResourceArtifact, Action: rbac.ActionList},

View File

@ -35,6 +35,7 @@ import (
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/accessory"
"github.com/goharbor/harbor/src/pkg/artifact"
"github.com/goharbor/harbor/src/pkg/artifactrash"
"github.com/goharbor/harbor/src/pkg/artifactrash/model"
@ -121,6 +122,7 @@ func NewController() Controller {
immutableMtr: rule.NewRuleMatcher(),
regCli: registry.Cli,
abstractor: NewAbstractor(),
accessoryMgr: accessory.Mgr,
}
}
@ -135,6 +137,7 @@ type controller struct {
immutableMtr match.ImmutableTagMatcher
regCli registry.Client
abstractor Abstractor
accessoryMgr accessory.Manager
}
func (c *controller) Ensure(ctx context.Context, repository, digest string, tags ...string) (bool, int64, error) {
@ -229,11 +232,18 @@ func (c *controller) List(ctx context.Context, query *q.Query, option *Option) (
return nil, err
}
var artifacts []*Artifact
var res []*Artifact
// Only the displayed accessory will in the artifact list
for _, art := range arts {
artifacts = append(artifacts, c.assembleArtifact(ctx, art, option))
accs, err := c.accessoryMgr.List(ctx, q.New(q.KeyWords{"ArtifactID": art.ID, "digest": art.Digest}))
if err != nil {
return nil, err
}
if len(accs) == 0 || (len(accs) > 0 && accs[0].Display()) {
res = append(res, c.assembleArtifact(ctx, art, option))
}
}
return artifacts, nil
return res, nil
}
func (c *controller) Get(ctx context.Context, id int64, option *Option) (*Artifact, error) {
@ -283,13 +293,18 @@ func (c *controller) getByTag(ctx context.Context, repository, tag string, optio
}
func (c *controller) Delete(ctx context.Context, id int64) error {
return c.deleteDeeply(ctx, id, true)
accs, err := c.accessoryMgr.List(ctx, q.New(q.KeyWords{"ArtifactID": id}))
if err != nil {
return err
}
return c.deleteDeeply(ctx, id, true, len(accs) > 0)
}
// "isRoot" is used to specify whether the artifact is the root parent artifact
// the error handling logic for the root parent artifact and others is different
func (c *controller) deleteDeeply(ctx context.Context, id int64, isRoot bool) error {
art, err := c.Get(ctx, id, &Option{WithTag: true})
// "isAccessory" is used to specify whether the artifact is an accessory.
func (c *controller) deleteDeeply(ctx context.Context, id int64, isRoot, isAccessory bool) error {
art, err := c.Get(ctx, id, &Option{WithTag: true, WithAccessory: true})
if err != nil {
// return nil if the nonexistent artifact isn't the root parent
if !isRoot && errors.IsErr(err, errors.NotFoundCode) {
@ -298,6 +313,12 @@ func (c *controller) deleteDeeply(ctx context.Context, id int64, isRoot bool) er
return err
}
if isAccessory {
if err := c.accessoryMgr.DeleteAccessories(ctx, q.New(q.KeyWords{"ArtifactID": art.ID, "Digest": art.Digest})); err != nil && !errors.IsErr(err, errors.NotFoundCode) {
return err
}
}
// the child artifact is referenced by some tags, skip
if !isRoot && len(art.Tags) > 0 {
return nil
@ -319,6 +340,17 @@ func (c *controller) deleteDeeply(ctx context.Context, id int64, isRoot bool) er
// the child artifact is referenced by other artifacts, skip
return nil
}
// delete accessories if contains any
for _, acc := range art.Accessories {
// only hard ref accessory should be removed
if acc.IsHard() {
if err = c.deleteDeeply(ctx, acc.GetData().ArtifactID, true, true); err != nil {
return err
}
}
}
// delete child artifacts if contains any
for _, reference := range art.References {
// delete reference
@ -326,7 +358,7 @@ func (c *controller) deleteDeeply(ctx context.Context, id int64, isRoot bool) er
!errors.IsErr(err, errors.NotFoundCode) {
return err
}
if err = c.deleteDeeply(ctx, reference.ChildID, false); err != nil {
if err = c.deleteDeeply(ctx, reference.ChildID, false, false); err != nil {
return err
}
}
@ -582,6 +614,9 @@ func (c *controller) assembleArtifact(ctx context.Context, art *artifact.Artifac
if option.WithLabel {
c.populateLabels(ctx, artifact)
}
if option.WithAccessory {
c.populateAccessories(ctx, artifact)
}
return artifact
}
@ -626,3 +661,12 @@ func (c *controller) populateAdditionLinks(ctx context.Context, artifact *Artifa
}
}
}
func (c *controller) populateAccessories(ctx context.Context, art *Artifact) {
accs, err := c.accessoryMgr.List(ctx, q.New(q.KeyWords{"SubjectArtifactID": art.ID}))
if err != nil {
log.Errorf("failed to list accessories of artifact %d: %v", art.ID, err)
return
}
art.Accessories = accs
}

View File

@ -28,12 +28,16 @@ import (
"github.com/goharbor/harbor/src/lib/icon"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/lib/q"
accessorymodel "github.com/goharbor/harbor/src/pkg/accessory/model"
basemodel "github.com/goharbor/harbor/src/pkg/accessory/model/base"
"github.com/goharbor/harbor/src/pkg/artifact"
"github.com/goharbor/harbor/src/pkg/label/model"
repomodel "github.com/goharbor/harbor/src/pkg/repository/model"
model_tag "github.com/goharbor/harbor/src/pkg/tag/model/tag"
tagtesting "github.com/goharbor/harbor/src/testing/controller/tag"
ormtesting "github.com/goharbor/harbor/src/testing/lib/orm"
"github.com/goharbor/harbor/src/testing/pkg/accessory"
accessorytesting "github.com/goharbor/harbor/src/testing/pkg/accessory"
arttesting "github.com/goharbor/harbor/src/testing/pkg/artifact"
artrashtesting "github.com/goharbor/harbor/src/testing/pkg/artifactrash"
"github.com/goharbor/harbor/src/testing/pkg/blob"
@ -69,6 +73,7 @@ type controllerTestSuite struct {
abstractor *fakeAbstractor
immutableMtr *immutable.FakeMatcher
regCli *registry.FakeClient
accMgr *accessory.Manager
}
func (c *controllerTestSuite) SetupTest() {
@ -80,6 +85,7 @@ func (c *controllerTestSuite) SetupTest() {
c.labelMgr = &label.Manager{}
c.abstractor = &fakeAbstractor{}
c.immutableMtr = &immutable.FakeMatcher{}
c.accMgr = &accessorytesting.Manager{}
c.regCli = &registry.FakeClient{}
c.ctl = &controller{
repoMgr: c.repoMgr,
@ -91,6 +97,7 @@ func (c *controllerTestSuite) SetupTest() {
abstractor: c.abstractor,
immutableMtr: c.immutableMtr,
regCli: c.regCli,
accessoryMgr: c.accMgr,
}
}
@ -105,7 +112,8 @@ func (c *controllerTestSuite) TestAssembleArtifact() {
TagOption: &tag.Option{
WithImmutableStatus: false,
},
WithLabel: true,
WithLabel: true,
WithAccessory: true,
}
tg := &tag.Tag{
Tag: model_tag.Tag{
@ -126,12 +134,24 @@ func (c *controllerTestSuite) TestAssembleArtifact() {
c.labelMgr.On("ListByArtifact", mock.Anything, mock.Anything).Return([]*model.Label{
lb,
}, nil)
acc := &basemodel.Default{
Data: accessorymodel.AccessoryData{
ID: 1,
ArtifactID: 2,
SubArtifactID: 1,
Type: accessorymodel.TypeCosignSignature,
},
}
c.accMgr.On("List", mock.Anything, mock.Anything).Return([]accessorymodel.Accessory{
acc,
}, nil)
artifact := c.ctl.assembleArtifact(ctx, art, option)
c.Require().NotNil(artifact)
c.Equal(art.ID, artifact.ID)
c.Equal(icon.DigestOfIconDefault, artifact.Icon)
c.Contains(artifact.Tags, tg)
c.Contains(artifact.Labels, lb)
c.Contains(artifact.Accessories, acc)
// TODO check other fields of option
}
@ -249,7 +269,8 @@ func (c *controllerTestSuite) TestCount() {
func (c *controllerTestSuite) TestList() {
query := &q.Query{}
option := &Option{
WithTag: true,
WithTag: true,
WithAccessory: true,
}
c.artMgr.On("List", mock.Anything, mock.Anything).Return([]*artifact.Artifact{
{
@ -273,12 +294,14 @@ func (c *controllerTestSuite) TestList() {
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.List(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() {
@ -406,7 +429,8 @@ func (c *controllerTestSuite) TestGetByReference() {
func (c *controllerTestSuite) TestDeleteDeeply() {
// root artifact and doesn't exist
c.artMgr.On("Get", mock.Anything, mock.Anything).Return(nil, errors.NotFoundError(nil))
err := c.ctl.deleteDeeply(orm.NewContext(nil, &ormtesting.FakeOrmer{}), 1, true)
c.accMgr.On("List", mock.Anything, mock.Anything).Return([]accessorymodel.Accessory{}, nil)
err := c.ctl.deleteDeeply(orm.NewContext(nil, &ormtesting.FakeOrmer{}), 1, true, false)
c.Require().NotNil(err)
c.Assert().True(errors.IsErr(err, errors.NotFoundCode))
@ -415,7 +439,8 @@ func (c *controllerTestSuite) TestDeleteDeeply() {
// child artifact and doesn't exist
c.artMgr.On("Get", mock.Anything, mock.Anything).Return(nil, errors.NotFoundError(nil))
err = c.ctl.deleteDeeply(orm.NewContext(nil, &ormtesting.FakeOrmer{}), 1, false)
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)
// reset the mock
@ -433,7 +458,8 @@ func (c *controllerTestSuite) TestDeleteDeeply() {
}, nil)
c.repoMgr.On("Get", mock.Anything, mock.Anything).Return(&repomodel.RepoRecord{}, nil)
c.artrashMgr.On("Create").Return(0, nil)
err = c.ctl.deleteDeeply(orm.NewContext(nil, &ormtesting.FakeOrmer{}), 1, false)
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)
// reset the mock
@ -448,7 +474,8 @@ func (c *controllerTestSuite) TestDeleteDeeply() {
ID: 1,
},
}, nil)
err = c.ctl.deleteDeeply(orm.NewContext(nil, &ormtesting.FakeOrmer{}), 1, true)
c.accMgr.On("List", mock.Anything, mock.Anything).Return([]accessorymodel.Accessory{}, nil)
err = c.ctl.deleteDeeply(orm.NewContext(nil, &ormtesting.FakeOrmer{}), 1, true, false)
c.Require().NotNil(err)
// reset the mock
@ -463,8 +490,35 @@ func (c *controllerTestSuite) TestDeleteDeeply() {
ID: 1,
},
}, nil)
err = c.ctl.deleteDeeply(nil, 1, false)
c.accMgr.On("List", mock.Anything, mock.Anything).Return([]accessorymodel.Accessory{}, nil)
err = c.ctl.deleteDeeply(nil, 1, false, false)
c.Require().Nil(err)
// reset the mock
c.SetupTest()
// accessory contains tag
c.artMgr.On("Get", mock.Anything, mock.Anything).Return(&artifact.Artifact{ID: 1}, nil)
c.artMgr.On("Delete", mock.Anything, mock.Anything).Return(nil)
c.tagCtl.On("List").Return([]*tag.Tag{
{
Tag: model_tag.Tag{
ID: 1,
},
},
}, nil)
c.tagCtl.On("DeleteTags", mock.Anything, mock.Anything).Return(nil)
c.labelMgr.On("RemoveAllFrom", mock.Anything, mock.Anything).Return(nil)
c.artMgr.On("ListReferences", mock.Anything, mock.Anything).Return([]*artifact.Reference{}, nil)
c.accMgr.On("List", mock.Anything, mock.Anything).Return([]accessorymodel.Accessory{}, nil)
c.accMgr.On("DeleteAccessories", mock.Anything, mock.Anything).Return(nil)
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)
err = c.ctl.deleteDeeply(orm.NewContext(nil, &ormtesting.FakeOrmer{}), 1, true, true)
c.Require().Nil(err)
}
func (c *controllerTestSuite) TestCopy() {
@ -554,6 +608,7 @@ func (c *controllerTestSuite) TestWalk() {
{Digest: "d1", ManifestMediaType: v1.MediaTypeImageManifest},
{Digest: "d2", ManifestMediaType: v1.MediaTypeImageManifest},
}, nil)
c.accMgr.On("List", mock.Anything, mock.Anything).Return([]accessorymodel.Accessory{}, nil)
{
root := &Artifact{}

View File

@ -16,10 +16,13 @@ package artifact
import (
"context"
accessorymodel "github.com/goharbor/harbor/src/pkg/accessory/model"
"testing"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/artifact"
"github.com/goharbor/harbor/src/testing/pkg/accessory"
accessorytesting "github.com/goharbor/harbor/src/testing/pkg/accessory"
artifacttesting "github.com/goharbor/harbor/src/testing/pkg/artifact"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
@ -29,6 +32,7 @@ type IteratorTestSuite struct {
suite.Suite
artMgr *artifacttesting.Manager
accMgr *accessory.Manager
ctl *controller
originalCtl Controller
@ -36,9 +40,13 @@ type IteratorTestSuite struct {
func (suite *IteratorTestSuite) SetupSuite() {
suite.artMgr = &artifacttesting.Manager{}
suite.accMgr = &accessorytesting.Manager{}
suite.originalCtl = Ctl
suite.ctl = &controller{artMgr: suite.artMgr}
suite.ctl = &controller{
artMgr: suite.artMgr,
accessoryMgr: suite.accMgr,
}
Ctl = suite.ctl
}
@ -47,6 +55,7 @@ func (suite *IteratorTestSuite) TeardownSuite() {
}
func (suite *IteratorTestSuite) TestIterator() {
suite.accMgr.On("List", mock.Anything, mock.Anything).Return([]accessorymodel.Accessory{}, nil)
q1 := &q.Query{PageNumber: 1, PageSize: 5, Keywords: map[string]interface{}{}}
suite.artMgr.On("List", mock.Anything, q1).Return([]*artifact.Artifact{
{ID: 1},

View File

@ -19,6 +19,7 @@ import (
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/controller/tag"
"github.com/goharbor/harbor/src/lib/encode/repository"
accessoryModel "github.com/goharbor/harbor/src/pkg/accessory/model"
"github.com/goharbor/harbor/src/pkg/artifact"
"github.com/goharbor/harbor/src/pkg/label/model"
)
@ -26,9 +27,10 @@ import (
// Artifact is the overall view of artifact
type Artifact struct {
artifact.Artifact
Tags []*tag.Tag `json:"tags"` // the list of tags that attached to the artifact
AdditionLinks map[string]*AdditionLink `json:"addition_links"` // the resource link for build history(image), values.yaml(chart), dependency(chart), etc
Labels []*model.Label `json:"labels"`
Tags []*tag.Tag `json:"tags"` // the list of tags that attached to the artifact
AdditionLinks map[string]*AdditionLink `json:"addition_links"` // the resource link for build history(image), values.yaml(chart), dependency(chart), etc
Labels []*model.Label `json:"labels"`
Accessories []accessoryModel.Accessory `json:"accessories"`
}
// SetAdditionLink set a addition link
@ -53,7 +55,8 @@ 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
WithTag bool
TagOption *tag.Option // only works when WithTag is set to true
WithLabel bool
WithAccessory bool
}

View File

@ -33,8 +33,8 @@ type DAO interface {
Create(ctx context.Context, accessory *Accessory) (id int64, err error)
// Delete the accessory specified by ID
Delete(ctx context.Context, id int64) (err error)
// DeleteOfArtifact deletes all accessory attached to the artifact
DeleteOfArtifact(ctx context.Context, subArtifactID int64) (err error)
// DeleteAccessories deletes accessories by query
DeleteAccessories(ctx context.Context, query *q.Query) (int64, error)
}
// New returns an instance of the default DAO
@ -116,15 +116,10 @@ func (d *dao) Delete(ctx context.Context, id int64) error {
return nil
}
func (d *dao) DeleteOfArtifact(ctx context.Context, subArtifactID int64) error {
qs, err := orm.QuerySetter(ctx, &Accessory{}, &q.Query{
Keywords: map[string]interface{}{
"SubjectArtifactID": subArtifactID,
},
})
func (d *dao) DeleteAccessories(ctx context.Context, query *q.Query) (int64, error) {
qs, err := orm.QuerySetter(ctx, &Accessory{}, query)
if err != nil {
return err
return 0, err
}
_, err = qs.Delete()
return err
return qs.Delete()
}

View File

@ -246,7 +246,11 @@ func (d *daoTestSuite) TestDeleteOfArtifact() {
d.Require().Nil(err)
d.Require().Len(accs, 2)
err = d.dao.DeleteOfArtifact(d.ctx, subArtID)
_, err = d.dao.DeleteAccessories(d.ctx, &q.Query{
Keywords: map[string]interface{}{
"SubjectArtifactID": subArtID,
},
})
d.Require().Nil(err)
accs, err = d.dao.List(d.ctx, &q.Query{

View File

@ -42,8 +42,8 @@ type Manager interface {
Create(ctx context.Context, accessory model.AccessoryData) (id int64, err error)
// Delete the tag specified by ID
Delete(ctx context.Context, id int64) (err error)
// DeleteOfArtifact deletes all tags attached to the artifact
DeleteOfArtifact(ctx context.Context, artifactID int64) (err error)
// DeleteAccessories deletes accessories according to the query
DeleteAccessories(ctx context.Context, q *q.Query) (err error)
}
// NewManager returns an instance of the default manager
@ -116,6 +116,7 @@ func (m *manager) Delete(ctx context.Context, id int64) error {
return m.dao.Delete(ctx, id)
}
func (m *manager) DeleteOfArtifact(ctx context.Context, artifactID int64) error {
return m.dao.DeleteOfArtifact(ctx, artifactID)
func (m *manager) DeleteAccessories(ctx context.Context, q *q.Query) error {
_, err := m.dao.DeleteAccessories(ctx, q)
return err
}

View File

@ -15,6 +15,7 @@
package accessory
import (
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/accessory/dao"
"github.com/goharbor/harbor/src/pkg/accessory/model"
"github.com/goharbor/harbor/src/testing/mock"
@ -90,8 +91,8 @@ func (m *managerTestSuite) TestCount() {
}
func (m *managerTestSuite) TestDeleteOfArtifact() {
mock.OnAnything(m.dao, "DeleteOfArtifact").Return(nil)
err := m.mgr.DeleteOfArtifact(nil, 1)
mock.OnAnything(m.dao, "DeleteAccessories").Return(int64(1), nil)
err := m.mgr.DeleteAccessories(nil, q.New(q.KeyWords{"ArtifactID": 1}))
m.Require().Nil(err)
m.dao.AssertExpectations(m.T())
}

View File

@ -76,6 +76,8 @@ type AccessoryData struct {
type Accessory interface {
RefProvider
RefIdentifier
// Define whether shows in the artifact list response.
Display() bool
GetData() AccessoryData
}

View File

@ -40,6 +40,11 @@ func (a *Default) IsHard() bool {
return false
}
// Display ...
func (a *Default) Display() bool {
return false
}
// GetData ...
func (a *Default) GetData() model.AccessoryData {
return a.Data

View File

@ -11,10 +11,12 @@ type BaseTestSuite struct {
htesting.Suite
accessory model.Accessory
digest string
subDigest string
}
func (suite *BaseTestSuite) SetupSuite() {
suite.digest = suite.DigestString()
suite.subDigest = suite.DigestString()
suite.accessory, _ = model.New(model.TypeNone,
model.AccessoryData{
ArtifactID: 1,
@ -60,6 +62,10 @@ func (suite *BaseTestSuite) TestIsHard() {
suite.False(suite.accessory.IsHard())
}
func (suite *BaseTestSuite) TestDisplay() {
suite.False(suite.accessory.Display())
}
func TestCacheTestSuite(t *testing.T) {
suite.Run(t, new(BaseTestSuite))
}

View File

@ -60,6 +60,10 @@ func (suite *CosignTestSuite) TestIsHard() {
suite.True(suite.accessory.IsHard())
}
func (suite *CosignTestSuite) TestDisplay() {
suite.False(suite.accessory.Display())
}
func TestCacheTestSuite(t *testing.T) {
suite.Run(t, new(CosignTestSuite))
}

View File

@ -35,6 +35,7 @@ import (
"github.com/goharbor/harbor/src/controller/tag"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/pkg/accessory"
"github.com/goharbor/harbor/src/pkg/notification"
"github.com/goharbor/harbor/src/pkg/scan/report"
"github.com/goharbor/harbor/src/server/v2.0/handler/assembler"
@ -46,6 +47,7 @@ import (
func newArtifactAPI() *artifactAPI {
return &artifactAPI{
accMgr: accessory.Mgr,
artCtl: artifact.Ctl,
proCtl: project.Ctl,
repoCtl: repository.Ctl,
@ -56,6 +58,7 @@ func newArtifactAPI() *artifactAPI {
type artifactAPI struct {
BaseAPI
accMgr accessory.Manager
artCtl artifact.Controller
proCtl project.Controller
repoCtl repository.Controller
@ -85,7 +88,7 @@ func (a *artifactAPI) ListArtifacts(ctx context.Context, params operation.ListAr
// set option
option := option(params.WithTag, params.WithImmutableStatus,
params.WithLabel, params.WithSignature)
params.WithLabel, params.WithSignature, params.WithAccessory)
// get the total count of artifacts
total, err := a.artCtl.Count(ctx, query)
@ -119,7 +122,7 @@ func (a *artifactAPI) GetArtifact(ctx context.Context, params operation.GetArtif
}
// set option
option := option(params.WithTag, params.WithImmutableStatus,
params.WithLabel, params.WithSignature)
params.WithLabel, params.WithSignature, params.WithAccessory)
// get the artifact
artifact, err := a.artCtl.GetByReference(ctx, fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName), params.Reference, option)
@ -333,6 +336,39 @@ func (a *artifactAPI) ListTags(ctx context.Context, params operation.ListTagsPar
WithPayload(ts)
}
func (a *artifactAPI) ListAccessories(ctx context.Context, params operation.ListAccessoriesParams) middleware.Responder {
if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionList, rbac.ResourceAccessory); err != nil {
return a.SendError(ctx, err)
}
// set query
query, err := a.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
if err != nil {
return a.SendError(ctx, err)
}
artifact, err := a.artCtl.GetByReference(ctx, fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName), params.Reference, nil)
if err != nil {
return a.SendError(ctx, err)
}
query.Keywords["SubjectArtifactID"] = artifact.ID
// list accessories according to the query
accs, err := a.accMgr.List(ctx, query)
if err != nil {
return a.SendError(ctx, err)
}
total := len(accs)
var res []*models.Accessory
for _, acc := range accs {
res = append(res, model.NewAccessory(acc.GetData()).ToSwagger())
}
return operation.NewListAccessoriesOK().
WithXTotalCount(int64(total)).
WithLink(a.Links(ctx, params.HTTPRequest.URL, int64(total), query.PageNumber, query.PageSize).String()).
WithPayload(res)
}
func (a *artifactAPI) GetVulnerabilitiesAddition(ctx context.Context, params operation.GetVulnerabilitiesAdditionParams) middleware.Responder {
if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionRead, rbac.ResourceArtifactAddition); err != nil {
return a.SendError(ctx, err)
@ -424,16 +460,21 @@ func (a *artifactAPI) RemoveLabel(ctx context.Context, params operation.RemoveLa
return operation.NewRemoveLabelOK()
}
func option(withTag, withImmutableStatus, withLabel, withSignature *bool) *artifact.Option {
func option(withTag, withImmutableStatus, withLabel, withSignature, withAccessory *bool) *artifact.Option {
option := &artifact.Option{
WithTag: true, // return the tag by default
WithLabel: lib.BoolValue(withLabel),
WithTag: true, // return the tag by default
WithLabel: lib.BoolValue(withLabel),
WithAccessory: true, // return the accessory by default
}
if withTag != nil {
option.WithTag = *(withTag)
}
if withAccessory != nil {
option.WithAccessory = *(withAccessory)
}
if option.WithTag {
option.TagOption = &tag.Option{
WithImmutableStatus: lib.BoolValue(withImmutableStatus),

View File

@ -0,0 +1,31 @@
package model
import (
"github.com/go-openapi/strfmt"
"github.com/goharbor/harbor/src/pkg/accessory/model"
"github.com/goharbor/harbor/src/server/v2.0/models"
)
// Accessory model
type Accessory struct {
model.AccessoryData
}
// ToSwagger converts the label to the swagger model
func (a *Accessory) ToSwagger() *models.Accessory {
return &models.Accessory{
ID: a.ID,
ArtifactID: a.ArtifactID,
SubjectArtifactID: a.SubArtifactID,
Size: a.Size,
Digest: a.Digest,
Type: a.Type,
Icon: "",
CreationTime: strfmt.DateTime(a.CreatTime),
}
}
// NewAccessory ...
func NewAccessory(a model.AccessoryData) *Accessory {
return &Accessory{AccessoryData: a}
}

View File

@ -51,6 +51,9 @@ func (a *Artifact) ToSwagger() *models.Artifact {
for _, reference := range a.References {
art.References = append(art.References, NewReference(reference).ToSwagger())
}
for _, acc := range a.Accessories {
art.Accessories = append(art.Accessories, NewAccessory(acc.GetData()).ToSwagger())
}
for _, tag := range a.Tags {
art.Tags = append(art.Tags, NewTag(tag).ToSwagger())
}

View File

@ -72,18 +72,25 @@ func (_m *DAO) Delete(ctx context.Context, id int64) error {
return r0
}
// DeleteOfArtifact provides a mock function with given fields: ctx, subArtifactID
func (_m *DAO) DeleteOfArtifact(ctx context.Context, subArtifactID int64) error {
ret := _m.Called(ctx, subArtifactID)
// DeleteAccessories provides a mock function with given fields: ctx, query
func (_m *DAO) DeleteAccessories(ctx context.Context, query *q.Query) (int64, error) {
ret := _m.Called(ctx, query)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
r0 = rf(ctx, subArtifactID)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) int64); ok {
r0 = rf(ctx, query)
} else {
r0 = ret.Error(0)
r0 = ret.Get(0).(int64)
}
return r0
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
}
// Get provides a mock function with given fields: ctx, id

View File

@ -0,0 +1,133 @@
// Code generated by mockery v2.1.0. DO NOT EDIT.
package accessory
import (
context "context"
model "github.com/goharbor/harbor/src/pkg/accessory/model"
mock "github.com/stretchr/testify/mock"
q "github.com/goharbor/harbor/src/lib/q"
)
// Manager is an autogenerated mock type for the Manager type
type Manager struct {
mock.Mock
}
// Count provides a mock function with given fields: ctx, query
func (_m *Manager) Count(ctx context.Context, query *q.Query) (int64, error) {
ret := _m.Called(ctx, query)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) int64); ok {
r0 = rf(ctx, query)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
r1 = rf(ctx, query)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Create provides a mock function with given fields: ctx, _a1
func (_m *Manager) Create(ctx context.Context, _a1 model.AccessoryData) (int64, error) {
ret := _m.Called(ctx, _a1)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context, model.AccessoryData) int64); ok {
r0 = rf(ctx, _a1)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, model.AccessoryData) error); ok {
r1 = rf(ctx, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: ctx, id
func (_m *Manager) Delete(ctx context.Context, id int64) error {
ret := _m.Called(ctx, id)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
r0 = rf(ctx, id)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteAccessories provides a mock function with given fields: ctx, _a1
func (_m *Manager) DeleteAccessories(ctx context.Context, _a1 *q.Query) error {
ret := _m.Called(ctx, _a1)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) error); ok {
r0 = rf(ctx, _a1)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: ctx, id
func (_m *Manager) Get(ctx context.Context, id int64) (model.Accessory, error) {
ret := _m.Called(ctx, id)
var r0 model.Accessory
if rf, ok := ret.Get(0).(func(context.Context, int64) model.Accessory); ok {
r0 = rf(ctx, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.Accessory)
}
}
var r1 error
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) ([]model.Accessory, error) {
ret := _m.Called(ctx, query)
var r0 []model.Accessory
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []model.Accessory); ok {
r0 = rf(ctx, query)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]model.Accessory)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
r1 = rf(ctx, query)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

View File

@ -12,6 +12,20 @@ type Accessory struct {
mock.Mock
}
// Display provides a mock function with given fields:
func (_m *Accessory) Display() bool {
ret := _m.Called()
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// GetData provides a mock function with given fields:
func (_m *Accessory) GetData() model.AccessoryData {
ret := _m.Called()

View File

@ -55,3 +55,4 @@ package pkg
//go:generate mockery --case snake --dir ../../pkg/joblog/dao --name DAO --output ./joblog/dao --outpkg dao
//go:generate mockery --case snake --dir ../../pkg/accessory/model --name Accessory --output ./accessory/model --outpkg model
//go:generate mockery --case snake --dir ../../pkg/accessory/dao --name DAO --output ./accessory/dao --outpkg dao
//go:generate mockery --case snake --dir ../../pkg/accessory --name Manager --output ./accessory --outpkg accessory