mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-23 10:45:45 +01:00
Merge latest updates.
This commit is contained in:
commit
4246f55180
@ -20,7 +20,6 @@ import (
|
||||
"strconv"
|
||||
|
||||
"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/utils/log"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
@ -91,20 +90,19 @@ var (
|
||||
|
||||
// ConfigAPI ...
|
||||
type ConfigAPI struct {
|
||||
api.BaseAPI
|
||||
BaseController
|
||||
}
|
||||
|
||||
// Prepare validates the user
|
||||
func (c *ConfigAPI) Prepare() {
|
||||
userID := c.ValidateUser()
|
||||
isSysAdmin, err := dao.IsAdminRole(userID)
|
||||
if err != nil {
|
||||
log.Errorf("failed to check the role of user: %v", err)
|
||||
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
c.BaseController.Prepare()
|
||||
if !c.SecurityCtx.IsAuthenticated() {
|
||||
c.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
|
||||
if !isSysAdmin {
|
||||
c.CustomAbort(http.StatusForbidden, http.StatusText(http.StatusForbidden))
|
||||
if !c.SecurityCtx.IsSysAdmin() {
|
||||
c.HandleForbidden(c.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,8 +20,6 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/vmware/harbor/src/common/api"
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
ldapUtils "github.com/vmware/harbor/src/common/utils/ldap"
|
||||
"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
|
||||
type LdapAPI struct {
|
||||
api.BaseAPI
|
||||
BaseController
|
||||
}
|
||||
|
||||
const metaChars = "&|!=~*<>()"
|
||||
|
||||
// Prepare ...
|
||||
func (l *LdapAPI) Prepare() {
|
||||
|
||||
userID := l.ValidateUser()
|
||||
isSysAdmin, err := dao.IsAdminRole(userID)
|
||||
if err != nil {
|
||||
log.Errorf("error occurred in IsAdminRole: %v", err)
|
||||
l.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
l.BaseController.Prepare()
|
||||
if !l.SecurityCtx.IsAuthenticated() {
|
||||
l.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
|
||||
if !isSysAdmin {
|
||||
l.CustomAbort(http.StatusForbidden, http.StatusText(http.StatusForbidden))
|
||||
if !l.SecurityCtx.IsSysAdmin() {
|
||||
l.HandleForbidden(l.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Ping ...
|
||||
|
@ -300,8 +300,7 @@ export const EN_US_LANG: any = {
|
||||
"FAILED_TO_DELETE_TARGET_IN_USED": "Failed to delete the endpoint in use."
|
||||
},
|
||||
"REPOSITORY": {
|
||||
"COPY_ID": "Copy ID",
|
||||
"COPY_PARENT_ID": "Copy Parent ID",
|
||||
"COPY_DIGEST_ID": "Copy Digest ID",
|
||||
"DELETE": "Delete",
|
||||
"NAME": "Name",
|
||||
"TAGS_COUNT": "Tags",
|
||||
|
@ -300,8 +300,7 @@ export const ZH_CN_LANG: any = {
|
||||
"FAILED_TO_DELETE_TARGET_IN_USED": "无法删除正在使用的目标。"
|
||||
},
|
||||
"REPOSITORY": {
|
||||
"COPY_ID": "复制ID",
|
||||
"COPY_PARENT_ID": "复制父级ID",
|
||||
"COPY_DIGEST_ID": "复制摘要ID",
|
||||
"DELETE": "删除",
|
||||
"NAME": "名称",
|
||||
"TAGS_COUNT": "标签数",
|
||||
|
@ -4,13 +4,14 @@ export const TAG_TEMPLATE = `
|
||||
<h3 class="modal-title">{{ manifestInfoTitle | translate }}</h3>
|
||||
<div class="modal-body">
|
||||
<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 class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" (click)="showTagManifestOpened = false">{{'BUTTON.OK' | translate}}</button>
|
||||
</div>
|
||||
</clr-modal>
|
||||
|
||||
<h2 *ngIf="!isEmbeded" class="sub-header-title">{{repoName}}</h2>
|
||||
<clr-datagrid [clrDgLoading]="loading" [class.embeded-datagrid]="isEmbeded">
|
||||
<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-row *clrDgItems="let t of tags" [clrDgItem]='t'>
|
||||
<clr-dg-action-overflow>
|
||||
<button class="action-item" (click)="showTagID('tag', t)">{{'REPOSITORY.COPY_ID' | translate}}</button>
|
||||
<button class="action-item" (click)="showTagID('parent', t)">{{'REPOSITORY.COPY_PARENT_ID' | translate}}</button>
|
||||
<button class="action-item" (click)="showDigestId(t)">{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
|
||||
<button class="action-item" [hidden]="!hasProjectAdminRole" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button>
|
||||
</clr-dg-action-overflow>
|
||||
<clr-dg-cell>{{t.name}}</clr-dg-cell>
|
||||
|
@ -57,7 +57,7 @@ export class TagComponent implements OnInit {
|
||||
|
||||
showTagManifestOpened: boolean;
|
||||
manifestInfoTitle: string;
|
||||
tagID: string;
|
||||
digestId: string;
|
||||
staticBackdrop: boolean = true;
|
||||
closable: boolean = false;
|
||||
|
||||
@ -76,24 +76,23 @@ export class TagComponent implements OnInit {
|
||||
|
||||
confirmDeletion(message: ConfirmationAcknowledgement) {
|
||||
if (message &&
|
||||
message.source === ConfirmationTargets.TAG
|
||||
&& message.state === ConfirmationState.CONFIRMED) {
|
||||
let tag: Tag = message.data;
|
||||
if (tag) {
|
||||
if (tag.signature) {
|
||||
return;
|
||||
} else {
|
||||
let tagName = tag.name;
|
||||
toPromise<number>(this.tagService
|
||||
.deleteTag(this.repoName, tagName))
|
||||
.then(
|
||||
response => {
|
||||
this.retrieve();
|
||||
this.translateService.get('REPOSITORY.DELETED_TAG_SUCCESS')
|
||||
.subscribe(res=>this.errorHandler.info(res));
|
||||
}).catch(error => this.errorHandler.error(error));
|
||||
}
|
||||
message.source === ConfirmationTargets.TAG
|
||||
&& message.state === ConfirmationState.CONFIRMED) {
|
||||
let tag: Tag = message.data;
|
||||
if (tag) {
|
||||
if (tag.signature) {
|
||||
return;
|
||||
} else {
|
||||
toPromise<number>(this.tagService
|
||||
.deleteTag(this.repoName, tag.name))
|
||||
.then(
|
||||
response => {
|
||||
this.retrieve();
|
||||
this.translateService.get('REPOSITORY.DELETED_TAG_SUCCESS')
|
||||
.subscribe(res=>this.errorHandler.info(res));
|
||||
}).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(type === 'tag') {
|
||||
this.manifestInfoTitle = 'REPOSITORY.COPY_ID';
|
||||
this.tagID = tag.digest;
|
||||
} else if(type === 'parent') {
|
||||
this.manifestInfoTitle = 'REPOSITORY.COPY_PARENT_ID';
|
||||
this.tagID = tag.digest;
|
||||
}
|
||||
this.manifestInfoTitle = 'REPOSITORY.COPY_DIGEST_ID';
|
||||
this.digestId = tag.digest;
|
||||
this.showTagManifestOpened = true;
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ import { Http, URLSearchParams, Response } from '@angular/http';
|
||||
|
||||
import { Repository } from './repository';
|
||||
import { Tag } from './tag';
|
||||
import { VerifiedSignature } from './verified-signature';
|
||||
|
||||
import { Observable } from 'rxjs/Observable'
|
||||
import 'rxjs/add/observable/of';
|
||||
@ -46,35 +45,6 @@ export class RepositoryService {
|
||||
.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> {
|
||||
return this.http
|
||||
.delete(`/api/repositories/${repoName}`)
|
||||
|
@ -6,7 +6,7 @@
|
||||
<h3 class="modal-title">{{ manifestInfoTitle | translate }}</h3>
|
||||
<div class="modal-body">
|
||||
<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 class="modal-footer">
|
||||
@ -24,25 +24,20 @@
|
||||
<clr-dg-column>{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPOSITORY.ARCHITECTURE' | 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>
|
||||
<button class="action-item" (click)="showTagID('tag', t)">{{'REPOSITORY.COPY_ID' | translate}}</button>
|
||||
<button class="action-item" (click)="showTagID('parent', t)">{{'REPOSITORY.COPY_PARENT_ID' | translate}}</button>
|
||||
<button class="action-item" (click)="showDigestId(t)">{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
|
||||
<button class="action-item" [hidden]="!hasProjectAdminRole" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button>
|
||||
</clr-dg-action-overflow>
|
||||
<clr-dg-cell>{{t.tag}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.pullCommand}}</clr-dg-cell>
|
||||
<clr-dg-cell *ngIf="withNotary" [ngSwitch]="t.signed">
|
||||
<clr-icon shape="check" *ngSwitchCase="1" style="color: #1D5100;"></clr-icon>
|
||||
<clr-icon shape="close" *ngSwitchCase="0" 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>{{t.name}}</clr-dg-cell>
|
||||
<clr-dg-cell>docker pull {{registryUrl}}/{{repoName}}:{{t.name}}</clr-dg-cell>
|
||||
<clr-dg-cell *ngIf="withNotary">
|
||||
<clr-icon *ngIf="t.signature" shape="check" style="color: #1D5100;"></clr-icon>
|
||||
<clr-icon *ngIf="!t.signature" shape="close" style="color: #C92100;"></clr-icon>
|
||||
</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.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.os}}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
|
@ -24,7 +24,6 @@ import { ConfirmationMessage } from '../../shared/confirmation-dialog/confirmati
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
|
||||
import { Tag } from '../tag';
|
||||
import { TagView } from '../tag-view';
|
||||
|
||||
import { AppConfigService } from '../../app-config.service';
|
||||
|
||||
@ -45,7 +44,7 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
|
||||
|
||||
hasProjectAdminRole: boolean = false;
|
||||
|
||||
tags: TagView[];
|
||||
tags: Tag[];
|
||||
registryUrl: string;
|
||||
withNotary: boolean;
|
||||
|
||||
@ -53,7 +52,7 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
|
||||
|
||||
showTagManifestOpened: boolean;
|
||||
manifestInfoTitle: string;
|
||||
tagID: string;
|
||||
digestId: string;
|
||||
staticBackdrop: boolean = true;
|
||||
closable: boolean = false;
|
||||
|
||||
@ -79,9 +78,8 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
|
||||
if (tag.signed) {
|
||||
return;
|
||||
} else {
|
||||
let tagName = tag.tag;
|
||||
this.repositoryService
|
||||
.deleteRepoByTag(this.repoName, tagName)
|
||||
.deleteRepoByTag(this.repoName, tag.name)
|
||||
.subscribe(
|
||||
response => {
|
||||
this.retrieve();
|
||||
@ -103,10 +101,11 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
this.projectId = this.route.snapshot.params['id'];
|
||||
this.repoName = this.route.snapshot.params['repo'];
|
||||
this.tags = [];
|
||||
|
||||
this.registryUrl = this.appConfigService.getConfig().registry_url;
|
||||
this.withNotary = this.appConfigService.getConfig().with_notary;
|
||||
this.retrieve();
|
||||
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@ -120,63 +119,25 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
|
||||
this.repositoryService
|
||||
.listTags(this.repoName)
|
||||
.subscribe(
|
||||
items => this.listTags(items),
|
||||
tags => this.tags = tags,
|
||||
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);
|
||||
setTimeout(()=>clearInterval(hnd), 1000);
|
||||
}
|
||||
|
||||
deleteTag(tag: TagView) {
|
||||
deleteTag(tag: Tag) {
|
||||
if (tag) {
|
||||
let titleKey: string, summaryKey: string, content: string, buttons: ConfirmationButtons;
|
||||
if (tag.signed) {
|
||||
if (tag.signature) {
|
||||
titleKey = 'REPOSITORY.DELETION_TITLE_TAG_DENIED';
|
||||
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG_DENIED';
|
||||
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 {
|
||||
titleKey = 'REPOSITORY.DELETION_TITLE_TAG';
|
||||
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG';
|
||||
buttons = ConfirmationButtons.DELETE_CANCEL;
|
||||
content = tag.tag;
|
||||
content = tag.name;
|
||||
}
|
||||
let message = new ConfirmationMessage(
|
||||
titleKey,
|
||||
@ -189,15 +150,10 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
showTagID(type: string, tag: TagView) {
|
||||
showDigestId(tag: Tag) {
|
||||
if(tag) {
|
||||
if(type === 'tag') {
|
||||
this.manifestInfoTitle = 'REPOSITORY.COPY_ID';
|
||||
this.tagID = tag.id;
|
||||
} else if(type === 'parent') {
|
||||
this.manifestInfoTitle = 'REPOSITORY.COPY_PARENT_ID';
|
||||
this.tagID = tag.parent;
|
||||
}
|
||||
this.manifestInfoTitle = 'REPOSITORY.COPY_DIGEST_ID';
|
||||
this.digestId = tag.digest;
|
||||
this.showTagManifestOpened = true;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -11,30 +11,13 @@
|
||||
// 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": "latest",
|
||||
"manifest": {
|
||||
"schemaVersion": 1,
|
||||
"name": "library/photon",
|
||||
"tag": "latest",
|
||||
"architecture": "amd64",
|
||||
"history": []
|
||||
},
|
||||
|
||||
*/
|
||||
export class Tag {
|
||||
tag: string;
|
||||
manifest: {
|
||||
schemaVersion: number;
|
||||
name: string;
|
||||
tag: string;
|
||||
architecture: string;
|
||||
history: [
|
||||
{
|
||||
v1Compatibility: string;
|
||||
}
|
||||
];
|
||||
};
|
||||
signed: number;
|
||||
digest: string;
|
||||
name: string;
|
||||
architecture: string;
|
||||
os: string;
|
||||
docker_version: string;
|
||||
author: string;
|
||||
created: Date;
|
||||
signature?: {[key: string]: any | any[]}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -300,8 +300,7 @@
|
||||
"FAILED_TO_DELETE_TARGET_IN_USED": "Failed to delete the endpoint in use."
|
||||
},
|
||||
"REPOSITORY": {
|
||||
"COPY_ID": "Copy ID",
|
||||
"COPY_PARENT_ID": "Copy Parent ID",
|
||||
"COPY_DIGEST_ID": "Copy Digest ID",
|
||||
"DELETE": "Delete",
|
||||
"NAME": "Name",
|
||||
"TAGS_COUNT": "Tags",
|
||||
|
@ -300,8 +300,7 @@
|
||||
"FAILED_TO_DELETE_TARGET_IN_USED": "无法删除正在使用的目标。"
|
||||
},
|
||||
"REPOSITORY": {
|
||||
"COPY_ID": "复制ID",
|
||||
"COPY_PARENT_ID": "复制父级ID",
|
||||
"COPY_DIGEST_ID": "复制摘要ID",
|
||||
"DELETE": "删除",
|
||||
"NAME": "名称",
|
||||
"TAGS_COUNT": "标签数",
|
||||
|
Loading…
Reference in New Issue
Block a user