mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-25 19:56:09 +01:00
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:
parent
5e34ba0c97
commit
93731eeb2e
@ -270,15 +270,13 @@ paths:
|
|||||||
- $ref: '#/parameters/reference'
|
- $ref: '#/parameters/reference'
|
||||||
- name: addition
|
- name: addition
|
||||||
in: path
|
in: path
|
||||||
description: The addition name, "build_history" for images; "values.yaml", "readme", "dependencies" for charts
|
description: The type of addition.
|
||||||
type: string
|
type: string
|
||||||
enum: [build_history, values.yaml, readme, dependencies]
|
enum: [build_history, values.yaml, readme, dependencies]
|
||||||
required: true
|
required: true
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Success
|
$ref: '#/responses/200'
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
'400':
|
'400':
|
||||||
$ref: '#/responses/400'
|
$ref: '#/responses/400'
|
||||||
'401':
|
'401':
|
||||||
@ -289,6 +287,70 @@ paths:
|
|||||||
$ref: '#/responses/404'
|
$ref: '#/responses/404'
|
||||||
'500':
|
'500':
|
||||||
$ref: '#/responses/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:
|
parameters:
|
||||||
requestId:
|
requestId:
|
||||||
name: X-Request-Id
|
name: X-Request-Id
|
||||||
@ -467,6 +529,10 @@ definitions:
|
|||||||
$ref: '#/definitions/Tag'
|
$ref: '#/definitions/Tag'
|
||||||
addition_links:
|
addition_links:
|
||||||
$ref: '#/definitions/AdditionLinks'
|
$ref: '#/definitions/AdditionLinks'
|
||||||
|
labels:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/Label'
|
||||||
Tag:
|
Tag:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@ -555,4 +621,38 @@ definitions:
|
|||||||
variant:
|
variant:
|
||||||
type: string
|
type: string
|
||||||
description: The variant of the CPU
|
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
|
||||||
|
|
||||||
|
@ -42,3 +42,18 @@ CREATE TABLE artifact_reference
|
|||||||
FOREIGN KEY (child_id) REFERENCES artifact_2(id),
|
FOREIGN KEY (child_id) REFERENCES artifact_2(id),
|
||||||
CONSTRAINT unique_reference UNIQUE (parent_id, child_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/art"
|
||||||
"github.com/goharbor/harbor/src/pkg/immutabletag/match"
|
"github.com/goharbor/harbor/src/pkg/immutabletag/match"
|
||||||
"github.com/goharbor/harbor/src/pkg/immutabletag/match/rule"
|
"github.com/goharbor/harbor/src/pkg/immutabletag/match/rule"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/label"
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -77,6 +78,10 @@ type Controller interface {
|
|||||||
// The addition is different according to the artifact type:
|
// The addition is different according to the artifact type:
|
||||||
// build history for image; values.yaml, readme and dependencies for chart, etc
|
// 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)
|
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?
|
// TODO move this to GC controller?
|
||||||
// Prune removes the useless artifact records. The underlying registry data will
|
// Prune removes the useless artifact records. The underlying registry data will
|
||||||
// be removed during garbage collection
|
// be removed during garbage collection
|
||||||
@ -89,6 +94,7 @@ func NewController() Controller {
|
|||||||
repoMgr: repository.Mgr,
|
repoMgr: repository.Mgr,
|
||||||
artMgr: artifact.Mgr,
|
artMgr: artifact.Mgr,
|
||||||
tagMgr: tag.Mgr,
|
tagMgr: tag.Mgr,
|
||||||
|
labelMgr: label.Mgr,
|
||||||
abstractor: abstractor.NewAbstractor(),
|
abstractor: abstractor.NewAbstractor(),
|
||||||
immutableMtr: rule.NewRuleMatcher(),
|
immutableMtr: rule.NewRuleMatcher(),
|
||||||
}
|
}
|
||||||
@ -100,6 +106,7 @@ type controller struct {
|
|||||||
repoMgr repository.Manager
|
repoMgr repository.Manager
|
||||||
artMgr artifact.Manager
|
artMgr artifact.Manager
|
||||||
tagMgr tag.Manager
|
tagMgr tag.Manager
|
||||||
|
labelMgr label.Manager
|
||||||
abstractor abstractor.Abstractor
|
abstractor abstractor.Abstractor
|
||||||
immutableMtr match.ImmutableTagMatcher
|
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 {
|
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
|
// delete all tags that attached to the artifact
|
||||||
_, tags, err := c.tagMgr.List(ctx, &q.Query{
|
_, tags, err := c.tagMgr.List(ctx, &q.Query{
|
||||||
Keywords: map[string]interface{}{
|
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 {
|
func (c *controller) DeleteTag(ctx context.Context, tagID int64) error {
|
||||||
// Immutable checking is covered in middleware
|
// Immutable checking is covered in middleware
|
||||||
// TODO check signature
|
// TODO check signature
|
||||||
// TODO delete label
|
|
||||||
// TODO fire delete tag event
|
// TODO fire delete tag event
|
||||||
return c.tagMgr.Delete(ctx, tagID)
|
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
|
// assemble several part into a single artifact
|
||||||
func (c *controller) assembleArtifact(ctx context.Context, art *artifact.Artifact, option *Option) *Artifact {
|
func (c *controller) assembleArtifact(ctx context.Context, art *artifact.Artifact, option *Option) *Artifact {
|
||||||
artifact := &Artifact{
|
artifact := &Artifact{
|
||||||
@ -370,34 +388,38 @@ func (c *controller) assembleArtifact(ctx context.Context, art *artifact.Artifac
|
|||||||
if option == nil {
|
if option == nil {
|
||||||
return artifact
|
return artifact
|
||||||
}
|
}
|
||||||
// populate tags
|
|
||||||
if option.WithTag {
|
if option.WithTag {
|
||||||
_, tgs, err := c.tagMgr.List(ctx, &q.Query{
|
c.populateTags(ctx, artifact, option.TagOption)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if option.WithLabel {
|
if option.WithLabel {
|
||||||
// TODO populate label
|
c.populateLabels(ctx, artifact)
|
||||||
}
|
}
|
||||||
if option.WithScanOverview {
|
if option.WithScanOverview {
|
||||||
// TODO populate scan overview
|
c.populateScanOverview(ctx, artifact)
|
||||||
|
}
|
||||||
|
if option.WithSignature {
|
||||||
|
c.populateSignature(ctx, artifact)
|
||||||
}
|
}
|
||||||
// populate addition links
|
// populate addition links
|
||||||
c.populateAdditionLinks(ctx, artifact)
|
c.populateAdditionLinks(ctx, artifact)
|
||||||
// TODO populate signature on artifact or label level?
|
|
||||||
return artifact
|
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
|
// assemble several part into a single tag
|
||||||
func (c *controller) assembleTag(ctx context.Context, tag *tm.Tag, option *TagOption) *Tag {
|
func (c *controller) assembleTag(ctx context.Context, tag *tm.Tag, option *TagOption) *Tag {
|
||||||
t := &Tag{
|
t := &Tag{
|
||||||
@ -407,30 +429,46 @@ func (c *controller) assembleTag(ctx context.Context, tag *tm.Tag, option *TagOp
|
|||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
if option.WithImmutableStatus {
|
if option.WithImmutableStatus {
|
||||||
repo, err := c.repoMgr.Get(ctx, tag.RepositoryID)
|
c.populateImmutableStatus(ctx, t)
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
} else {
|
|
||||||
t.Immutable = c.isImmutable(repo.ProjectID, repo.Name, tag.Name)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// TODO populate signature on tag level?
|
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
// check whether the tag is Immutable
|
func (c *controller) populateLabels(ctx context.Context, art *Artifact) {
|
||||||
func (c *controller) isImmutable(projectID int64, repo string, tag string) bool {
|
labels, err := c.labelMgr.ListByArtifact(ctx, art.ID)
|
||||||
_, repoName := utils.ParseRepository(repo)
|
if err != nil {
|
||||||
matched, err := c.immutableMtr.Match(projectID, art.Candidate{
|
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,
|
Repository: repoName,
|
||||||
Tag: tag,
|
Tag: tag.Name,
|
||||||
NamespaceID: projectID,
|
NamespaceID: repo.ProjectID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
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) {
|
func (c *controller) populateAdditionLinks(ctx context.Context, artifact *Artifact) {
|
||||||
|
@ -26,6 +26,7 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/pkg/tag/model/tag"
|
"github.com/goharbor/harbor/src/pkg/tag/model/tag"
|
||||||
arttesting "github.com/goharbor/harbor/src/testing/pkg/artifact"
|
arttesting "github.com/goharbor/harbor/src/testing/pkg/artifact"
|
||||||
immutesting "github.com/goharbor/harbor/src/testing/pkg/immutabletag"
|
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"
|
repotesting "github.com/goharbor/harbor/src/testing/pkg/repository"
|
||||||
tagtesting "github.com/goharbor/harbor/src/testing/pkg/tag"
|
tagtesting "github.com/goharbor/harbor/src/testing/pkg/tag"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
@ -69,6 +70,7 @@ type controllerTestSuite struct {
|
|||||||
repoMgr *repotesting.FakeManager
|
repoMgr *repotesting.FakeManager
|
||||||
artMgr *arttesting.FakeManager
|
artMgr *arttesting.FakeManager
|
||||||
tagMgr *tagtesting.FakeManager
|
tagMgr *tagtesting.FakeManager
|
||||||
|
labelMgr *label.FakeManager
|
||||||
abstractor *fakeAbstractor
|
abstractor *fakeAbstractor
|
||||||
immutableMtr *immutesting.FakeMatcher
|
immutableMtr *immutesting.FakeMatcher
|
||||||
}
|
}
|
||||||
@ -77,12 +79,14 @@ func (c *controllerTestSuite) SetupTest() {
|
|||||||
c.repoMgr = &repotesting.FakeManager{}
|
c.repoMgr = &repotesting.FakeManager{}
|
||||||
c.artMgr = &arttesting.FakeManager{}
|
c.artMgr = &arttesting.FakeManager{}
|
||||||
c.tagMgr = &tagtesting.FakeManager{}
|
c.tagMgr = &tagtesting.FakeManager{}
|
||||||
|
c.labelMgr = &label.FakeManager{}
|
||||||
c.abstractor = &fakeAbstractor{}
|
c.abstractor = &fakeAbstractor{}
|
||||||
c.immutableMtr = &immutesting.FakeMatcher{}
|
c.immutableMtr = &immutesting.FakeMatcher{}
|
||||||
c.ctl = &controller{
|
c.ctl = &controller{
|
||||||
repoMgr: c.repoMgr,
|
repoMgr: c.repoMgr,
|
||||||
artMgr: c.artMgr,
|
artMgr: c.artMgr,
|
||||||
tagMgr: c.tagMgr,
|
tagMgr: c.tagMgr,
|
||||||
|
labelMgr: c.labelMgr,
|
||||||
abstractor: c.abstractor,
|
abstractor: c.abstractor,
|
||||||
immutableMtr: c.immutableMtr,
|
immutableMtr: c.immutableMtr,
|
||||||
}
|
}
|
||||||
@ -125,7 +129,7 @@ func (c *controllerTestSuite) TestAssembleArtifact() {
|
|||||||
TagOption: &TagOption{
|
TagOption: &TagOption{
|
||||||
WithImmutableStatus: false,
|
WithImmutableStatus: false,
|
||||||
},
|
},
|
||||||
WithLabel: false,
|
WithLabel: true,
|
||||||
WithScanOverview: true,
|
WithScanOverview: true,
|
||||||
WithSignature: true,
|
WithSignature: true,
|
||||||
}
|
}
|
||||||
@ -142,6 +146,13 @@ func (c *controllerTestSuite) TestAssembleArtifact() {
|
|||||||
Name: "library/hello-world",
|
Name: "library/hello-world",
|
||||||
}, nil)
|
}, nil)
|
||||||
ctx := internal.SetAPIVersion(nil, "2.0")
|
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)
|
artifact := c.ctl.assembleArtifact(ctx, art, option)
|
||||||
c.Require().NotNil(artifact)
|
c.Require().NotNil(artifact)
|
||||||
c.Equal(art.ID, artifact.ID)
|
c.Equal(art.ID, artifact.ID)
|
||||||
@ -151,6 +162,7 @@ func (c *controllerTestSuite) TestAssembleArtifact() {
|
|||||||
c.False(artifact.AdditionLinks["build_history"].Absolute)
|
c.False(artifact.AdditionLinks["build_history"].Absolute)
|
||||||
c.Equal("/api/2.0/projects/library/repositories/hello-world/artifacts/sha256:123/additions/build_history",
|
c.Equal("/api/2.0/projects/library/repositories/hello-world/artifacts/sha256:123/additions/build_history",
|
||||||
artifact.AdditionLinks["build_history"].HREF)
|
artifact.AdditionLinks["build_history"].HREF)
|
||||||
|
c.Contains(artifact.Labels, lb)
|
||||||
// TODO check other fields of option
|
// TODO check other fields of option
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -413,6 +425,7 @@ func (c *controllerTestSuite) TestDelete() {
|
|||||||
},
|
},
|
||||||
}, nil)
|
}, nil)
|
||||||
c.tagMgr.On("Delete").Return(nil)
|
c.tagMgr.On("Delete").Return(nil)
|
||||||
|
c.labelMgr.On("RemoveAllFrom").Return(nil)
|
||||||
err := c.ctl.Delete(nil, 1)
|
err := c.ctl.Delete(nil, 1)
|
||||||
c.Require().Nil(err)
|
c.Require().Nil(err)
|
||||||
c.artMgr.AssertExpectations(c.T())
|
c.artMgr.AssertExpectations(c.T())
|
||||||
@ -485,6 +498,18 @@ func (c *controllerTestSuite) TestGetAddition() {
|
|||||||
c.Require().Nil(err)
|
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) {
|
func TestControllerTestSuite(t *testing.T) {
|
||||||
suite.Run(t, &controllerTestSuite{})
|
suite.Run(t, &controllerTestSuite{})
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ package artifact
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/go-openapi/strfmt"
|
"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/artifact"
|
||||||
"github.com/goharbor/harbor/src/pkg/tag/model/tag"
|
"github.com/goharbor/harbor/src/pkg/tag/model/tag"
|
||||||
"github.com/goharbor/harbor/src/server/v2.0/models"
|
"github.com/goharbor/harbor/src/server/v2.0/models"
|
||||||
@ -25,7 +26,8 @@ import (
|
|||||||
type Artifact struct {
|
type Artifact struct {
|
||||||
artifact.Artifact
|
artifact.Artifact
|
||||||
Tags []*Tag // the list of tags that attached to the 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
|
// TODO add other attrs: signature, scan result, etc
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,6 +84,19 @@ func (a *Artifact) ToSwagger() *models.Artifact {
|
|||||||
Href: link.HREF,
|
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
|
return art
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +104,7 @@ func (a *Artifact) ToSwagger() *models.Artifact {
|
|||||||
type Tag struct {
|
type Tag struct {
|
||||||
tag.Tag
|
tag.Tag
|
||||||
Immutable bool
|
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
|
// AdditionLink is a link via that the addition can be fetched
|
||||||
|
@ -52,9 +52,9 @@ const (
|
|||||||
ResourceRepository = Resource("repository")
|
ResourceRepository = Resource("repository")
|
||||||
ResourceTagRetention = Resource("tag-retention")
|
ResourceTagRetention = Resource("tag-retention")
|
||||||
ResourceImmutableTag = Resource("immutable-tag")
|
ResourceImmutableTag = Resource("immutable-tag")
|
||||||
ResourceRepositoryLabel = Resource("repository-label")
|
ResourceRepositoryLabel = Resource("repository-label") // TODO remove
|
||||||
ResourceRepositoryTag = Resource("repository-tag") // 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")
|
ResourceRepositoryTagManifest = Resource("repository-tag-manifest")
|
||||||
ResourceRepositoryTagScanJob = Resource("repository-tag-scan-job") // TODO: remove
|
ResourceRepositoryTagScanJob = Resource("repository-tag-scan-job") // TODO: remove
|
||||||
ResourceRepositoryTagVulnerability = Resource("repository-tag-vulnerability") // TODO: remove
|
ResourceRepositoryTagVulnerability = Resource("repository-tag-vulnerability") // TODO: remove
|
||||||
@ -65,5 +65,6 @@ const (
|
|||||||
ResourceArtifact = Resource("artifact")
|
ResourceArtifact = Resource("artifact")
|
||||||
ResourceTag = Resource("tag")
|
ResourceTag = Resource("tag")
|
||||||
ResourceArtifactAddition = Resource("artifact-addition")
|
ResourceArtifactAddition = Resource("artifact-addition")
|
||||||
|
ResourceArtifactLabel = Resource("artifact-label")
|
||||||
ResourceSelf = Resource("") // subresource for self
|
ResourceSelf = Resource("") // subresource for self
|
||||||
)
|
)
|
||||||
|
@ -135,6 +135,9 @@ var (
|
|||||||
|
|
||||||
{Resource: rbac.ResourceTag, Action: rbac.ActionCreate},
|
{Resource: rbac.ResourceTag, Action: rbac.ActionCreate},
|
||||||
{Resource: rbac.ResourceTag, Action: rbac.ActionDelete},
|
{Resource: rbac.ResourceTag, Action: rbac.ActionDelete},
|
||||||
|
|
||||||
|
{Resource: rbac.ResourceArtifactLabel, Action: rbac.ActionCreate},
|
||||||
|
{Resource: rbac.ResourceArtifactLabel, Action: rbac.ActionDelete},
|
||||||
},
|
},
|
||||||
|
|
||||||
"master": {
|
"master": {
|
||||||
@ -232,6 +235,9 @@ var (
|
|||||||
|
|
||||||
{Resource: rbac.ResourceTag, Action: rbac.ActionCreate},
|
{Resource: rbac.ResourceTag, Action: rbac.ActionCreate},
|
||||||
{Resource: rbac.ResourceTag, Action: rbac.ActionDelete},
|
{Resource: rbac.ResourceTag, Action: rbac.ActionDelete},
|
||||||
|
|
||||||
|
{Resource: rbac.ResourceArtifactLabel, Action: rbac.ActionCreate},
|
||||||
|
{Resource: rbac.ResourceArtifactLabel, Action: rbac.ActionDelete},
|
||||||
},
|
},
|
||||||
|
|
||||||
"developer": {
|
"developer": {
|
||||||
@ -294,6 +300,9 @@ var (
|
|||||||
{Resource: rbac.ResourceArtifactAddition, Action: rbac.ActionRead},
|
{Resource: rbac.ResourceArtifactAddition, Action: rbac.ActionRead},
|
||||||
|
|
||||||
{Resource: rbac.ResourceTag, Action: rbac.ActionCreate},
|
{Resource: rbac.ResourceTag, Action: rbac.ActionCreate},
|
||||||
|
|
||||||
|
{Resource: rbac.ResourceArtifactLabel, Action: rbac.ActionCreate},
|
||||||
|
{Resource: rbac.ResourceArtifactLabel, Action: rbac.ActionDelete},
|
||||||
},
|
},
|
||||||
|
|
||||||
"guest": {
|
"guest": {
|
||||||
|
@ -166,6 +166,7 @@ func (d *dao) CreateReference(ctx context.Context, reference *ArtifactReference)
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
id, err := ormer.Insert(reference)
|
id, err := ormer.Insert(reference)
|
||||||
|
if err != nil {
|
||||||
if e := orm.AsConflictError(err, "reference already exists, parent artifact ID: %d, child artifact ID: %d",
|
if e := orm.AsConflictError(err, "reference already exists, parent artifact ID: %d, child artifact ID: %d",
|
||||||
reference.ParentID, reference.ChildID); e != nil {
|
reference.ParentID, reference.ChildID); e != nil {
|
||||||
err = e
|
err = e
|
||||||
@ -173,6 +174,7 @@ func (d *dao) CreateReference(ctx context.Context, reference *ArtifactReference)
|
|||||||
reference.ParentID, reference.ChildID); e != nil {
|
reference.ParentID, reference.ChildID); e != nil {
|
||||||
err = e
|
err = e
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return id, err
|
return id, err
|
||||||
}
|
}
|
||||||
func (d *dao) ListReferences(ctx context.Context, query *q.Query) ([]*ArtifactReference, error) {
|
func (d *dao) ListReferences(ctx context.Context, query *q.Query) ([]*ArtifactReference, error) {
|
||||||
|
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,6 +96,7 @@ func (d *dao) Create(ctx context.Context, tag *tag.Tag) (int64, error) {
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
id, err := ormer.Insert(tag)
|
id, err := ormer.Insert(tag)
|
||||||
|
if err != nil {
|
||||||
if e := orm.AsConflictError(err, "tag %s already exists under the repository %d",
|
if e := orm.AsConflictError(err, "tag %s already exists under the repository %d",
|
||||||
tag.Name, tag.RepositoryID); e != nil {
|
tag.Name, tag.RepositoryID); e != nil {
|
||||||
err = e
|
err = e
|
||||||
@ -103,6 +104,7 @@ func (d *dao) Create(ctx context.Context, tag *tag.Tag) (int64, error) {
|
|||||||
tag.Name, tag.ArtifactID); e != nil {
|
tag.Name, tag.ArtifactID); e != nil {
|
||||||
err = e
|
err = e
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return id, err
|
return id, err
|
||||||
}
|
}
|
||||||
func (d *dao) Update(ctx context.Context, tag *tag.Tag, props ...string) error {
|
func (d *dao) Update(ctx context.Context, tag *tag.Tag, props ...string) error {
|
||||||
|
@ -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 {
|
func option(withTag, withImmutableStatus, withLabel, withScanOverview, withSignature *bool) *artifact.Option {
|
||||||
option := &artifact.Option{
|
option := &artifact.Option{
|
||||||
WithTag: true, // return the tag by default
|
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)
|
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