mirror of
https://github.com/goharbor/harbor.git
synced 2025-02-16 20:01:35 +01:00
Update tags related APIs (#11435)
* Update tags related APIs 1. Remove API for listing tags of repository 2. Add API for listing tags of artifact 3. Support filter artifact by tag name Signed-off-by: Wenkai Yin <yinw@vmware.com> * [OCI] modify artifact tag name check 1. switch api get tag list 2. modify artifact tag name check Signed-off-by: Yogi_Wang <yawang@vmware.com> Co-authored-by: Yogi_Wang <yawang@vmware.com>
This commit is contained in:
parent
e064bd4c01
commit
88ae9c458c
@ -138,7 +138,7 @@ paths:
|
||||
/projects/{project_name}/repositories/{repository_name}/artifacts:
|
||||
get:
|
||||
summary: List artifacts
|
||||
description: List artifacts under the specific project and repository. Except the basic properties, the other supported queries in "q" includes "tags=*" to list only tagged artifacts, "tags=nil" to list only untagged artifacts, "tags=~v" to list artifacts whose tag fuzzy matches "v", "labels=(id1, id2)" to list artifacts that both labels with id1 and id2 are added to
|
||||
description: List artifacts under the specific project and repository. Except the basic properties, the other supported queries in "q" includes "tags=*" to list only tagged artifacts, "tags=nil" to list only untagged artifacts, "tags=~v" to list artifacts whose tag fuzzy matches "v", "tags=v" to list artifact whose tag exactly matches "v", "labels=(id1, id2)" to list artifacts that both labels with id1 and id2 are added to
|
||||
tags:
|
||||
- artifact
|
||||
operationId: listArtifacts
|
||||
@ -401,6 +401,56 @@ paths:
|
||||
$ref: '#/responses/409'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
get:
|
||||
summary: List tags
|
||||
description: List tags of the specific artifact
|
||||
tags:
|
||||
- artifact
|
||||
operationId: listTags
|
||||
parameters:
|
||||
- $ref: '#/parameters/requestId'
|
||||
- $ref: '#/parameters/projectName'
|
||||
- $ref: '#/parameters/repositoryName'
|
||||
- $ref: '#/parameters/reference'
|
||||
- $ref: '#/parameters/query'
|
||||
- $ref: '#/parameters/page'
|
||||
- $ref: '#/parameters/pageSize'
|
||||
- name: with_signature
|
||||
in: query
|
||||
description: Specify whether the signature is included inside the returning tags
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
- name: with_immutable_status
|
||||
in: query
|
||||
description: Specify whether the immutable status is included inside the returning tags
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
headers:
|
||||
X-Total-Count:
|
||||
description: The total count of tags
|
||||
type: integer
|
||||
Link:
|
||||
description: Link refers to the previous page and next page
|
||||
type: string
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Tag'
|
||||
'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}/tags/{tag_name}:
|
||||
delete:
|
||||
summary: Delete tag
|
||||
@ -526,56 +576,6 @@ paths:
|
||||
$ref: '#/responses/409'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
/projects/{project_name}/repositories/{repository_name}/tags:
|
||||
get:
|
||||
summary: List tags
|
||||
description: List tags under the specific project and repository.
|
||||
tags:
|
||||
- tag
|
||||
operationId: listTags
|
||||
parameters:
|
||||
- $ref: '#/parameters/requestId'
|
||||
- $ref: '#/parameters/projectName'
|
||||
- $ref: '#/parameters/repositoryName'
|
||||
- $ref: '#/parameters/query'
|
||||
- $ref: '#/parameters/page'
|
||||
- $ref: '#/parameters/pageSize'
|
||||
- name: with_signature
|
||||
in: query
|
||||
description: Specify whether the signature is included inside the returning tags
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
- name: with_immutable_status
|
||||
in: query
|
||||
description: Specify whether the immutable status is included inside the returning tags
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
headers:
|
||||
X-Total-Count:
|
||||
description: The total count of tags
|
||||
type: integer
|
||||
Link:
|
||||
description: Link refers to the previous page and next page
|
||||
type: string
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Tag'
|
||||
'400':
|
||||
$ref: '#/responses/400'
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'403':
|
||||
$ref: '#/responses/403'
|
||||
'404':
|
||||
$ref: '#/responses/404'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
/audit-logs:
|
||||
get:
|
||||
summary: Get recent logs of the projects which the user is a member of
|
||||
|
@ -318,7 +318,10 @@ func setTagQuery(qs beegoorm.QuerySeter, query *q.Query) (beegoorm.QuerySeter, e
|
||||
qs = qs.FilterRaw("id", sql)
|
||||
return qs, nil
|
||||
}
|
||||
// exact match, only handle "*" for listing tagged artifacts and "nil" for listing untagged artifacts
|
||||
// exact match:
|
||||
// "*" for listing tagged artifacts
|
||||
// "nil" for listing untagged artifacts
|
||||
// others for get the artifact with the specified tag
|
||||
s, ok := tags.(string)
|
||||
if ok {
|
||||
if s == "*" {
|
||||
@ -329,9 +332,15 @@ func setTagQuery(qs beegoorm.QuerySeter, query *q.Query) (beegoorm.QuerySeter, e
|
||||
qs = qs.FilterRaw("id", untagged)
|
||||
return qs, nil
|
||||
}
|
||||
sql := fmt.Sprintf(`IN (
|
||||
SELECT DISTINCT art.id FROM artifact art
|
||||
JOIN tag ON art.id=tag.artifact_id
|
||||
WHERE tag.name = '%s')`, orm.Escape(s))
|
||||
qs = qs.FilterRaw("id", sql)
|
||||
return qs, nil
|
||||
}
|
||||
return qs, errors.New(nil).WithCode(errors.BadRequestCode).
|
||||
WithMessage(`the value of "tags" query can only be fuzzy match value or exact match value with "*" and "nil"`)
|
||||
WithMessage(`the value of "tags" query can only be fuzzy match value or exact match value`)
|
||||
}
|
||||
|
||||
// handle query string: q=labels=(1 2 3)
|
||||
|
@ -169,18 +169,18 @@ func (d *daoTestSuite) TestCount() {
|
||||
d.Require().Nil(err)
|
||||
d.Equal(totalOfAll-1, totalOfUnTagged)
|
||||
|
||||
// invalid tags value
|
||||
_, err = d.dao.Count(d.ctx, &q.Query{
|
||||
// specific tag value
|
||||
total, err := d.dao.Count(d.ctx, &q.Query{
|
||||
Keywords: map[string]interface{}{
|
||||
"RepositoryID": 1,
|
||||
"Tags": "invalid_value",
|
||||
"Tags": "latest",
|
||||
},
|
||||
})
|
||||
d.Require().NotNil(err)
|
||||
d.True(errors.IsErr(err, errors.BadRequestCode))
|
||||
d.Require().Nil(err)
|
||||
d.Equal(int64(1), total)
|
||||
|
||||
// query by repository ID and digest
|
||||
total, err := d.dao.Count(d.ctx, &q.Query{
|
||||
total, err = d.dao.Count(d.ctx, &q.Query{
|
||||
Keywords: map[string]interface{}{
|
||||
"RepositoryID": 1,
|
||||
"Digest": "parent_digest",
|
||||
|
@ -18,10 +18,12 @@
|
||||
<label class="clr-control-container" [class.clr-error]="isTagNameExist || name.hasError('pattern')">
|
||||
<input clrInput type="text" id="name" name="name" required size="20" autocomplete="off"
|
||||
[(ngModel)]="newTagName.name" #name="ngModel" pattern="^[\w][\w.-]{0,127}$" (keyup)="existValid(newTagName.name)">
|
||||
<clr-control-error class="position-ab" *ngIf="isTagNameExist">
|
||||
<span class="spinner spinner-inline spinner-tag" [hidden]="!tagNameCheckOnGoing"></span>
|
||||
|
||||
<clr-control-error class="position-ab white-space-nowrap" *ngIf="isTagNameExist">
|
||||
{{'TAG.NAME_ALREADY_EXISTS' | translate }}
|
||||
</clr-control-error>
|
||||
<clr-control-error class="position-ab" *ngIf="name.hasError('pattern')">
|
||||
<clr-control-error class="position-ab white-space-nowrap" *ngIf="name.hasError('pattern')">
|
||||
{{'RETAG.TIP_TAG' | translate }}
|
||||
</clr-control-error>
|
||||
</label>
|
||||
@ -31,7 +33,7 @@
|
||||
'BUTTON.CANCEL' | translate }}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-sm btn-primary" (click)="saveAddTag()"
|
||||
[disabled]="isTagNameExist || !newTagName.name ||tagForm.invalid">{{
|
||||
[disabled]="isTagNameExist || tagNameCheckOnGoing || !newTagName.name ||tagForm.invalid">{{
|
||||
'BUTTON.OK' | translate }}
|
||||
</button>
|
||||
</label>
|
||||
|
@ -4,6 +4,7 @@
|
||||
}
|
||||
.clr-control-container {
|
||||
margin-bottom: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
.btn.remove-btn {
|
||||
border: none;
|
||||
@ -25,9 +26,17 @@
|
||||
.position-ab {
|
||||
position: absolute;
|
||||
}
|
||||
.white-space-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.refresh-btn {
|
||||
float: right;
|
||||
margin-top: 15px;
|
||||
margin-right: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.spinner-tag {
|
||||
position: absolute;
|
||||
right: -.7rem;
|
||||
top: 1.2rem;
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ import { ArtifactService } from '../../../../../../ng-swagger-gen/services/artif
|
||||
import { OperationService } from "../../../../../lib/components/operation/operation.service";
|
||||
import { CURRENT_BASE_HREF } from "../../../../../lib/utils/utils";
|
||||
import { USERSTATICPERMISSION, UserPermissionService, UserPermissionDefaultService } from '../../../../../lib/services';
|
||||
import { TagService } from '../../../../../../ng-swagger-gen/services/tag.service';
|
||||
import { delay } from 'rxjs/operators';
|
||||
|
||||
|
||||
@ -21,13 +20,11 @@ describe('ArtifactTagComponent', () => {
|
||||
const mockErrorHandler = {
|
||||
error: () => {}
|
||||
};
|
||||
const mockTagService = {
|
||||
listTagsResponse: () => of({headers: null, body: []}).pipe(delay(0)),
|
||||
listTags: () => of([]),
|
||||
};
|
||||
const mockArtifactService = {
|
||||
createTag: () => of([]),
|
||||
deleteTag: () => of(null),
|
||||
listTagsResponse: () => of([]).pipe(delay(0))
|
||||
|
||||
};
|
||||
const config: IServiceConfig = {
|
||||
repositoryBaseEndpoint: CURRENT_BASE_HREF + "/repositories/testing"
|
||||
@ -54,7 +51,6 @@ describe('ArtifactTagComponent', () => {
|
||||
{ provide: SERVICE_CONFIG, useValue: config },
|
||||
{ provide: mockErrorHandler, useValue: ErrorHandler },
|
||||
{ provide: ArtifactService, useValue: mockArtifactService },
|
||||
{ provide: TagService, useValue: mockTagService },
|
||||
{ provide: UserPermissionService, useClass: UserPermissionDefaultService },
|
||||
{ provide: OperationService },
|
||||
]
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Component, OnInit, Input, ViewChild, Output, EventEmitter } from '@angular/core';
|
||||
import { Observable, of, forkJoin } from 'rxjs';
|
||||
import { map, catchError, finalize } from 'rxjs/operators';
|
||||
import { Component, OnInit, Input, ViewChild, OnDestroy } from '@angular/core';
|
||||
import { Observable, of, forkJoin, Subject, Subscription } from 'rxjs';
|
||||
import { map, catchError, finalize, debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { NgForm } from '@angular/forms';
|
||||
import { AVAILABLE_TIME } from "../../artifact-list-page/artifact-list/artifact-list-tab/artifact-list-tab.component";
|
||||
@ -16,7 +16,6 @@ import { operateChanges, OperateInfo, OperationState } from "../../../../../lib/
|
||||
import { errorHandler } from "../../../../../lib/utils/shared/shared.utils";
|
||||
import { ArtifactFront as Artifact } from "../artifact";
|
||||
import { ArtifactService } from '../../../../../../ng-swagger-gen/services/artifact.service';
|
||||
import { TagService } from '../../../../../../ng-swagger-gen/services/tag.service';
|
||||
import { Tag } from '../../../../../../ng-swagger-gen/models/tag';
|
||||
import {
|
||||
UserPermissionService, USERSTATICPERMISSION
|
||||
@ -33,7 +32,7 @@ class InitTag {
|
||||
styleUrls: ['./artifact-tag.component.scss']
|
||||
})
|
||||
|
||||
export class ArtifactTagComponent implements OnInit {
|
||||
export class ArtifactTagComponent implements OnInit, OnDestroy {
|
||||
@Input() artifactDetails: Artifact;
|
||||
@Input() projectName: string;
|
||||
@Input() projectId: number;
|
||||
@ -57,10 +56,12 @@ export class ArtifactTagComponent implements OnInit {
|
||||
currentTags: Tag[] = [];
|
||||
pageSize: number = DEFAULT_PAGE_SIZE;
|
||||
currentPage = 1;
|
||||
tagNameChecker: Subject<string> = new Subject<string>();
|
||||
tagNameCheckSub: Subscription;
|
||||
tagNameCheckOnGoing = false;
|
||||
constructor(
|
||||
private operationService: OperationService,
|
||||
private artifactService: ArtifactService,
|
||||
private tagService: TagService,
|
||||
private translateService: TranslateService,
|
||||
private userPermissionService: UserPermissionService,
|
||||
private errorHandlerService: ErrorHandler
|
||||
@ -68,7 +69,39 @@ export class ArtifactTagComponent implements OnInit {
|
||||
) { }
|
||||
ngOnInit() {
|
||||
this.getImagePermissionRule(this.projectId);
|
||||
this.getAllTags();
|
||||
this.invalidCreateTag();
|
||||
}
|
||||
checkTagName(name) {
|
||||
let listArtifactParams: ArtifactService.ListArtifactsParams = {
|
||||
projectName: this.projectName,
|
||||
repositoryName: this.repositoryName,
|
||||
withLabel: true,
|
||||
withScanOverview: true,
|
||||
withTag: true,
|
||||
q: encodeURIComponent(`tags=${name}`)
|
||||
};
|
||||
return this.artifactService.listArtifacts(listArtifactParams)
|
||||
.pipe(finalize(() => this.tagNameCheckOnGoing = false));
|
||||
}
|
||||
invalidCreateTag() {
|
||||
if (!this.tagNameCheckSub) {
|
||||
this.tagNameCheckSub = this.tagNameChecker
|
||||
.pipe(debounceTime(200))
|
||||
.pipe(distinctUntilChanged())
|
||||
.pipe(switchMap(name => {
|
||||
this.tagNameCheckOnGoing = true;
|
||||
this.isTagNameExist = false;
|
||||
return this.checkTagName(name);
|
||||
}))
|
||||
.subscribe(response => {
|
||||
// tag existing
|
||||
if (response && response.length) {
|
||||
this.isTagNameExist = true;
|
||||
}
|
||||
}, error => {
|
||||
this.errorHandlerService.error(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
getCurrentArtifactTags(state: ClrDatagridStateInterface) {
|
||||
if (!state || !state.page) {
|
||||
@ -76,16 +109,16 @@ export class ArtifactTagComponent implements OnInit {
|
||||
}
|
||||
let pageNumber: number = calculatePage(state);
|
||||
if (pageNumber <= 0) { pageNumber = 1; }
|
||||
let params: TagService.ListTagsParams = {
|
||||
let params: ArtifactService.ListTagsParams = {
|
||||
projectName: this.projectName,
|
||||
repositoryName: this.repositoryName,
|
||||
reference: this.artifactDetails.digest,
|
||||
page: pageNumber,
|
||||
withSignature: true,
|
||||
withImmutableStatus: true,
|
||||
pageSize: this.pageSize,
|
||||
q: encodeURIComponent(`artifact_id=${this.artifactDetails.id}`)
|
||||
pageSize: this.pageSize
|
||||
};
|
||||
this.tagService.listTagsResponse(params).pipe(finalize(() => {
|
||||
this.artifactService.listTagsResponse(params).pipe(finalize(() => {
|
||||
this.loading = false;
|
||||
})).subscribe(res => {
|
||||
if (res.headers) {
|
||||
@ -99,17 +132,6 @@ export class ArtifactTagComponent implements OnInit {
|
||||
this.errorHandlerService.error(error);
|
||||
});
|
||||
}
|
||||
getAllTags() {
|
||||
let params: TagService.ListTagsParams = {
|
||||
projectName: this.projectName,
|
||||
repositoryName: this.repositoryName
|
||||
};
|
||||
this.tagService.listTags(params).subscribe(res => {
|
||||
this.allTags = res;
|
||||
}, error => {
|
||||
this.errorHandlerService.error(error);
|
||||
});
|
||||
}
|
||||
getImagePermissionRule(projectId: number): void {
|
||||
const permissions = [
|
||||
{ resource: USERSTATICPERMISSION.REPOSITORY_TAG.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE },
|
||||
@ -142,7 +164,6 @@ export class ArtifactTagComponent implements OnInit {
|
||||
this.artifactService.createTag(createTagParams).subscribe(res => {
|
||||
this.newTagformShow = false;
|
||||
this.newTagName = new InitTag();
|
||||
this.getAllTags();
|
||||
this.currentPage = 1;
|
||||
let st: ClrDatagridStateInterface = { page: {from: 0, to: this.pageSize - 1, size: this.pageSize} };
|
||||
this.getCurrentArtifactTags(st);
|
||||
@ -249,15 +270,9 @@ export class ArtifactTagComponent implements OnInit {
|
||||
}
|
||||
|
||||
existValid(name) {
|
||||
this.isTagNameExist = false;
|
||||
if (this.allTags) {
|
||||
this.allTags.forEach(tag => {
|
||||
if (tag.name === name) {
|
||||
this.isTagNameExist = true;
|
||||
}
|
||||
});
|
||||
if (name) {
|
||||
this.tagNameChecker.next(name);
|
||||
}
|
||||
|
||||
}
|
||||
toggleTagListOpenOrClose() {
|
||||
this.openTag = !this.openTag;
|
||||
@ -272,4 +287,7 @@ export class ArtifactTagComponent implements OnInit {
|
||||
let st: ClrDatagridStateInterface = { page: {from: 0, to: this.pageSize - 1, size: this.pageSize} };
|
||||
this.getCurrentArtifactTags(st);
|
||||
}
|
||||
ngOnDestroy(): void {
|
||||
this.tagNameCheckSub.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
@ -147,7 +147,6 @@ func (a *artifactAPI) DeleteArtifact(ctx context.Context, params operation.Delet
|
||||
return operation.NewDeleteArtifactOK()
|
||||
}
|
||||
|
||||
// TODO immutable, quota, readonly middlewares should cover this API
|
||||
func (a *artifactAPI) CopyArtifact(ctx context.Context, params operation.CopyArtifactParams) middleware.Responder {
|
||||
if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionCreate, rbac.ResourceArtifact); err != nil {
|
||||
return a.SendError(ctx, err)
|
||||
@ -224,7 +223,7 @@ func (a *artifactAPI) CreateTag(ctx context.Context, params operation.CreateTagP
|
||||
AttachedArtifact: &art.Artifact,
|
||||
})
|
||||
|
||||
// TODO as we provide no API for get the single tag, ignore setting the location header here
|
||||
// as we provide no API for get the single tag, ignore setting the location header here
|
||||
return operation.NewCreateTagCreated()
|
||||
}
|
||||
|
||||
@ -266,6 +265,52 @@ func (a *artifactAPI) DeleteTag(ctx context.Context, params operation.DeleteTagP
|
||||
return operation.NewDeleteTagOK()
|
||||
}
|
||||
|
||||
func (a *artifactAPI) ListTags(ctx context.Context, params operation.ListTagsParams) middleware.Responder {
|
||||
if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionList, rbac.ResourceTag); err != nil {
|
||||
return a.SendError(ctx, err)
|
||||
}
|
||||
// set query
|
||||
query, err := a.BuildQuery(ctx, params.Q, params.Page, params.PageSize)
|
||||
if err != nil {
|
||||
return a.SendError(ctx, err)
|
||||
}
|
||||
|
||||
artifact, err := a.artCtl.GetByReference(ctx, fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName), params.Reference, nil)
|
||||
if err != nil {
|
||||
return a.SendError(ctx, err)
|
||||
}
|
||||
query.Keywords["ArtifactID"] = artifact.ID
|
||||
|
||||
// get the total count of tags
|
||||
total, err := a.tagCtl.Count(ctx, query)
|
||||
if err != nil {
|
||||
return a.SendError(ctx, err)
|
||||
}
|
||||
|
||||
// set option
|
||||
option := &tag.Option{}
|
||||
if params.WithSignature != nil {
|
||||
option.WithSignature = *params.WithSignature
|
||||
}
|
||||
if params.WithImmutableStatus != nil {
|
||||
option.WithImmutableStatus = *params.WithImmutableStatus
|
||||
}
|
||||
// list tags according to the query and option
|
||||
tags, err := a.tagCtl.List(ctx, query, option)
|
||||
if err != nil {
|
||||
return a.SendError(ctx, err)
|
||||
}
|
||||
|
||||
var ts []*models.Tag
|
||||
for _, tag := range tags {
|
||||
ts = append(ts, tag.ToSwagger())
|
||||
}
|
||||
return operation.NewListTagsOK().
|
||||
WithXTotalCount(total).
|
||||
WithLink(a.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String()).
|
||||
WithPayload(ts)
|
||||
}
|
||||
|
||||
func (a *artifactAPI) GetAddition(ctx context.Context, params operation.GetAdditionParams) middleware.Responder {
|
||||
if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionRead, rbac.ResourceArtifactAddition); err != nil {
|
||||
return a.SendError(ctx, err)
|
||||
|
@ -34,7 +34,6 @@ func New() http.Handler {
|
||||
AuditlogAPI: newAuditLogAPI(),
|
||||
ScanAPI: newScanAPI(),
|
||||
ProjectAPI: newProjectAPI(),
|
||||
TagAPI: newTagAPI(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
@ -1,93 +0,0 @@
|
||||
// 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 handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/controller/repository"
|
||||
"github.com/goharbor/harbor/src/controller/tag"
|
||||
"github.com/goharbor/harbor/src/server/v2.0/models"
|
||||
operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/tag"
|
||||
)
|
||||
|
||||
func newTagAPI() *tagAPI {
|
||||
return &tagAPI{
|
||||
repoCtl: repository.Ctl,
|
||||
tagCtl: tag.Ctl,
|
||||
}
|
||||
}
|
||||
|
||||
type tagAPI struct {
|
||||
BaseAPI
|
||||
repoCtl repository.Controller
|
||||
tagCtl tag.Controller
|
||||
}
|
||||
|
||||
func (t *tagAPI) Prepare(ctx context.Context, operation string, params interface{}) middleware.Responder {
|
||||
if err := unescapePathParams(params, "RepositoryName"); err != nil {
|
||||
t.SendError(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *tagAPI) ListTags(ctx context.Context, params operation.ListTagsParams) middleware.Responder {
|
||||
if err := t.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionList, rbac.ResourceTag); err != nil {
|
||||
return t.SendError(ctx, err)
|
||||
}
|
||||
// set query
|
||||
query, err := t.BuildQuery(ctx, params.Q, params.Page, params.PageSize)
|
||||
if err != nil {
|
||||
return t.SendError(ctx, err)
|
||||
}
|
||||
|
||||
repository, err := t.repoCtl.GetByName(ctx, fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName))
|
||||
if err != nil {
|
||||
return t.SendError(ctx, err)
|
||||
}
|
||||
query.Keywords["RepositoryID"] = repository.RepositoryID
|
||||
|
||||
// get the total count of tags
|
||||
total, err := t.tagCtl.Count(ctx, query)
|
||||
if err != nil {
|
||||
return t.SendError(ctx, err)
|
||||
}
|
||||
|
||||
// set option
|
||||
option := &tag.Option{}
|
||||
if params.WithSignature != nil {
|
||||
option.WithSignature = *params.WithSignature
|
||||
}
|
||||
if params.WithImmutableStatus != nil {
|
||||
option.WithImmutableStatus = *params.WithImmutableStatus
|
||||
}
|
||||
// list tags according to the query and option
|
||||
tags, err := t.tagCtl.List(ctx, query, option)
|
||||
if err != nil {
|
||||
return t.SendError(ctx, err)
|
||||
}
|
||||
|
||||
var ts []*models.Tag
|
||||
for _, tag := range tags {
|
||||
ts = append(ts, tag.ToSwagger())
|
||||
}
|
||||
return operation.NewListTagsOK().
|
||||
WithXTotalCount(total).
|
||||
WithLink(t.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String()).
|
||||
WithPayload(ts)
|
||||
}
|
Loading…
Reference in New Issue
Block a user