Support add/remove label to/from artifact

This commit add supporting for adding/removing label to/from artifacts and populates labels when listing artifacts

Signed-off-by: Wenkai Yin <yinw@vmware.com>
This commit is contained in:
Wenkai Yin 2020-01-27 20:32:14 +08:00
parent 5e34ba0c97
commit 93731eeb2e
17 changed files with 948 additions and 56 deletions

View File

@ -270,15 +270,13 @@ paths:
- $ref: '#/parameters/reference'
- name: addition
in: path
description: The addition name, "build_history" for images; "values.yaml", "readme", "dependencies" for charts
description: The type of addition.
type: string
enum: [build_history, values.yaml, readme, dependencies]
required: true
responses:
'200':
description: Success
schema:
type: string
$ref: '#/responses/200'
'400':
$ref: '#/responses/400'
'401':
@ -289,6 +287,70 @@ paths:
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
/projects/{project_name}/repositories/{repository_name}/artifacts/{reference}/labels:
post:
summary: Add label to artifact
description: Add label to the specified artiact.
tags:
- artifact
operationId: addLabel
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectName'
- $ref: '#/parameters/repositoryName'
- $ref: '#/parameters/reference'
- name: label
in: body
description: The label that added to the artifact. Only the ID property is needed.
required: true
schema:
$ref: '#/definitions/Label'
responses:
'200':
$ref: '#/responses/200'
'400':
$ref: '#/responses/400'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'409':
$ref: '#/responses/409'
'500':
$ref: '#/responses/500'
/projects/{project_name}/repositories/{repository_name}/artifacts/{reference}/labels/{label_id}:
delete:
summary: Remove label from artifact
description: Remove the label from the specified artiact.
tags:
- artifact
operationId: removeLabel
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectName'
- $ref: '#/parameters/repositoryName'
- $ref: '#/parameters/reference'
- name: label_id
in: path
description: The ID of the label that removed from the artifact.
type: integer
format: int64
required: true
responses:
'200':
$ref: '#/responses/200'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'409':
$ref: '#/responses/409'
'500':
$ref: '#/responses/500'
parameters:
requestId:
name: X-Request-Id
@ -467,6 +529,10 @@ definitions:
$ref: '#/definitions/Tag'
addition_links:
$ref: '#/definitions/AdditionLinks'
labels:
type: array
items:
$ref: '#/definitions/Label'
Tag:
type: object
properties:
@ -555,4 +621,38 @@ definitions:
variant:
type: string
description: The variant of the CPU
Label:
type: object
properties:
id:
type: integer
format: int64
description: The ID of the label
name:
type: string
description: The name the label
description:
type: string
description: The description the label
color:
type: string
description: The color the label
scope:
type: string
description: The scope the label
project_id:
type: integer
format: int64
description: The ID of project that the label belongs to
creation_time:
type: string
format: date-time
description: The creation time the label
update_time:
type: string
format: date-time
description: The update time of the label
deleted:
type: boolean
description: Whether the label is deleted or not

View File

@ -42,3 +42,18 @@ CREATE TABLE artifact_reference
FOREIGN KEY (child_id) REFERENCES artifact_2(id),
CONSTRAINT unique_reference UNIQUE (parent_id, child_id)
);
/* TODO upgrade: how about keep the table "harbor_resource_label" only for helm v2 chart and use the new table for artifact label reference? */
/* label_reference records the labels added to the artifact */
CREATE TABLE label_reference (
id SERIAL PRIMARY KEY NOT NULL,
label_id int NOT NULL,
artifact_id int NOT NULL,
creation_time timestamp default CURRENT_TIMESTAMP,
update_time timestamp default CURRENT_TIMESTAMP,
FOREIGN KEY (label_id) REFERENCES harbor_label(id),
/* TODO replace artifact_2 after finishing the upgrade work */
FOREIGN KEY (artifact_id) REFERENCES artifact_2(id),
CONSTRAINT unique_label_reference UNIQUE (label_id,artifact_id)
);

View File

@ -26,6 +26,7 @@ import (
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/immutabletag/match"
"github.com/goharbor/harbor/src/pkg/immutabletag/match/rule"
"github.com/goharbor/harbor/src/pkg/label"
"github.com/opencontainers/go-digest"
"strings"
@ -77,6 +78,10 @@ type Controller interface {
// The addition is different according to the artifact type:
// build history for image; values.yaml, readme and dependencies for chart, etc
GetAddition(ctx context.Context, artifactID int64, additionType string) (addition *resolver.Addition, err error)
// AddLabel to the specified artifact
AddLabel(ctx context.Context, artifactID int64, labelID int64) (err error)
// RemoveLabel from the specified artifact
RemoveLabel(ctx context.Context, artifactID int64, labelID int64) (err error)
// TODO move this to GC controller?
// Prune removes the useless artifact records. The underlying registry data will
// be removed during garbage collection
@ -89,6 +94,7 @@ func NewController() Controller {
repoMgr: repository.Mgr,
artMgr: artifact.Mgr,
tagMgr: tag.Mgr,
labelMgr: label.Mgr,
abstractor: abstractor.NewAbstractor(),
immutableMtr: rule.NewRuleMatcher(),
}
@ -100,6 +106,7 @@ type controller struct {
repoMgr repository.Manager
artMgr artifact.Manager
tagMgr tag.Manager
labelMgr label.Manager
abstractor abstractor.Abstractor
immutableMtr match.ImmutableTagMatcher
}
@ -284,6 +291,10 @@ func (c *controller) getByTag(ctx context.Context, repository, tag string, optio
}
func (c *controller) Delete(ctx context.Context, id int64) error {
// remove labels added to the artifact
if err := c.labelMgr.RemoveAllFrom(ctx, id); err != nil {
return err
}
// delete all tags that attached to the artifact
_, tags, err := c.tagMgr.List(ctx, &q.Query{
Keywords: map[string]interface{}{
@ -326,7 +337,6 @@ func (c *controller) ListTags(ctx context.Context, query *q.Query, option *TagOp
func (c *controller) DeleteTag(ctx context.Context, tagID int64) error {
// Immutable checking is covered in middleware
// TODO check signature
// TODO delete label
// TODO fire delete tag event
return c.tagMgr.Delete(ctx, tagID)
}
@ -362,6 +372,14 @@ func (c *controller) GetAddition(ctx context.Context, artifactID int64, addition
}
}
func (c *controller) AddLabel(ctx context.Context, artifactID int64, labelID int64) error {
return c.labelMgr.AddTo(ctx, labelID, artifactID)
}
func (c *controller) RemoveLabel(ctx context.Context, artifactID int64, labelID int64) error {
return c.labelMgr.RemoveFrom(ctx, labelID, artifactID)
}
// assemble several part into a single artifact
func (c *controller) assembleArtifact(ctx context.Context, art *artifact.Artifact, option *Option) *Artifact {
artifact := &Artifact{
@ -370,34 +388,38 @@ func (c *controller) assembleArtifact(ctx context.Context, art *artifact.Artifac
if option == nil {
return artifact
}
// populate tags
if option.WithTag {
_, tgs, err := c.tagMgr.List(ctx, &q.Query{
Keywords: map[string]interface{}{
"artifact_id": artifact.ID,
},
})
if err == nil {
// assemble tags
for _, tg := range tgs {
artifact.Tags = append(artifact.Tags, c.assembleTag(ctx, tg, option.TagOption))
}
} else {
log.Errorf("failed to list tag of artifact %d: %v", artifact.ID, err)
}
c.populateTags(ctx, artifact, option.TagOption)
}
if option.WithLabel {
// TODO populate label
c.populateLabels(ctx, artifact)
}
if option.WithScanOverview {
// TODO populate scan overview
c.populateScanOverview(ctx, artifact)
}
if option.WithSignature {
c.populateSignature(ctx, artifact)
}
// populate addition links
c.populateAdditionLinks(ctx, artifact)
// TODO populate signature on artifact or label level?
return artifact
}
func (c *controller) populateTags(ctx context.Context, art *Artifact, option *TagOption) {
_, tags, err := c.tagMgr.List(ctx, &q.Query{
Keywords: map[string]interface{}{
"artifact_id": art.ID,
},
})
if err != nil {
log.Errorf("failed to list tag of artifact %d: %v", art.ID, err)
return
}
for _, tag := range tags {
art.Tags = append(art.Tags, c.assembleTag(ctx, tag, option))
}
}
// assemble several part into a single tag
func (c *controller) assembleTag(ctx context.Context, tag *tm.Tag, option *TagOption) *Tag {
t := &Tag{
@ -407,30 +429,46 @@ func (c *controller) assembleTag(ctx context.Context, tag *tm.Tag, option *TagOp
return t
}
if option.WithImmutableStatus {
repo, err := c.repoMgr.Get(ctx, tag.RepositoryID)
if err != nil {
log.Error(err)
} else {
t.Immutable = c.isImmutable(repo.ProjectID, repo.Name, tag.Name)
c.populateImmutableStatus(ctx, t)
}
}
// TODO populate signature on tag level?
return t
}
// check whether the tag is Immutable
func (c *controller) isImmutable(projectID int64, repo string, tag string) bool {
_, repoName := utils.ParseRepository(repo)
matched, err := c.immutableMtr.Match(projectID, art.Candidate{
func (c *controller) populateLabels(ctx context.Context, art *Artifact) {
labels, err := c.labelMgr.ListByArtifact(ctx, art.ID)
if err != nil {
log.Errorf("failed to list labels of artifact %d: %v", art.ID, err)
return
}
art.Labels = labels
}
func (c *controller) populateImmutableStatus(ctx context.Context, tag *Tag) {
repo, err := c.repoMgr.Get(ctx, tag.RepositoryID)
if err != nil {
log.Error(err)
return
}
_, repoName := utils.ParseRepository(repo.Name)
matched, err := c.immutableMtr.Match(repo.ProjectID, art.Candidate{
Repository: repoName,
Tag: tag,
NamespaceID: projectID,
Tag: tag.Name,
NamespaceID: repo.ProjectID,
})
if err != nil {
log.Error(err)
return false
return
}
return matched
tag.Immutable = matched
}
func (c *controller) populateScanOverview(ctx context.Context, art *Artifact) {
// TODO implement
}
func (c *controller) populateSignature(ctx context.Context, art *Artifact) {
// TODO implement
// TODO populate signature on artifact or tag level?
}
func (c *controller) populateAdditionLinks(ctx context.Context, artifact *Artifact) {

View File

@ -26,6 +26,7 @@ import (
"github.com/goharbor/harbor/src/pkg/tag/model/tag"
arttesting "github.com/goharbor/harbor/src/testing/pkg/artifact"
immutesting "github.com/goharbor/harbor/src/testing/pkg/immutabletag"
"github.com/goharbor/harbor/src/testing/pkg/label"
repotesting "github.com/goharbor/harbor/src/testing/pkg/repository"
tagtesting "github.com/goharbor/harbor/src/testing/pkg/tag"
"github.com/stretchr/testify/mock"
@ -69,6 +70,7 @@ type controllerTestSuite struct {
repoMgr *repotesting.FakeManager
artMgr *arttesting.FakeManager
tagMgr *tagtesting.FakeManager
labelMgr *label.FakeManager
abstractor *fakeAbstractor
immutableMtr *immutesting.FakeMatcher
}
@ -77,12 +79,14 @@ func (c *controllerTestSuite) SetupTest() {
c.repoMgr = &repotesting.FakeManager{}
c.artMgr = &arttesting.FakeManager{}
c.tagMgr = &tagtesting.FakeManager{}
c.labelMgr = &label.FakeManager{}
c.abstractor = &fakeAbstractor{}
c.immutableMtr = &immutesting.FakeMatcher{}
c.ctl = &controller{
repoMgr: c.repoMgr,
artMgr: c.artMgr,
tagMgr: c.tagMgr,
labelMgr: c.labelMgr,
abstractor: c.abstractor,
immutableMtr: c.immutableMtr,
}
@ -125,7 +129,7 @@ func (c *controllerTestSuite) TestAssembleArtifact() {
TagOption: &TagOption{
WithImmutableStatus: false,
},
WithLabel: false,
WithLabel: true,
WithScanOverview: true,
WithSignature: true,
}
@ -142,6 +146,13 @@ func (c *controllerTestSuite) TestAssembleArtifact() {
Name: "library/hello-world",
}, nil)
ctx := internal.SetAPIVersion(nil, "2.0")
lb := &models.Label{
ID: 1,
Name: "label",
}
c.labelMgr.On("ListByArtifact").Return([]*models.Label{
lb,
}, nil)
artifact := c.ctl.assembleArtifact(ctx, art, option)
c.Require().NotNil(artifact)
c.Equal(art.ID, artifact.ID)
@ -151,6 +162,7 @@ func (c *controllerTestSuite) TestAssembleArtifact() {
c.False(artifact.AdditionLinks["build_history"].Absolute)
c.Equal("/api/2.0/projects/library/repositories/hello-world/artifacts/sha256:123/additions/build_history",
artifact.AdditionLinks["build_history"].HREF)
c.Contains(artifact.Labels, lb)
// TODO check other fields of option
}
@ -413,6 +425,7 @@ func (c *controllerTestSuite) TestDelete() {
},
}, nil)
c.tagMgr.On("Delete").Return(nil)
c.labelMgr.On("RemoveAllFrom").Return(nil)
err := c.ctl.Delete(nil, 1)
c.Require().Nil(err)
c.artMgr.AssertExpectations(c.T())
@ -485,6 +498,18 @@ func (c *controllerTestSuite) TestGetAddition() {
c.Require().Nil(err)
}
func (c *controllerTestSuite) TestAddTo() {
c.labelMgr.On("AddTo").Return(nil)
err := c.ctl.AddLabel(nil, 1, 1)
c.Require().Nil(err)
}
func (c *controllerTestSuite) TestRemoveFrom() {
c.labelMgr.On("RemoveFrom").Return(nil)
err := c.ctl.RemoveLabel(nil, 1, 1)
c.Require().Nil(err)
}
func TestControllerTestSuite(t *testing.T) {
suite.Run(t, &controllerTestSuite{})
}

View File

@ -16,6 +16,7 @@ package artifact
import (
"github.com/go-openapi/strfmt"
cmodels "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/pkg/artifact"
"github.com/goharbor/harbor/src/pkg/tag/model/tag"
"github.com/goharbor/harbor/src/server/v2.0/models"
@ -25,7 +26,8 @@ import (
type Artifact struct {
artifact.Artifact
Tags []*Tag // the list of tags that attached to the artifact
AdditionLinks map[string]*AdditionLink // the link for build history(image), values.yaml(chart), dependency(chart), etc
AdditionLinks map[string]*AdditionLink // the resource link for build history(image), values.yaml(chart), dependency(chart), etc
Labels []*cmodels.Label
// TODO add other attrs: signature, scan result, etc
}
@ -82,6 +84,19 @@ func (a *Artifact) ToSwagger() *models.Artifact {
Href: link.HREF,
}
}
for _, label := range a.Labels {
art.Labels = append(art.Labels, &models.Label{
ID: label.ID,
Name: label.Name,
Description: label.Description,
Color: label.Color,
CreationTime: strfmt.DateTime(label.CreationTime),
ProjectID: label.ProjectID,
Scope: label.Scope,
UpdateTime: strfmt.DateTime(label.UpdateTime),
Deleted: label.Deleted,
})
}
return art
}
@ -89,7 +104,7 @@ func (a *Artifact) ToSwagger() *models.Artifact {
type Tag struct {
tag.Tag
Immutable bool
// TODO add other attrs: signature, label, etc
// TODO add other attrs: signature, etc
}
// AdditionLink is a link via that the addition can be fetched

View File

@ -52,9 +52,9 @@ const (
ResourceRepository = Resource("repository")
ResourceTagRetention = Resource("tag-retention")
ResourceImmutableTag = Resource("immutable-tag")
ResourceRepositoryLabel = Resource("repository-label")
ResourceRepositoryLabel = Resource("repository-label") // TODO remove
ResourceRepositoryTag = Resource("repository-tag") // TODO remove
ResourceRepositoryTagLabel = Resource("repository-tag-label")
ResourceRepositoryTagLabel = Resource("repository-tag-label") // TODO remove
ResourceRepositoryTagManifest = Resource("repository-tag-manifest")
ResourceRepositoryTagScanJob = Resource("repository-tag-scan-job") // TODO: remove
ResourceRepositoryTagVulnerability = Resource("repository-tag-vulnerability") // TODO: remove
@ -65,5 +65,6 @@ const (
ResourceArtifact = Resource("artifact")
ResourceTag = Resource("tag")
ResourceArtifactAddition = Resource("artifact-addition")
ResourceArtifactLabel = Resource("artifact-label")
ResourceSelf = Resource("") // subresource for self
)

View File

@ -135,6 +135,9 @@ var (
{Resource: rbac.ResourceTag, Action: rbac.ActionCreate},
{Resource: rbac.ResourceTag, Action: rbac.ActionDelete},
{Resource: rbac.ResourceArtifactLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceArtifactLabel, Action: rbac.ActionDelete},
},
"master": {
@ -232,6 +235,9 @@ var (
{Resource: rbac.ResourceTag, Action: rbac.ActionCreate},
{Resource: rbac.ResourceTag, Action: rbac.ActionDelete},
{Resource: rbac.ResourceArtifactLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceArtifactLabel, Action: rbac.ActionDelete},
},
"developer": {
@ -294,6 +300,9 @@ var (
{Resource: rbac.ResourceArtifactAddition, Action: rbac.ActionRead},
{Resource: rbac.ResourceTag, Action: rbac.ActionCreate},
{Resource: rbac.ResourceArtifactLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceArtifactLabel, Action: rbac.ActionDelete},
},
"guest": {

View File

@ -166,6 +166,7 @@ func (d *dao) CreateReference(ctx context.Context, reference *ArtifactReference)
return 0, err
}
id, err := ormer.Insert(reference)
if err != nil {
if e := orm.AsConflictError(err, "reference already exists, parent artifact ID: %d, child artifact ID: %d",
reference.ParentID, reference.ChildID); e != nil {
err = e
@ -173,6 +174,7 @@ func (d *dao) CreateReference(ctx context.Context, reference *ArtifactReference)
reference.ParentID, reference.ChildID); e != nil {
err = e
}
}
return id, err
}
func (d *dao) ListReferences(ctx context.Context, query *q.Query) ([]*ArtifactReference, error) {

158
src/pkg/label/dao.go Normal file
View File

@ -0,0 +1,158 @@
// 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 label
import (
"context"
beego_orm "github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/common/models"
ierror "github.com/goharbor/harbor/src/internal/error"
"github.com/goharbor/harbor/src/internal/orm"
"github.com/goharbor/harbor/src/pkg/q"
)
func init() {
beego_orm.RegisterModel(&Reference{})
}
// DAO is the data access object interface for label
type DAO interface {
// Get the specified label
Get(ctx context.Context, id int64) (label *models.Label, err error)
// Create the label
Create(ctx context.Context, label *models.Label) (id int64, err error)
// Delete the label
Delete(ctx context.Context, id int64) (err error)
// List labels that added to the artifact specified by the ID
ListByArtifact(ctx context.Context, artifactID int64) (labels []*models.Label, err error)
// Create label reference
CreateReference(ctx context.Context, reference *Reference) (id int64, err error)
// Delete the label reference specified by ID
DeleteReference(ctx context.Context, id int64) (err error)
// Delete label references specified by query
DeleteReferences(ctx context.Context, query *q.Query) (n int64, err error)
}
// NewDAO creates an instance of the default DAO
func NewDAO() DAO {
return &defaultDAO{}
}
type defaultDAO struct{}
func (d *defaultDAO) Get(ctx context.Context, id int64) (*models.Label, error) {
ormer, err := orm.FromContext(ctx)
if err != nil {
return nil, err
}
label := &models.Label{
ID: id,
}
if err = ormer.Read(label); err != nil {
if e := orm.AsNotFoundError(err, "label %d not found", id); e != nil {
err = e
}
return nil, err
}
return label, nil
}
func (d *defaultDAO) Create(ctx context.Context, label *models.Label) (int64, error) {
ormer, err := orm.FromContext(ctx)
if err != nil {
return 0, err
}
id, err := ormer.Insert(label)
if err != nil {
if e := orm.AsConflictError(err, "label %s already exists", label.Name); e != nil {
err = e
}
}
return id, err
}
func (d *defaultDAO) Delete(ctx context.Context, id int64) error {
ormer, err := orm.FromContext(ctx)
if err != nil {
return err
}
n, err := ormer.Delete(&models.Label{
ID: id,
})
if err != nil {
return err
}
if n == 0 {
return ierror.NotFoundError(nil).WithMessage("label %d not found", id)
}
return nil
}
func (d *defaultDAO) ListByArtifact(ctx context.Context, artifactID int64) ([]*models.Label, error) {
sql := `select label.* from harbor_label label
join label_reference ref on label.id = ref.label_id
where ref.artifact_id = ?`
ormer, err := orm.FromContext(ctx)
if err != nil {
return nil, err
}
labels := []*models.Label{}
if _, err = ormer.Raw(sql, artifactID).QueryRows(&labels); err != nil {
return nil, err
}
return labels, nil
}
func (d *defaultDAO) CreateReference(ctx context.Context, ref *Reference) (int64, error) {
ormer, err := orm.FromContext(ctx)
if err != nil {
return 0, err
}
id, err := ormer.Insert(ref)
if err != nil {
if e := orm.AsConflictError(err, "label %d is already added to the artifact %d",
ref.LabelID, ref.ArtifactID); e != nil {
err = e
} else if e := orm.AsForeignKeyError(err, "the reference tries to refer a non existing label %d or artifact %d",
ref.LabelID, ref.ArtifactID); e != nil {
err = ierror.New(e).WithCode(ierror.NotFoundCode).WithMessage(e.Message)
}
}
return id, err
}
func (d *defaultDAO) DeleteReference(ctx context.Context, id int64) error {
ormer, err := orm.FromContext(ctx)
if err != nil {
return err
}
n, err := ormer.Delete(&Reference{
ID: id,
})
if err != nil {
return err
}
if n == 0 {
return ierror.NotFoundError(nil).WithMessage("label reference %d not found", id)
}
return nil
}
func (d *defaultDAO) DeleteReferences(ctx context.Context, query *q.Query) (int64, error) {
qs, err := orm.QuerySetter(ctx, &Reference{}, query)
if err != nil {
return 0, err
}
return qs.Delete()
}

175
src/pkg/label/dao_test.go Normal file
View File

@ -0,0 +1,175 @@
// 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 label
import (
"context"
beegoorm "github.com/astaxie/beego/orm"
common_dao "github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
ierror "github.com/goharbor/harbor/src/internal/error"
"github.com/goharbor/harbor/src/internal/orm"
artdao "github.com/goharbor/harbor/src/pkg/artifact/dao"
"github.com/goharbor/harbor/src/pkg/q"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/stretchr/testify/suite"
"testing"
)
type labelDaoTestSuite struct {
suite.Suite
dao DAO
artDAO artdao.DAO
ctx context.Context
artID int64
id int64
refID int64
}
func (l *labelDaoTestSuite) SetupSuite() {
common_dao.PrepareTestForPostgresSQL()
l.dao = &defaultDAO{}
l.artDAO = artdao.New()
l.ctx = orm.NewContext(nil, beegoorm.NewOrm())
}
func (l *labelDaoTestSuite) SetupTest() {
id, err := l.dao.Create(l.ctx, &models.Label{
Name: "label_for_label_dao_test_suite",
Scope: "g",
})
l.Require().Nil(err)
l.id = id
id, err = l.artDAO.Create(l.ctx, &artdao.Artifact{
Type: "IMAGE",
MediaType: v1.MediaTypeImageConfig,
ManifestMediaType: v1.MediaTypeImageManifest,
ProjectID: 1,
RepositoryID: 1,
Digest: "sha256",
})
l.Require().Nil(err)
l.artID = id
id, err = l.dao.CreateReference(l.ctx, &Reference{
LabelID: l.id,
ArtifactID: l.artID,
})
l.Require().Nil(err)
l.refID = id
}
func (l *labelDaoTestSuite) TearDownTest() {
err := l.dao.DeleteReference(l.ctx, l.refID)
l.Require().Nil(err)
err = l.dao.Delete(l.ctx, l.id)
l.Require().Nil(err)
err = l.artDAO.Delete(l.ctx, l.artID)
l.Require().Nil(err)
}
func (l *labelDaoTestSuite) TestGet() {
// not found
_, err := l.dao.Get(l.ctx, 1000)
l.Require().NotNil(err)
l.True(ierror.IsErr(err, ierror.NotFoundCode))
// success
label, err := l.dao.Get(l.ctx, l.id)
l.Require().Nil(err)
l.Equal(l.id, label.ID)
}
func (l *labelDaoTestSuite) TestCreate() {
// happy pass is covered by SetupTest
// conflict
_, err := l.dao.Create(l.ctx, &models.Label{
Name: "label_for_label_dao_test_suite",
Scope: "g",
})
l.Require().NotNil(err)
l.True(ierror.IsErr(err, ierror.ConflictCode))
}
func (l *labelDaoTestSuite) TestDelete() {
// happy pass is covered by TearDownTest
// not found
err := l.dao.Delete(l.ctx, 1000)
l.Require().NotNil(err)
l.True(ierror.IsErr(err, ierror.NotFoundCode))
}
func (l *labelDaoTestSuite) TestListByResource() {
labels, err := l.dao.ListByArtifact(l.ctx, l.artID)
l.Require().Nil(err)
l.Require().Len(labels, 1)
l.Equal(l.id, labels[0].ID)
}
func (l *labelDaoTestSuite) TestCreateReference() {
// happy pass is covered by SetupTest
// conflict
_, err := l.dao.CreateReference(l.ctx, &Reference{
LabelID: l.id,
ArtifactID: l.artID,
})
l.Require().NotNil(err)
l.True(ierror.IsErr(err, ierror.ConflictCode))
// violating foreign key constraint: the label that the ref tries to refer doesn't exist
_, err = l.dao.CreateReference(l.ctx, &Reference{
LabelID: 1000,
ArtifactID: l.artID,
})
l.Require().NotNil(err)
l.True(ierror.IsErr(err, ierror.NotFoundCode))
// violating foreign key constraint: the artifact that the ref tries to refer doesn't exist
_, err = l.dao.CreateReference(l.ctx, &Reference{
LabelID: l.id,
ArtifactID: 1000,
})
l.Require().NotNil(err)
l.True(ierror.IsErr(err, ierror.NotFoundCode))
}
func (l *labelDaoTestSuite) DeleteReference() {
// happy pass is covered by TearDownTest
// not found
err := l.dao.DeleteReference(l.ctx, 1000)
l.Require().NotNil(err)
l.True(ierror.IsErr(err, ierror.NotFoundCode))
}
func (l *labelDaoTestSuite) DeleteReferences() {
n, err := l.dao.DeleteReferences(l.ctx, &q.Query{
Keywords: map[string]interface{}{
"LabelID": 1000,
},
})
l.Require().Nil(err)
l.Equal(int64(0), n)
}
func TestLabelDaoTestSuite(t *testing.T) {
suite.Run(t, &labelDaoTestSuite{})
}

94
src/pkg/label/manager.go Normal file
View File

@ -0,0 +1,94 @@
// 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 label
import (
"context"
"github.com/goharbor/harbor/src/common/models"
ierror "github.com/goharbor/harbor/src/internal/error"
"github.com/goharbor/harbor/src/pkg/q"
"time"
)
// Mgr is a global instance of label manager
var Mgr = New()
// Manager manages the labels and references between label and resource
type Manager interface {
// Get the label specified by ID
Get(ctx context.Context, id int64) (label *models.Label, err error)
// List labels that added to the artifact specified by the ID
ListByArtifact(ctx context.Context, artifactID int64) (labels []*models.Label, err error)
// Add label to the artifact specified the ID
AddTo(ctx context.Context, labelID int64, artifactID int64) (err error)
// Remove the label added to the artifact specified by the ID
RemoveFrom(ctx context.Context, labelID int64, artifactID int64) (err error)
// Remove all labels added to the artifact specified by the ID
RemoveAllFrom(ctx context.Context, artifactID int64) (err error)
}
// New creates an instance of the default label manager
func New() Manager {
return &manager{
dao: &defaultDAO{},
}
}
type manager struct {
dao DAO
}
func (m *manager) Get(ctx context.Context, id int64) (*models.Label, error) {
return m.dao.Get(ctx, id)
}
func (m *manager) ListByArtifact(ctx context.Context, artifactID int64) ([]*models.Label, error) {
return m.dao.ListByArtifact(ctx, artifactID)
}
func (m *manager) AddTo(ctx context.Context, labelID int64, artifactID int64) error {
now := time.Now()
_, err := m.dao.CreateReference(ctx, &Reference{
LabelID: labelID,
ArtifactID: artifactID,
CreationTime: now,
UpdateTime: now,
})
return err
}
func (m *manager) RemoveFrom(ctx context.Context, labelID int64, artifactID int64) error {
n, err := m.dao.DeleteReferences(ctx, &q.Query{
Keywords: map[string]interface{}{
"LabelID": labelID,
"ArtifactID": artifactID,
},
})
if err != nil {
return err
}
if n == 0 {
return ierror.NotFoundError(nil).WithMessage("reference with label %d and artifact %d not found", labelID, artifactID)
}
return nil
}
func (m *manager) RemoveAllFrom(ctx context.Context, artifactID int64) error {
_, err := m.dao.DeleteReferences(ctx, &q.Query{
Keywords: map[string]interface{}{
"ArtifactID": artifactID,
},
})
return err
}

View File

@ -0,0 +1,123 @@
// 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 label
import (
"context"
"github.com/goharbor/harbor/src/common/models"
ierror "github.com/goharbor/harbor/src/internal/error"
"github.com/goharbor/harbor/src/pkg/q"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
"testing"
)
type fakeDao struct {
mock.Mock
}
func (f *fakeDao) Get(ctx context.Context, id int64) (*models.Label, error) {
args := f.Called()
var label *models.Label
if args.Get(0) != nil {
label = args.Get(0).(*models.Label)
}
return label, args.Error(1)
}
func (f *fakeDao) Create(ctx context.Context, label *models.Label) (int64, error) {
args := f.Called()
return int64(args.Int(0)), args.Error(1)
}
func (f *fakeDao) Delete(ctx context.Context, id int64) error {
args := f.Called()
return args.Error(0)
}
func (f *fakeDao) ListByArtifact(ctx context.Context, artifactID int64) ([]*models.Label, error) {
args := f.Called()
var labels []*models.Label
if args.Get(0) != nil {
labels = args.Get(0).([]*models.Label)
}
return labels, args.Error(1)
}
func (f *fakeDao) CreateReference(ctx context.Context, reference *Reference) (int64, error) {
args := f.Called()
return int64(args.Int(0)), args.Error(1)
}
func (f *fakeDao) DeleteReference(ctx context.Context, id int64) error {
args := f.Called()
return args.Error(0)
}
func (f *fakeDao) DeleteReferences(ctx context.Context, query *q.Query) (int64, error) {
args := f.Called()
return int64(args.Int(0)), args.Error(1)
}
type managerTestSuite struct {
suite.Suite
mgr *manager
dao *fakeDao
}
func (m *managerTestSuite) SetupTest() {
m.dao = &fakeDao{}
m.mgr = &manager{
dao: m.dao,
}
}
func (m *managerTestSuite) TestGet() {
m.dao.On("Get").Return(nil, nil)
_, err := m.mgr.Get(nil, 1)
m.Require().Nil(err)
}
func (m *managerTestSuite) TestListArtifact() {
m.dao.On("ListByArtifact").Return(nil, nil)
_, err := m.mgr.ListByArtifact(nil, 1)
m.Require().Nil(err)
}
func (m *managerTestSuite) TestAddTo() {
m.dao.On("CreateReference").Return(1, nil)
err := m.mgr.AddTo(nil, 1, 1)
m.Require().Nil(err)
}
func (m *managerTestSuite) TestRemoveFrom() {
// success
m.dao.On("DeleteReferences").Return(1, nil)
err := m.mgr.RemoveFrom(nil, 1, 1)
m.Require().Nil(err)
// reset mock
m.SetupTest()
// not found
m.dao.On("DeleteReferences").Return(0, nil)
err = m.mgr.RemoveFrom(nil, 1, 1)
m.Require().NotNil(err)
m.True(ierror.IsErr(err, ierror.NotFoundCode))
}
func (m *managerTestSuite) TestRemoveAllFrom() {
m.dao.On("DeleteReferences").Return(2, nil)
err := m.mgr.RemoveAllFrom(nil, 1)
m.Require().Nil(err)
}
func TestManager(t *testing.T) {
suite.Run(t, &managerTestSuite{})
}

31
src/pkg/label/model.go Normal file
View File

@ -0,0 +1,31 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package label
import "time"
// Reference is the reference of label and artifact
type Reference struct {
ID int64 `orm:"pk;auto;column(id)"`
LabelID int64 `orm:"column(label_id)"`
ArtifactID int64 `orm:"column(artifact_id)"`
CreationTime time.Time `orm:"column(creation_time);auto_now_add"`
UpdateTime time.Time `orm:"column(update_time);auto_now"`
}
// TableName defines the database table name
func (r *Reference) TableName() string {
return "label_reference"
}

View File

@ -96,6 +96,7 @@ func (d *dao) Create(ctx context.Context, tag *tag.Tag) (int64, error) {
return 0, err
}
id, err := ormer.Insert(tag)
if err != nil {
if e := orm.AsConflictError(err, "tag %s already exists under the repository %d",
tag.Name, tag.RepositoryID); e != nil {
err = e
@ -103,6 +104,7 @@ func (d *dao) Create(ctx context.Context, tag *tag.Tag) (int64, error) {
tag.Name, tag.ArtifactID); e != nil {
err = e
}
}
return id, err
}
func (d *dao) Update(ctx context.Context, tag *tag.Tag, props ...string) error {

View File

@ -193,6 +193,34 @@ func (a *artifactAPI) GetAddition(ctx context.Context, params operation.GetAddit
})
}
func (a *artifactAPI) AddLabel(ctx context.Context, params operation.AddLabelParams) middleware.Responder {
if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionCreate, rbac.ResourceArtifactLabel); err != nil {
return a.SendError(ctx, err)
}
art, err := a.artCtl.GetByReference(ctx, fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName), params.Reference, nil)
if err != nil {
return a.SendError(ctx, err)
}
if err = a.artCtl.AddLabel(ctx, art.ID, params.Label.ID); err != nil {
return a.SendError(ctx, err)
}
return operation.NewAddLabelOK()
}
func (a *artifactAPI) RemoveLabel(ctx context.Context, params operation.RemoveLabelParams) middleware.Responder {
if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionDelete, rbac.ResourceArtifactLabel); err != nil {
return a.SendError(ctx, err)
}
art, err := a.artCtl.GetByReference(ctx, fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName), params.Reference, nil)
if err != nil {
return a.SendError(ctx, err)
}
if err = a.artCtl.RemoveLabel(ctx, art.ID, params.LabelID); err != nil {
return a.SendError(ctx, err)
}
return operation.NewRemoveLabelOK()
}
func option(withTag, withImmutableStatus, withLabel, withScanOverview, withSignature *bool) *artifact.Option {
option := &artifact.Option{
WithTag: true, // return the tag by default

View File

@ -107,3 +107,15 @@ func (f *FakeController) GetAddition(ctx context.Context, artifactID int64, addi
}
return res, args.Error(1)
}
// AddLabel ...
func (f *FakeController) AddLabel(ctx context.Context, artifactID int64, labelID int64) error {
args := f.Called()
return args.Error(0)
}
// RemoveLabel ...
func (f *FakeController) RemoveLabel(ctx context.Context, artifactID int64, labelID int64) error {
args := f.Called()
return args.Error(0)
}

View File

@ -0,0 +1,64 @@
// 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 label
import (
"context"
"github.com/goharbor/harbor/src/common/models"
"github.com/stretchr/testify/mock"
)
// FakeManager is a fake label manager that implement the src/pkg/label.Manager interface
type FakeManager struct {
mock.Mock
}
// Get ...
func (f *FakeManager) Get(ctx context.Context, id int64) (*models.Label, error) {
args := f.Called()
var label *models.Label
if args.Get(0) != nil {
label = args.Get(0).(*models.Label)
}
return label, args.Error(1)
}
// ListByArtifact ...
func (f *FakeManager) ListByArtifact(ctx context.Context, artifactID int64) ([]*models.Label, error) {
args := f.Called()
var labels []*models.Label
if args.Get(0) != nil {
labels = args.Get(0).([]*models.Label)
}
return labels, args.Error(1)
}
// AddTo ...
func (f *FakeManager) AddTo(ctx context.Context, labelID int64, artifactID int64) error {
args := f.Called()
return args.Error(0)
}
// RemoveFrom ...
func (f *FakeManager) RemoveFrom(ctx context.Context, labelID int64, artifactID int64) error {
args := f.Called()
return args.Error(0)
}
// RemoveAllFrom ...
func (f *FakeManager) RemoveAllFrom(ctx context.Context, artifactID int64) error {
args := f.Called()
return args.Error(0)
}