mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-22 16:48:30 +01:00
Merge pull request #10682 from ywk253100/200126_label
Support add/remove label to/from artifact
This commit is contained in:
commit
d7903fcf1b
@ -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
|
||||
|
||||
|
@ -41,4 +41,19 @@ CREATE TABLE artifact_reference
|
||||
FOREIGN KEY (parent_id) REFERENCES artifact_2(id),
|
||||
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)
|
||||
);
|
||||
|
@ -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
|
||||
}
|
||||
@ -269,6 +276,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{}{
|
||||
@ -311,7 +322,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)
|
||||
}
|
||||
@ -347,6 +357,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{
|
||||
@ -355,34 +373,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{
|
||||
@ -392,30 +414,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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
@ -407,6 +419,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())
|
||||
@ -479,6 +492,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{})
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -52,9 +52,9 @@ const (
|
||||
ResourceRepository = Resource("repository")
|
||||
ResourceTagRetention = Resource("tag-retention")
|
||||
ResourceImmutableTag = Resource("immutable-tag")
|
||||
ResourceRepositoryLabel = Resource("repository-label")
|
||||
ResourceRepositoryTag = Resource("repository-tag") // TODO remove
|
||||
ResourceRepositoryTagLabel = Resource("repository-tag-label")
|
||||
ResourceRepositoryLabel = Resource("repository-label") // TODO remove
|
||||
ResourceRepositoryTag = Resource("repository-tag") // TODO remove
|
||||
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
|
||||
)
|
||||
|
@ -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": {
|
||||
|
@ -191,12 +191,14 @@ func (d *dao) CreateReference(ctx context.Context, reference *ArtifactReference)
|
||||
return 0, err
|
||||
}
|
||||
id, err := ormer.Insert(reference)
|
||||
if e := orm.AsConflictError(err, "reference already exists, parent artifact ID: %d, child artifact ID: %d",
|
||||
reference.ParentID, reference.ChildID); e != nil {
|
||||
err = e
|
||||
} else if e := orm.AsForeignKeyError(err, "the reference tries to reference a non existing artifact, parent artifact ID: %d, child artifact ID: %d",
|
||||
reference.ParentID, reference.ChildID); e != nil {
|
||||
err = e
|
||||
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
|
||||
} else if e := orm.AsForeignKeyError(err, "the reference tries to reference a non existing artifact, parent artifact ID: %d, child artifact ID: %d",
|
||||
reference.ParentID, reference.ChildID); e != nil {
|
||||
err = e
|
||||
}
|
||||
}
|
||||
return id, err
|
||||
}
|
||||
|
158
src/pkg/label/dao.go
Normal file
158
src/pkg/label/dao.go
Normal 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
175
src/pkg/label/dao_test.go
Normal 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
94
src/pkg/label/manager.go
Normal 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
|
||||
}
|
123
src/pkg/label/manager_test.go
Normal file
123
src/pkg/label/manager_test.go
Normal 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
31
src/pkg/label/model.go
Normal 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"
|
||||
}
|
@ -96,12 +96,14 @@ func (d *dao) Create(ctx context.Context, tag *tag.Tag) (int64, error) {
|
||||
return 0, err
|
||||
}
|
||||
id, err := ormer.Insert(tag)
|
||||
if e := orm.AsConflictError(err, "tag %s already exists under the repository %d",
|
||||
tag.Name, tag.RepositoryID); e != nil {
|
||||
err = e
|
||||
} else if e := orm.AsForeignKeyError(err, "the tag %s tries to attach to a non existing artifact %d",
|
||||
tag.Name, tag.ArtifactID); e != nil {
|
||||
err = e
|
||||
if err != nil {
|
||||
if e := orm.AsConflictError(err, "tag %s already exists under the repository %d",
|
||||
tag.Name, tag.RepositoryID); e != nil {
|
||||
err = e
|
||||
} else if e := orm.AsForeignKeyError(err, "the tag %s tries to attach to a non existing artifact %d",
|
||||
tag.Name, tag.ArtifactID); e != nil {
|
||||
err = e
|
||||
}
|
||||
}
|
||||
return id, err
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
64
src/testing/pkg/label/manager.go
Normal file
64
src/testing/pkg/label/manager.go
Normal 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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user