Merge latest updates.

This commit is contained in:
kunw 2017-06-02 16:53:03 +08:00
commit 4246f55180
14 changed files with 73 additions and 241 deletions

View File

@ -20,7 +20,6 @@ import (
"strconv" "strconv"
"github.com/vmware/harbor/src/common" "github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/api"
"github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/config" "github.com/vmware/harbor/src/ui/config"
@ -91,20 +90,19 @@ var (
// ConfigAPI ... // ConfigAPI ...
type ConfigAPI struct { type ConfigAPI struct {
api.BaseAPI BaseController
} }
// Prepare validates the user // Prepare validates the user
func (c *ConfigAPI) Prepare() { func (c *ConfigAPI) Prepare() {
userID := c.ValidateUser() c.BaseController.Prepare()
isSysAdmin, err := dao.IsAdminRole(userID) if !c.SecurityCtx.IsAuthenticated() {
if err != nil { c.HandleUnauthorized()
log.Errorf("failed to check the role of user: %v", err) return
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
} }
if !c.SecurityCtx.IsSysAdmin() {
if !isSysAdmin { c.HandleForbidden(c.SecurityCtx.GetUsername())
c.CustomAbort(http.StatusForbidden, http.StatusText(http.StatusForbidden)) return
} }
} }

View File

@ -20,8 +20,6 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/vmware/harbor/src/common/api"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/models"
ldapUtils "github.com/vmware/harbor/src/common/utils/ldap" ldapUtils "github.com/vmware/harbor/src/common/utils/ldap"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
@ -29,25 +27,22 @@ import (
// LdapAPI handles requesst to /api/ldap/ping /api/ldap/user/search /api/ldap/user/import // LdapAPI handles requesst to /api/ldap/ping /api/ldap/user/search /api/ldap/user/import
type LdapAPI struct { type LdapAPI struct {
api.BaseAPI BaseController
} }
const metaChars = "&|!=~*<>()" const metaChars = "&|!=~*<>()"
// Prepare ... // Prepare ...
func (l *LdapAPI) Prepare() { func (l *LdapAPI) Prepare() {
l.BaseController.Prepare()
userID := l.ValidateUser() if !l.SecurityCtx.IsAuthenticated() {
isSysAdmin, err := dao.IsAdminRole(userID) l.HandleUnauthorized()
if err != nil { return
log.Errorf("error occurred in IsAdminRole: %v", err)
l.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
} }
if !l.SecurityCtx.IsSysAdmin() {
if !isSysAdmin { l.HandleForbidden(l.SecurityCtx.GetUsername())
l.CustomAbort(http.StatusForbidden, http.StatusText(http.StatusForbidden)) return
} }
} }
// Ping ... // Ping ...

View File

@ -300,8 +300,7 @@ export const EN_US_LANG: any = {
"FAILED_TO_DELETE_TARGET_IN_USED": "Failed to delete the endpoint in use." "FAILED_TO_DELETE_TARGET_IN_USED": "Failed to delete the endpoint in use."
}, },
"REPOSITORY": { "REPOSITORY": {
"COPY_ID": "Copy ID", "COPY_DIGEST_ID": "Copy Digest ID",
"COPY_PARENT_ID": "Copy Parent ID",
"DELETE": "Delete", "DELETE": "Delete",
"NAME": "Name", "NAME": "Name",
"TAGS_COUNT": "Tags", "TAGS_COUNT": "Tags",

View File

@ -300,8 +300,7 @@ export const ZH_CN_LANG: any = {
"FAILED_TO_DELETE_TARGET_IN_USED": "无法删除正在使用的目标。" "FAILED_TO_DELETE_TARGET_IN_USED": "无法删除正在使用的目标。"
}, },
"REPOSITORY": { "REPOSITORY": {
"COPY_ID": "复制ID", "COPY_DIGEST_ID": "复制摘要ID",
"COPY_PARENT_ID": "复制父级ID",
"DELETE": "删除", "DELETE": "删除",
"NAME": "名称", "NAME": "名称",
"TAGS_COUNT": "标签数", "TAGS_COUNT": "标签数",

View File

@ -4,13 +4,14 @@ export const TAG_TEMPLATE = `
<h3 class="modal-title">{{ manifestInfoTitle | translate }}</h3> <h3 class="modal-title">{{ manifestInfoTitle | translate }}</h3>
<div class="modal-body"> <div class="modal-body">
<div class="row col-md-12"> <div class="row col-md-12">
<textarea rows="3" (click)="selectAndCopy($event)">{{tagID}}</textarea> <textarea rows="3" (click)="selectAndCopy($event)">{{digestId}}</textarea>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-primary" (click)="showTagManifestOpened = false">{{'BUTTON.OK' | translate}}</button> <button type="button" class="btn btn-primary" (click)="showTagManifestOpened = false">{{'BUTTON.OK' | translate}}</button>
</div> </div>
</clr-modal> </clr-modal>
<h2 *ngIf="!isEmbeded" class="sub-header-title">{{repoName}}</h2> <h2 *ngIf="!isEmbeded" class="sub-header-title">{{repoName}}</h2>
<clr-datagrid [clrDgLoading]="loading" [class.embeded-datagrid]="isEmbeded"> <clr-datagrid [clrDgLoading]="loading" [class.embeded-datagrid]="isEmbeded">
<clr-dg-column [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column> <clr-dg-column [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
@ -23,8 +24,7 @@ export const TAG_TEMPLATE = `
<clr-dg-column [clrDgField]="'os'">{{'REPOSITORY.OS' | translate}}</clr-dg-column> <clr-dg-column [clrDgField]="'os'">{{'REPOSITORY.OS' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'> <clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
<clr-dg-action-overflow> <clr-dg-action-overflow>
<button class="action-item" (click)="showTagID('tag', t)">{{'REPOSITORY.COPY_ID' | translate}}</button> <button class="action-item" (click)="showDigestId(t)">{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
<button class="action-item" (click)="showTagID('parent', t)">{{'REPOSITORY.COPY_PARENT_ID' | translate}}</button>
<button class="action-item" [hidden]="!hasProjectAdminRole" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button> <button class="action-item" [hidden]="!hasProjectAdminRole" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button>
</clr-dg-action-overflow> </clr-dg-action-overflow>
<clr-dg-cell>{{t.name}}</clr-dg-cell> <clr-dg-cell>{{t.name}}</clr-dg-cell>

View File

@ -57,7 +57,7 @@ export class TagComponent implements OnInit {
showTagManifestOpened: boolean; showTagManifestOpened: boolean;
manifestInfoTitle: string; manifestInfoTitle: string;
tagID: string; digestId: string;
staticBackdrop: boolean = true; staticBackdrop: boolean = true;
closable: boolean = false; closable: boolean = false;
@ -76,24 +76,23 @@ export class TagComponent implements OnInit {
confirmDeletion(message: ConfirmationAcknowledgement) { confirmDeletion(message: ConfirmationAcknowledgement) {
if (message && if (message &&
message.source === ConfirmationTargets.TAG message.source === ConfirmationTargets.TAG
&& message.state === ConfirmationState.CONFIRMED) { && message.state === ConfirmationState.CONFIRMED) {
let tag: Tag = message.data; let tag: Tag = message.data;
if (tag) { if (tag) {
if (tag.signature) { if (tag.signature) {
return; return;
} else { } else {
let tagName = tag.name; toPromise<number>(this.tagService
toPromise<number>(this.tagService .deleteTag(this.repoName, tag.name))
.deleteTag(this.repoName, tagName)) .then(
.then( response => {
response => { this.retrieve();
this.retrieve(); this.translateService.get('REPOSITORY.DELETED_TAG_SUCCESS')
this.translateService.get('REPOSITORY.DELETED_TAG_SUCCESS') .subscribe(res=>this.errorHandler.info(res));
.subscribe(res=>this.errorHandler.info(res)); }).catch(error => this.errorHandler.error(error));
}).catch(error => this.errorHandler.error(error));
}
} }
}
} }
} }
@ -163,15 +162,10 @@ export class TagComponent implements OnInit {
} }
} }
showTagID(type: string, tag: Tag) { showDigestId(tag: Tag) {
if(tag) { if(tag) {
if(type === 'tag') { this.manifestInfoTitle = 'REPOSITORY.COPY_DIGEST_ID';
this.manifestInfoTitle = 'REPOSITORY.COPY_ID'; this.digestId = tag.digest;
this.tagID = tag.digest;
} else if(type === 'parent') {
this.manifestInfoTitle = 'REPOSITORY.COPY_PARENT_ID';
this.tagID = tag.digest;
}
this.showTagManifestOpened = true; this.showTagManifestOpened = true;
} }
} }

View File

@ -16,7 +16,6 @@ import { Http, URLSearchParams, Response } from '@angular/http';
import { Repository } from './repository'; import { Repository } from './repository';
import { Tag } from './tag'; import { Tag } from './tag';
import { VerifiedSignature } from './verified-signature';
import { Observable } from 'rxjs/Observable' import { Observable } from 'rxjs/Observable'
import 'rxjs/add/observable/of'; import 'rxjs/add/observable/of';
@ -46,35 +45,6 @@ export class RepositoryService {
.catch(error=>Observable.throw(error)); .catch(error=>Observable.throw(error));
} }
listNotarySignatures(repoName: string): Observable<VerifiedSignature[]> {
return this.http
.get(`/api/repositories/${repoName}/signatures`)
.map(response=>response.json())
.catch(error=>Observable.throw(error));
}
listTagsWithVerifiedSignatures(repoName: string): Observable<Tag[]> {
return this.listTags(repoName)
.map(res=>res)
.flatMap(tags=>{
return this.listNotarySignatures(repoName).map(signatures=>{
tags.forEach(t=>{
for(let i = 0; i < signatures.length; i++) {
if(signatures[i].tag === t.tag) {
t.signed = 1;
break;
}
}
});
return tags;
})
.catch(error=>{
return Observable.of(tags);
})
})
.catch(error=>Observable.throw(error));
}
deleteRepository(repoName: string): Observable<any> { deleteRepository(repoName: string): Observable<any> {
return this.http return this.http
.delete(`/api/repositories/${repoName}`) .delete(`/api/repositories/${repoName}`)

View File

@ -6,7 +6,7 @@
<h3 class="modal-title">{{ manifestInfoTitle | translate }}</h3> <h3 class="modal-title">{{ manifestInfoTitle | translate }}</h3>
<div class="modal-body"> <div class="modal-body">
<div class="row col-md-12"> <div class="row col-md-12">
<textarea rows="3" (click)="selectAndCopy($event)">{{tagID}}</textarea> <textarea rows="3" (click)="selectAndCopy($event)">{{digestId}}</textarea>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@ -24,25 +24,20 @@
<clr-dg-column>{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column> <clr-dg-column>{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.ARCHITECTURE' | translate}}</clr-dg-column> <clr-dg-column>{{'REPOSITORY.ARCHITECTURE' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.OS' | translate}}</clr-dg-column> <clr-dg-column>{{'REPOSITORY.OS' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'> <clr-dg-row *clrDgItems="let t of tags">
<clr-dg-action-overflow> <clr-dg-action-overflow>
<button class="action-item" (click)="showTagID('tag', t)">{{'REPOSITORY.COPY_ID' | translate}}</button> <button class="action-item" (click)="showDigestId(t)">{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
<button class="action-item" (click)="showTagID('parent', t)">{{'REPOSITORY.COPY_PARENT_ID' | translate}}</button>
<button class="action-item" [hidden]="!hasProjectAdminRole" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button> <button class="action-item" [hidden]="!hasProjectAdminRole" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button>
</clr-dg-action-overflow> </clr-dg-action-overflow>
<clr-dg-cell>{{t.tag}}</clr-dg-cell> <clr-dg-cell>{{t.name}}</clr-dg-cell>
<clr-dg-cell>{{t.pullCommand}}</clr-dg-cell> <clr-dg-cell>docker pull {{registryUrl}}/{{repoName}}:{{t.name}}</clr-dg-cell>
<clr-dg-cell *ngIf="withNotary" [ngSwitch]="t.signed"> <clr-dg-cell *ngIf="withNotary">
<clr-icon shape="check" *ngSwitchCase="1" style="color: #1D5100;"></clr-icon> <clr-icon *ngIf="t.signature" shape="check" style="color: #1D5100;"></clr-icon>
<clr-icon shape="close" *ngSwitchCase="0" style="color: #C92100;"></clr-icon> <clr-icon *ngIf="!t.signature" shape="close" style="color: #C92100;"></clr-icon>
<a href="javascript:void(0)" *ngSwitchDefault role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="help" style="color: #565656;" size="16"></clr-icon>
<span class="tooltip-content">{{'REPOSITORY.NOTARY_IS_UNDETERMINED' | translate}}</span>
</a>
</clr-dg-cell> </clr-dg-cell>
<clr-dg-cell>{{t.author}}</clr-dg-cell> <clr-dg-cell>{{t.author}}</clr-dg-cell>
<clr-dg-cell>{{t.created | date: 'short'}}</clr-dg-cell> <clr-dg-cell>{{t.created | date: 'short'}}</clr-dg-cell>
<clr-dg-cell>{{t.dockerVersion}}</clr-dg-cell> <clr-dg-cell>{{t.docker_version}}</clr-dg-cell>
<clr-dg-cell>{{t.architecture}}</clr-dg-cell> <clr-dg-cell>{{t.architecture}}</clr-dg-cell>
<clr-dg-cell>{{t.os}}</clr-dg-cell> <clr-dg-cell>{{t.os}}</clr-dg-cell>
</clr-dg-row> </clr-dg-row>

View File

@ -24,7 +24,6 @@ import { ConfirmationMessage } from '../../shared/confirmation-dialog/confirmati
import { Subscription } from 'rxjs/Subscription'; import { Subscription } from 'rxjs/Subscription';
import { Tag } from '../tag'; import { Tag } from '../tag';
import { TagView } from '../tag-view';
import { AppConfigService } from '../../app-config.service'; import { AppConfigService } from '../../app-config.service';
@ -45,7 +44,7 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
hasProjectAdminRole: boolean = false; hasProjectAdminRole: boolean = false;
tags: TagView[]; tags: Tag[];
registryUrl: string; registryUrl: string;
withNotary: boolean; withNotary: boolean;
@ -53,7 +52,7 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
showTagManifestOpened: boolean; showTagManifestOpened: boolean;
manifestInfoTitle: string; manifestInfoTitle: string;
tagID: string; digestId: string;
staticBackdrop: boolean = true; staticBackdrop: boolean = true;
closable: boolean = false; closable: boolean = false;
@ -79,9 +78,8 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
if (tag.signed) { if (tag.signed) {
return; return;
} else { } else {
let tagName = tag.tag;
this.repositoryService this.repositoryService
.deleteRepoByTag(this.repoName, tagName) .deleteRepoByTag(this.repoName, tag.name)
.subscribe( .subscribe(
response => { response => {
this.retrieve(); this.retrieve();
@ -103,10 +101,11 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
} }
this.projectId = this.route.snapshot.params['id']; this.projectId = this.route.snapshot.params['id'];
this.repoName = this.route.snapshot.params['repo']; this.repoName = this.route.snapshot.params['repo'];
this.tags = [];
this.registryUrl = this.appConfigService.getConfig().registry_url; this.registryUrl = this.appConfigService.getConfig().registry_url;
this.withNotary = this.appConfigService.getConfig().with_notary; this.withNotary = this.appConfigService.getConfig().with_notary;
this.retrieve(); this.retrieve();
} }
ngOnDestroy() { ngOnDestroy() {
@ -120,63 +119,25 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
this.repositoryService this.repositoryService
.listTags(this.repoName) .listTags(this.repoName)
.subscribe( .subscribe(
items => this.listTags(items), tags => this.tags = tags,
error => this.messageHandlerService.handleError(error)); error => this.messageHandlerService.handleError(error));
if(this.withNotary) {
this.repositoryService
.listNotarySignatures(this.repoName)
.subscribe(
signatures => {
this.tags.forEach((t, n)=>{
let signed = false;
for(let i = 0; i < signatures.length; i++) {
if (signatures[i].tag === t.tag) {
signed = true;
break;
}
}
this.tags[n].signed = (signed) ? 1 : 0;
this.ref.markForCheck();
});
},
error => console.error('Cannot determine the signature of this tag.'));
}
}
listTags(tags: Tag[]): void {
tags.forEach(t => {
let tag = new TagView();
tag.tag = t.tag;
let data = JSON.parse(t.manifest.history[0].v1Compatibility);
tag.architecture = data['architecture'];
tag.author = data['author'];
tag.signed = t.signed;
tag.created = data['created'];
tag.dockerVersion = data['docker_version'];
tag.pullCommand = 'docker pull ' + this.registryUrl + '/' + t.manifest.name + ':' + t.tag;
tag.os = data['os'];
tag.id = data['id'];
tag.parent = data['parent'];
this.tags.push(tag);
});
let hnd = setInterval(()=>this.ref.markForCheck(), 100); let hnd = setInterval(()=>this.ref.markForCheck(), 100);
setTimeout(()=>clearInterval(hnd), 1000); setTimeout(()=>clearInterval(hnd), 1000);
} }
deleteTag(tag: TagView) { deleteTag(tag: Tag) {
if (tag) { if (tag) {
let titleKey: string, summaryKey: string, content: string, buttons: ConfirmationButtons; let titleKey: string, summaryKey: string, content: string, buttons: ConfirmationButtons;
if (tag.signed) { if (tag.signature) {
titleKey = 'REPOSITORY.DELETION_TITLE_TAG_DENIED'; titleKey = 'REPOSITORY.DELETION_TITLE_TAG_DENIED';
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG_DENIED'; summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG_DENIED';
buttons = ConfirmationButtons.CLOSE; buttons = ConfirmationButtons.CLOSE;
content = 'notary -s https://' + this.registryUrl + ':4443 -d ~/.docker/trust remove -p ' + this.registryUrl + '/' + this.repoName + ' ' + tag.tag; content = 'notary -s https://' + this.registryUrl + ':4443 -d ~/.docker/trust remove -p ' + this.registryUrl + '/' + this.repoName + ' ' + tag.name;
} else { } else {
titleKey = 'REPOSITORY.DELETION_TITLE_TAG'; titleKey = 'REPOSITORY.DELETION_TITLE_TAG';
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG'; summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG';
buttons = ConfirmationButtons.DELETE_CANCEL; buttons = ConfirmationButtons.DELETE_CANCEL;
content = tag.tag; content = tag.name;
} }
let message = new ConfirmationMessage( let message = new ConfirmationMessage(
titleKey, titleKey,
@ -189,15 +150,10 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
} }
} }
showTagID(type: string, tag: TagView) { showDigestId(tag: Tag) {
if(tag) { if(tag) {
if(type === 'tag') { this.manifestInfoTitle = 'REPOSITORY.COPY_DIGEST_ID';
this.manifestInfoTitle = 'REPOSITORY.COPY_ID'; this.digestId = tag.digest;
this.tagID = tag.id;
} else if(type === 'parent') {
this.manifestInfoTitle = 'REPOSITORY.COPY_PARENT_ID';
this.tagID = tag.parent;
}
this.showTagManifestOpened = true; this.showTagManifestOpened = true;
} }
} }

View File

@ -1,25 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// 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.
export class TagView {
tag: string;
pullCommand: string;
signed: number = -1;
author: string;
created: Date;
dockerVersion: string;
architecture: string;
os: string;
id: string;
parent: string;
}

View File

@ -11,30 +11,13 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
/*
{
"tag": "latest",
"manifest": {
"schemaVersion": 1,
"name": "library/photon",
"tag": "latest",
"architecture": "amd64",
"history": []
},
*/
export class Tag { export class Tag {
tag: string; digest: string;
manifest: { name: string;
schemaVersion: number; architecture: string;
name: string; os: string;
tag: string; docker_version: string;
architecture: string; author: string;
history: [ created: Date;
{ signature?: {[key: string]: any | any[]}
v1Compatibility: string;
}
];
};
signed: number;
} }

View File

@ -1,30 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// 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.
/*
[
{
"tag": "2.0",
"hashes": {
"sha256": "E1lggRW5RZnlZBY4usWu8d36p5u5YFfr9B68jTOs+Kc="
}
}
]
*/
export class VerifiedSignature {
tag: string;
hashes: {
sha256: string;
}
}

View File

@ -300,8 +300,7 @@
"FAILED_TO_DELETE_TARGET_IN_USED": "Failed to delete the endpoint in use." "FAILED_TO_DELETE_TARGET_IN_USED": "Failed to delete the endpoint in use."
}, },
"REPOSITORY": { "REPOSITORY": {
"COPY_ID": "Copy ID", "COPY_DIGEST_ID": "Copy Digest ID",
"COPY_PARENT_ID": "Copy Parent ID",
"DELETE": "Delete", "DELETE": "Delete",
"NAME": "Name", "NAME": "Name",
"TAGS_COUNT": "Tags", "TAGS_COUNT": "Tags",

View File

@ -300,8 +300,7 @@
"FAILED_TO_DELETE_TARGET_IN_USED": "无法删除正在使用的目标。" "FAILED_TO_DELETE_TARGET_IN_USED": "无法删除正在使用的目标。"
}, },
"REPOSITORY": { "REPOSITORY": {
"COPY_ID": "复制ID", "COPY_DIGEST_ID": "复制摘要ID",
"COPY_PARENT_ID": "复制父级ID",
"DELETE": "删除", "DELETE": "删除",
"NAME": "名称", "NAME": "名称",
"TAGS_COUNT": "标签数", "TAGS_COUNT": "标签数",