Implement artifact/tag related API

Implement APIs:
1. Get artifact
2. Delete artifact
3. Create tag
4. Delete tag

Signed-off-by: Wenkai Yin <yinw@vmware.com>
This commit is contained in:
Wenkai Yin 2020-01-26 12:52:51 +08:00
parent e75220ab38
commit 793b23a444
5 changed files with 300 additions and 108 deletions

View File

@ -18,80 +18,6 @@ securityDefinitions:
security:
- basicAuth: []
paths:
/projects/{project_name}/repositories/{repository_name}/artifacts/{digest}:
get:
summary: Get the specific artifact
description: Get the artifact specified by the digest under the project and repository
tags:
- artifact
operationId: getArtifact
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectName'
- $ref: '#/parameters/repositoryName'
- $ref: '#/parameters/digest'
- name: with_tag
in: query
description: Specify whether the tags are inclued inside the returning artifacts
type: boolean
required: false
default: true
- name: with_label
in: query
description: Specify whether the labels are inclued inside the returning artifacts
type: boolean
required: false
default: false
- name: with_scan_overview
in: query
description: Specify whether the scan overview is inclued inside the returning artifacts
type: boolean
required: false
default: false
# should be in tag level
- name: with_signatrue
in: query
description: Specify whether the signature is inclued inside the returning artifacts
type: boolean
required: false
default: false
- name: with_immutable_status
in: query
description: Specify whether the immutable status is inclued inside the tags of the returning artifacts. Only works when setting "with_tag=true"
type: boolean
required: false
default: false
responses:
'200':
description: Success
schema:
$ref: '#/definitions/Artifact'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'500':
$ref: '#/responses/500'
delete:
summary: Delete the specific artifact
description: Delete the artifact specified by the digest under the project and repository
tags:
- artifact
operationId: deleteArtifact
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectName'
- $ref: '#/parameters/repositoryName'
- $ref: '#/parameters/digest'
responses:
'200':
description: Success
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'500':
$ref: '#/responses/500'
/projects/{project_name}/repositories/{repository_name}/artifacts:
get:
summary: List artifacts
@ -129,7 +55,7 @@ paths:
required: false
default: false
# should be in tag level
- name: with_signatrue
- name: with_signature
in: query
description: Specify whether the signature is inclued inside the returning artifacts
type: boolean
@ -156,10 +82,151 @@ paths:
type: array
items:
$ref: '#/definitions/Artifact'
'400':
$ref: '#/responses/400'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
/projects/{project_name}/repositories/{repository_name}/artifacts/{reference}:
get:
summary: Get the specific artifact
description: Get the artifact specified by the reference under the project and repository. The reference can be digest or tag.
tags:
- artifact
operationId: getArtifact
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectName'
- $ref: '#/parameters/repositoryName'
- $ref: '#/parameters/reference'
- name: with_tag
in: query
description: Specify whether the tags are inclued inside the returning artifacts
type: boolean
required: false
default: true
- name: with_label
in: query
description: Specify whether the labels are inclued inside the returning artifacts
type: boolean
required: false
default: false
- name: with_scan_overview
in: query
description: Specify whether the scan overview is inclued inside the returning artifacts
type: boolean
required: false
default: false
# should be in tag level
- name: with_signature
in: query
description: Specify whether the signature is inclued inside the returning artifacts
type: boolean
required: false
default: false
- name: with_immutable_status
in: query
description: Specify whether the immutable status is inclued inside the tags of the returning artifacts. Only works when setting "with_tag=true"
type: boolean
required: false
default: false
responses:
'200':
description: Success
schema:
$ref: '#/definitions/Artifact'
'400':
$ref: '#/responses/400'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
delete:
summary: Delete the specific artifact
description: Delete the artifact specified by the reference under the project and repository. The reference can be digest or tag
tags:
- artifact
operationId: deleteArtifact
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectName'
- $ref: '#/parameters/repositoryName'
- $ref: '#/parameters/reference'
responses:
'200':
$ref: '#/responses/200'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
/projects/{project_name}/repositories/{repository_name}/artifacts/{reference}/tags:
post:
summary: Create tag
description: Create a tag for the specified artifact
tags:
- artifact
operationId: createTag
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectName'
- $ref: '#/parameters/repositoryName'
- $ref: '#/parameters/reference'
- name: tag
in: body
description: The JSON object of tag.
required: true
schema:
$ref: '#/definitions/Tag'
responses:
'201':
$ref: '#/responses/201'
'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}/tags/{tag_name}:
delete:
summary: Delete tag
description: Delete the tag of the specified artifact
tags:
- artifact
operationId: deleteTag
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectName'
- $ref: '#/parameters/repositoryName'
- $ref: '#/parameters/reference'
- $ref: '#/parameters/tagName'
responses:
'200':
$ref: '#/responses/200'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
parameters:
@ -182,10 +249,16 @@ parameters:
description: The name of the repository
required: true
type: string
digest:
name: digest
reference:
name: reference
in: path
description: The digest of the artifact
description: The reference of the artifact, can be digest or tag
required: true
type: string
tagName:
name: tag_name
in: path
description: The name of the tag
required: true
type: string
page:
@ -205,6 +278,26 @@ parameters:
description: The size of per page
default: 10
responses:
'200':
description: Success
headers:
X-Request-Id:
description: The ID of the corresponding request for the response
type: string
'201':
description: Created
headers:
X-Request-Id:
description: The ID of the corresponding request for the response
type: string
'400':
description: Bad request
headers:
X-Request-Id:
description: The ID of the corresponding request for the response
type: string
schema:
$ref: '#/definitions/Error'
'401':
description: Unauthorized
headers:
@ -221,6 +314,22 @@ responses:
type: string
schema:
$ref: '#/definitions/Error'
'404':
description: Not found
headers:
X-Request-Id:
description: The ID of the corresponding request for the response
type: string
schema:
$ref: '#/definitions/Error'
'409':
description: Conflict
headers:
X-Request-Id:
description: The ID of the corresponding request for the response
type: string
schema:
$ref: '#/definitions/Error'
'500':
description: Internal server error
headers:

View File

@ -54,8 +54,10 @@ type Controller interface {
GetByReference(ctx context.Context, repository, reference string, option *Option) (artifact *Artifact, err error)
// Delete the artifact specified by ID. All tags attached to the artifact are deleted as well
Delete(ctx context.Context, id int64) (err error)
// Tags returns the tags according to the query, specify the properties returned with option
Tags(ctx context.Context, query *q.Query, option *TagOption) (total int64, tags []*Tag, err error)
// ListTags lists the tags according to the query, specify the properties returned with option
ListTags(ctx context.Context, query *q.Query, option *TagOption) (total int64, tags []*Tag, err error)
// CreateTag creates a tag
CreateTag(ctx context.Context, tag *Tag) (id int64, err error)
// DeleteTag deletes the tag specified by tagID
DeleteTag(ctx context.Context, tagID int64) (err error)
// UpdatePullTime updates the pull time for the artifact. If the tagID is provides, update the pull
@ -285,7 +287,11 @@ func (c *controller) Delete(ctx context.Context, id int64) error {
// TODO fire delete artifact event
return nil
}
func (c *controller) Tags(ctx context.Context, query *q.Query, option *TagOption) (int64, []*Tag, error) {
func (c *controller) CreateTag(ctx context.Context, tag *Tag) (int64, error) {
return c.tagMgr.Create(ctx, &(tag.Tag))
}
func (c *controller) ListTags(ctx context.Context, query *q.Query, option *TagOption) (int64, []*Tag, error) {
total, tgs, err := c.tagMgr.List(ctx, query)
if err != nil {
return 0, nil, err

View File

@ -385,7 +385,7 @@ func (c *controllerTestSuite) TestDelete() {
c.tagMgr.AssertExpectations(c.T())
}
func (c *controllerTestSuite) TestTags() {
func (c *controllerTestSuite) TestListTags() {
c.tagMgr.On("List").Return(1, []*tag.Tag{
{
ID: 1,
@ -394,7 +394,7 @@ func (c *controllerTestSuite) TestTags() {
Name: "latest",
},
}, nil)
total, tags, err := c.ctl.Tags(nil, nil, nil)
total, tags, err := c.ctl.ListTags(nil, nil, nil)
c.Require().Nil(err)
c.Equal(int64(1), total)
c.Len(tags, 1)
@ -402,6 +402,13 @@ func (c *controllerTestSuite) TestTags() {
// TODO check other properties: label, etc
}
func (c *controllerTestSuite) TestCreateTag() {
c.tagMgr.On("Create").Return(1, nil)
id, err := c.ctl.CreateTag(nil, &Tag{})
c.Require().Nil(err)
c.Equal(int64(1), id)
}
func (c *controllerTestSuite) TestDeleteTag() {
c.tagMgr.On("Delete").Return(nil)
err := c.ctl.DeleteTag(nil, 1)

View File

@ -76,6 +76,7 @@ require (
github.com/stretchr/testify v1.4.0
github.com/theupdateframework/notary v0.6.1
golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421
gopkg.in/asn1-ber.v1 v1.0.0-20150924051756-4e86f4367175 // indirect
gopkg.in/dancannon/gorethink.v3 v3.0.5 // indirect

View File

@ -19,11 +19,13 @@ import (
"fmt"
"github.com/go-openapi/runtime/middleware"
"github.com/goharbor/harbor/src/api/artifact"
ierror "github.com/goharbor/harbor/src/internal/error"
"github.com/goharbor/harbor/src/pkg/project"
"github.com/goharbor/harbor/src/pkg/q"
"github.com/goharbor/harbor/src/pkg/repository"
"github.com/goharbor/harbor/src/server/v2.0/models"
operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/artifact"
"time"
)
func newArtifactAPI() *artifactAPI {
@ -64,28 +66,8 @@ func (a *artifactAPI) ListArtifacts(ctx context.Context, params operation.ListAr
query.Keywords["RepositoryID"] = repository.RepositoryID
// set option
option := &artifact.Option{
WithTag: true, // return the tag by default
}
if params.WithTag != nil {
option.WithTag = *(params.WithTag)
}
if option.WithTag {
if params.WithImmutableStatus != nil {
option.TagOption = &artifact.TagOption{
WithImmutableStatus: *(params.WithImmutableStatus),
}
}
}
if params.WithLabel != nil {
option.WithLabel = *(params.WithLabel)
}
if params.WithScanOverview != nil {
option.WithScanOverview = *(params.WithScanOverview)
}
if params.WithSignatrue != nil {
option.WithSignature = *(params.WithSignatrue)
}
option := option(params.WithTag, params.WithImmutableStatus,
params.WithLabel, params.WithScanOverview, params.WithSignature)
// list artifacts according to the query and option
total, arts, err := a.artCtl.List(ctx, query, option)
@ -103,11 +85,98 @@ func (a *artifactAPI) ListArtifacts(ctx context.Context, params operation.ListAr
}
func (a *artifactAPI) GetArtifact(ctx context.Context, params operation.GetArtifactParams) middleware.Responder {
// TODO implement
return operation.NewGetArtifactOK()
// set option
option := option(params.WithTag, params.WithImmutableStatus,
params.WithLabel, params.WithScanOverview, params.WithSignature)
// get the artifact
artifact, err := a.artCtl.GetByReference(ctx, fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName), params.Reference, option)
if err != nil {
return a.SendError(ctx, err)
}
return operation.NewGetArtifactOK().WithPayload(artifact.ToSwagger())
}
func (a *artifactAPI) DeleteArtifact(ctx context.Context, params operation.DeleteArtifactParams) middleware.Responder {
// TODO implement
artifact, err := a.artCtl.GetByReference(ctx, fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName), params.Reference, nil)
if err != nil {
return a.SendError(ctx, err)
}
if err = a.artCtl.Delete(ctx, artifact.ID); err != nil {
return a.SendError(ctx, err)
}
return operation.NewDeleteArtifactOK()
}
func (a *artifactAPI) CreateTag(ctx context.Context, params operation.CreateTagParams) middleware.Responder {
art, err := a.artCtl.GetByReference(ctx, fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName),
params.Reference, &artifact.Option{
WithTag: true,
})
if err != nil {
return a.SendError(ctx, err)
}
tag := &artifact.Tag{}
tag.RepositoryID = art.RepositoryID
tag.ArtifactID = art.ID
tag.Name = params.Tag.Name
tag.PushTime = time.Now()
if _, err = a.artCtl.CreateTag(ctx, tag); err != nil {
return a.SendError(ctx, err)
}
// TODO set location header?
return operation.NewCreateTagCreated()
}
func (a *artifactAPI) DeleteTag(ctx context.Context, params operation.DeleteTagParams) middleware.Responder {
artifact, err := a.artCtl.GetByReference(ctx, fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName),
params.Reference, &artifact.Option{
WithTag: true,
})
if err != nil {
return a.SendError(ctx, err)
}
var id int64
for _, tag := range artifact.Tags {
if tag.Name == params.TagName {
id = tag.ID
break
}
}
// the tag not found
if id == 0 {
err = ierror.New(nil).WithCode(ierror.NotFoundCode).WithMessage(
"tag %s attached to artifact %d not found", params.TagName, artifact.ID)
return a.SendError(ctx, err)
}
if err = a.artCtl.DeleteTag(ctx, id); err != nil {
return a.SendError(ctx, err)
}
return operation.NewDeleteTagOK()
}
func option(withTag, withImmutableStatus, withLabel, withScanOverview, withSignature *bool) *artifact.Option {
option := &artifact.Option{
WithTag: true, // return the tag by default
}
if withTag != nil {
option.WithTag = *(withTag)
}
if option.WithTag {
if withImmutableStatus != nil {
option.TagOption = &artifact.TagOption{
WithImmutableStatus: *(withImmutableStatus),
}
}
}
if withLabel != nil {
option.WithLabel = *(withLabel)
}
if withScanOverview != nil {
option.WithScanOverview = *(withScanOverview)
}
if withSignature != nil {
option.WithSignature = *(withSignature)
}
return option
}