mirror of
https://github.com/goharbor/harbor
synced 2025-04-09 19:03:41 +00:00
Merge branch 'master' into job_service
This commit is contained in:
commit
44808650be
|
@ -85,7 +85,7 @@ create table project_member (
|
|||
id int not null AUTO_INCREMENT,
|
||||
project_id int NOT NULL,
|
||||
entity_id int NOT NULL,
|
||||
entity_type char NOT NULL, ## u for user, g for user group
|
||||
entity_type char(1) NOT NULL, ## u for user, g for user group
|
||||
role int NOT NULL,
|
||||
creation_time timestamp default CURRENT_TIMESTAMP,
|
||||
update_time timestamp default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
|
||||
|
@ -218,7 +218,7 @@ create table img_scan_job (
|
|||
id int NOT NULL AUTO_INCREMENT,
|
||||
status varchar(64) NOT NULL,
|
||||
repository varchar(256) NOT NULL,
|
||||
tag varchar(128) NOT NULL,
|
||||
tag varchar(128) NOT NULL,
|
||||
digest varchar(128),
|
||||
#New job service only records uuid, for compatibility in this table both IDs are stored.
|
||||
job_uuid varchar(64),
|
||||
|
|
|
@ -20,4 +20,5 @@ export const CREATE_EDIT_LABEL_STYLE: string = `
|
|||
height:22px;
|
||||
min-width: 26px;}
|
||||
.dropdown-item{border:0px; color: white; font-size:12px;}
|
||||
.dropdown .dropdown-toggle.btn{padding:0 !important;}
|
||||
`;
|
|
@ -68,7 +68,8 @@ export class CreateEditLabelComponent implements OnInit, OnDestroy {
|
|||
ngOnInit(): void {
|
||||
this.nameChecker.debounceTime(500).subscribe((name: string) => {
|
||||
this.checkOnGoing = true;
|
||||
toPromise<Label[]>(this.labelService.getLabels(this.scope, this.projectId, name))
|
||||
let labelName = this.currentForm.controls['name'].value;
|
||||
toPromise<Label[]>(this.labelService.getLabels(this.scope, this.projectId, labelName))
|
||||
.then(targets => {
|
||||
if (targets && targets.length) {
|
||||
this.isLabelNameExist = true;
|
||||
|
|
|
@ -19,7 +19,7 @@ h4{
|
|||
}
|
||||
.colorRed{color: red;}
|
||||
.colorRed a{text-decoration: underline;color: #007CBB;}
|
||||
.alertLabel{display:block; margin-top:0; line-height:1em; font-size:12px;}
|
||||
.alertLabel{display:block; margin-top:2px; line-height:1em; font-size:12px;}
|
||||
|
||||
.inputWidth{width: 270px;}
|
||||
.endpointSelect{ width: 270px; margin-right: 20px;}
|
||||
|
|
|
@ -157,7 +157,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
|
|||
// if input value exit in project list
|
||||
let pro = res.find((data: any) => data.name === name);
|
||||
if (!pro) {
|
||||
this.noProjectInfo = 'REPLICATION.PROJECT_NOT_EXIST_INFO';
|
||||
this.noProjectInfo = 'REPLICATION.NO_PROJECT_INFO';
|
||||
this.noSelectedProject = true;
|
||||
} else {
|
||||
this.noProjectInfo = '';
|
||||
|
@ -165,12 +165,12 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
|
|||
this.setProject([pro])
|
||||
}
|
||||
} else {
|
||||
this.noProjectInfo = 'REPLICATION.PROJECT_NOT_EXIST_INFO';
|
||||
this.noProjectInfo = 'REPLICATION.NO_PROJECT_INFO';
|
||||
this.noSelectedProject = true;
|
||||
}
|
||||
}).catch((error: any) => {
|
||||
this.errorHandler.error(error);
|
||||
this.noProjectInfo = 'REPLICATION.PROJECT_NOT_EXIST_INFO';
|
||||
this.noProjectInfo = 'REPLICATION.NO_PROJECT_INFO';
|
||||
this.noSelectedProject = true;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -30,6 +30,7 @@ import {Label} from "../service/interface";
|
|||
|
||||
export class LabelPieceComponent implements OnInit {
|
||||
@Input() label: Label;
|
||||
@Input() labelWidth: number;
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
*/
|
||||
|
||||
export const LABEL_PIEICE_TEMPLATE: string = `
|
||||
<label class="label" [ngStyle]="{'background-color': label.color}">
|
||||
<label class="label" [ngStyle]="{'background-color': label.color}" [style.max-width.px]="labelWidth">
|
||||
<clr-icon *ngIf="label.scope=='p'" shape="organization"></clr-icon>
|
||||
<clr-icon *ngIf="label.scope=='g'" shape="administrator"></clr-icon>
|
||||
{{label.name}}
|
||||
|
@ -11,7 +11,12 @@ export const LABEL_PIEICE_TEMPLATE: string = `
|
|||
`;
|
||||
|
||||
export const LABEL_PIEICE_STYLES: string = `
|
||||
.label{border: none; color:#222;padding-top:2px;}
|
||||
.label clr-icon{ margin-right: 3px; display:block;}
|
||||
.label{border: none; color:#222;
|
||||
display: inline-block;
|
||||
justify-content: flex-start;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: .875rem;}
|
||||
.label clr-icon{ margin-right: 3px;}
|
||||
.btn-group .dropdown-menu clr-icon{display:block;}
|
||||
`;
|
|
@ -112,8 +112,8 @@ export class LabelComponent implements OnInit {
|
|||
this.batchDelectionInfos.push(initBatchMessage);
|
||||
});
|
||||
let deletionMessage = new ConfirmationMessage(
|
||||
'REPLICATION.DELETION_TITLE_TARGET',
|
||||
'REPLICATION.DELETION_SUMMARY_TARGET',
|
||||
'LABEL.DELETION_TITLE_TARGET',
|
||||
'LABEL.DELETION_SUMMARY_TARGET',
|
||||
targetNames.join(', ') || '',
|
||||
targets,
|
||||
ConfirmationTargets.TARGET,
|
||||
|
|
|
@ -176,6 +176,13 @@ export class RepositoryListviewComponent implements OnChanges, OnInit {
|
|||
});
|
||||
return;
|
||||
}
|
||||
if (error.status === 503) {
|
||||
Observable.forkJoin(this.translateService.get('BATCH.DELETED_FAILURE'),
|
||||
this.translateService.get('REPOSITORY.TAGS_NO_DELETE')).subscribe(res => {
|
||||
findedList = BathInfoChanges(findedList, res[0], false, true, res[1]);
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.translateService.get('BATCH.DELETED_FAILURE').subscribe(res => {
|
||||
findedList = BathInfoChanges(findedList, res, false, true);
|
||||
});
|
||||
|
|
|
@ -30,7 +30,7 @@ export const TAG_DETAIL_HTML: string = `
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div *ngIf="withClair">
|
||||
<div class="vulnerability">
|
||||
<hbr-vulnerability-bar [repoName]="repositoryId" [tagId]="tagDetails.name" [summary]="tagDetails.scan_overview"></hbr-vulnerability-bar>
|
||||
</div>
|
||||
|
|
|
@ -148,18 +148,4 @@ describe('TagDetailComponent (inline template)', () => {
|
|||
});
|
||||
}));
|
||||
|
||||
it('should display vulnerability details', async(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
let el: HTMLElement = fixture.nativeElement.querySelector('.second-column');
|
||||
expect(el).toBeTruthy();
|
||||
let el2: HTMLElement = el.querySelector('div');
|
||||
expect(el2).toBeTruthy();
|
||||
expect(el2.textContent.trim()).toEqual("13 VULNERABILITY.SEVERITY.HIGHTAG.LEVEL_VULNERABILITIES");
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
|
@ -25,6 +25,7 @@ export class TagDetailComponent implements OnInit {
|
|||
@Input() tagId: string;
|
||||
@Input() repositoryId: string;
|
||||
@Input() withAdmiral: boolean;
|
||||
@Input() withClair: boolean;
|
||||
tagDetails: Tag = {
|
||||
name: "--",
|
||||
size: "--",
|
||||
|
|
|
@ -67,10 +67,6 @@ export const TAG_STYLE = `
|
|||
:host >>> .signpost-content-header{display:none;}
|
||||
.filterLabelPiece{position: absolute; bottom :0px;z-index:1;}
|
||||
.dropdown .dropdown-toggle.btn {
|
||||
padding-right: 1rem;
|
||||
border-left-width: 0;
|
||||
border-right-width: 0;
|
||||
border-radius: 0;
|
||||
margin-top: -2px;
|
||||
margin: .25rem .5rem .25rem 0;
|
||||
}
|
||||
`;
|
|
@ -15,7 +15,7 @@ export const TAG_TEMPLATE = `
|
|||
<div class="row" style="position:relative;">
|
||||
<div>
|
||||
<div class="row flex-items-xs-right rightPos">
|
||||
<div class='filterLabelPiece' [style.left.px]='filterLabelPieceWidth' ><hbr-label-piece [hidden]='!filterOneLabel' [label]="filterOneLabel"></hbr-label-piece></div>
|
||||
<div class='filterLabelPiece' [style.left.px]='filterLabelPieceWidth' ><hbr-label-piece [hidden]='!filterOneLabel' [label]="filterOneLabel" [labelWidth]="130"></hbr-label-piece></div>
|
||||
<div class="flex-xs-middle">
|
||||
<hbr-filter *ngIf="withAdmiral" [withDivider]="true" filterPlaceholder="{{'TAG.FILTER_FOR_TAGS' | translate}}" (filter)="doSearchTagNames($event)" [currentValue]="lastFilteredTagName"></hbr-filter>
|
||||
<clr-dropdown *ngIf="!withAdmiral">
|
||||
|
@ -23,12 +23,12 @@ export const TAG_TEMPLATE = `
|
|||
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
|
||||
<div style='display:grid'>
|
||||
<label class="dropdown-header">{{'REPOSITORY.FILTER_BY_LABEL' | translate}}</label>
|
||||
<div class="form-group"><input type="text" placeholder="Filter labels" #labelNamePiece (keyup)="handleInputFilter(labelNamePiece.value)"></div>
|
||||
<div [hidden]='imageFilterLabels.length'>{{'LABEL.NO_LABELS' | translate }}</div>
|
||||
<div class="form-group"><input type="text" placeholder="Filter labels" [(ngModel)]= "filterName" (keyup)="handleInputFilter()"></div>
|
||||
<div [hidden]='imageFilterLabels.length' style="padding-left:10px;">{{'LABEL.NO_LABELS' | translate }}</div>
|
||||
<div [hidden]='!imageFilterLabels.length' style='max-height:300px;overflow-y: auto;'>
|
||||
<button type="button" class="dropdown-item" *ngFor='let label of imageFilterLabels' (click)="label.iconsShow = true; filterLabel(label)">
|
||||
<clr-icon shape="check" class='pull-left' [hidden]='!label.iconsShow'></clr-icon>
|
||||
<div class='labelDiv'><hbr-label-piece [label]="label.label"></hbr-label-piece></div>
|
||||
<div class='labelDiv'><hbr-label-piece [label]="label.label" [labelWidth]="130"></hbr-label-piece></div>
|
||||
<clr-icon shape="times-circle" class='pull-right' [hidden]='!label.iconsShow' (click)="$event.stopPropagation(); label.iconsShow = false; unFilterLabel(label)"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -44,23 +44,23 @@ export const TAG_TEMPLATE = `
|
|||
<clr-dg-action-bar>
|
||||
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(canScanNow(selectedRow) && selectedRow.length==1)" (click)="scanNow(selectedRow)"><clr-icon shape="shield-check" size="16"></clr-icon> {{'VULNERABILITY.SCAN_NOW' | translate}}</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(selectedRow.length==1)" (click)="showDigestId(selectedRow)" ><clr-icon shape="copy" size="16"></clr-icon> {{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
|
||||
<clr-dropdown *ngIf="!withAdmiral" class="btn btn-sm btn-secondary">
|
||||
<button type="button" class="btn btn-sm btn-secondary" clrDropdownTrigger [disabled]="!(selectedRow.length==1) || isGuest" (click)="addLabels(selectedRow)" >{{'REPOSITORY.ADD_LABELS' | translate}}</button>
|
||||
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
|
||||
<div style='display:grid'>
|
||||
<label class="dropdown-header">{{'REPOSITORY.ADD_TO_IMAGE' | translate}}</label>
|
||||
<div class="form-group"><input type="text" placeholder="Filter labels" #stickLabelNamePiece (keyup)="handleStickInputFilter(stickLabelNamePiece.value)"></div>
|
||||
<div [hidden]='imageStickLabels.length'>{{'LABEL.NO_LABELS' | translate }}</div>
|
||||
<div [hidden]='!imageStickLabels.length' style='max-height:300px;overflow-y: auto;'>
|
||||
<button type="button" class="dropdown-item" *ngFor='let label of imageStickLabels' (click)="selectLabel(label); label.iconsShow = true">
|
||||
<clr-icon shape="check" class='pull-left' [hidden]='!label.iconsShow'></clr-icon>
|
||||
<div class='labelDiv'><hbr-label-piece [label]="label.label"></hbr-label-piece></div>
|
||||
<clr-icon shape="times-circle" class='pull-right' [hidden]='!label.iconsShow' (click)="$event.stopPropagation(); unSelectLabel(label); label.iconsShow = false"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</clr-dropdown-menu>
|
||||
</clr-dropdown>
|
||||
<clr-dropdown *ngIf="!withAdmiral">
|
||||
<button type="button" class="btn btn-sm btn-secondary" clrDropdownTrigger [disabled]="!(selectedRow.length==1) || isGuest" (click)="addLabels(selectedRow)" ><clr-icon shape="plus" size="16"></clr-icon>{{'REPOSITORY.ADD_LABELS' | translate}}</button>
|
||||
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
|
||||
<div style='display:grid'>
|
||||
<label class="dropdown-header">{{'REPOSITORY.ADD_TO_IMAGE' | translate}}</label>
|
||||
<div class="form-group"><input type="text" placeholder="Filter labels" [(ngModel)]="stickName" (keyup)="handleStickInputFilter()"></div>
|
||||
<div [hidden]='imageStickLabels.length' style="padding-left:10px;">{{'LABEL.NO_LABELS' | translate }}</div>
|
||||
<div [hidden]='!imageStickLabels.length' style='max-height:300px;overflow-y: auto;'>
|
||||
<button type="button" class="dropdown-item" *ngFor='let label of imageStickLabels' (click)="selectLabel(label); label.iconsShow = true">
|
||||
<clr-icon shape="check" class='pull-left' [hidden]='!label.iconsShow'></clr-icon>
|
||||
<div class='labelDiv'><hbr-label-piece [label]="label.label" [labelWidth]="130"></hbr-label-piece></div>
|
||||
<clr-icon shape="times-circle" class='pull-right' [hidden]='!label.iconsShow' (click)="$event.stopPropagation(); unSelectLabel(label); label.iconsShow = false"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</clr-dropdown-menu>
|
||||
</clr-dropdown>
|
||||
<button type="button" class="btn btn-sm btn-secondary" *ngIf="hasProjectAdminRole" (click)="deleteTags(selectedRow)" [disabled]="!selectedRow.length"><clr-icon shape="times" size="16"></clr-icon> {{'REPOSITORY.DELETE' | translate}}</button>
|
||||
</clr-dg-action-bar>
|
||||
<clr-dg-column style="width: 120px;" [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
|
||||
|
@ -74,9 +74,8 @@ export const TAG_TEMPLATE = `
|
|||
<clr-dg-column *ngIf="!withAdmiral" style="width: 140px;" [clrDgField]="'labels'">{{'REPOSITORY.LABELS' | translate}}</clr-dg-column>
|
||||
<clr-dg-placeholder>{{'TAG.PLACEHOLDER' | translate }}</clr-dg-placeholder>
|
||||
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
|
||||
<clr-dg-cell class="truncated" style="width: 120px;" [ngSwitch]="withClair">
|
||||
<a *ngSwitchCase="true" href="javascript:void(0)" (click)="onTagClick(t)" title="{{t.name}}">{{t.name}}</a>
|
||||
<span *ngSwitchDefault>{{t.name}}</span>
|
||||
<clr-dg-cell class="truncated" style="width: 120px;">
|
||||
<a href="javascript:void(0)" (click)="onTagClick(t)" title="{{t.name}}">{{t.name}}</a>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell style="width: 90px;">{{sizeTransform(t.size)}}</clr-dg-cell>
|
||||
<clr-dg-cell style="min-width: 100px; max-width:220px;" class="truncated" title="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}">
|
||||
|
@ -97,7 +96,7 @@ export const TAG_TEMPLATE = `
|
|||
<clr-dg-cell style="width: 160px;">{{t.created | date: 'short'}}</clr-dg-cell>
|
||||
<clr-dg-cell style="width: 80px;" *ngIf="!withClair">{{t.docker_version}}</clr-dg-cell>
|
||||
<clr-dg-cell *ngIf="!withAdmiral" style="width: 140px;">
|
||||
<hbr-label-piece *ngIf="t.labels?.length" [label]="t.labels[0]"></hbr-label-piece>
|
||||
<hbr-label-piece *ngIf="t.labels?.length" [label]="t.labels[0]" [labelWidth]="90"> </hbr-label-piece>
|
||||
<div class="signpost-item" [hidden]="t.labels?.length<=1">
|
||||
<div class="trigger-item">
|
||||
<clr-signpost>
|
||||
|
|
|
@ -111,7 +111,8 @@ export class TagComponent implements OnInit, AfterViewInit {
|
|||
labelNameFilter: Subject<string> = new Subject<string> ();
|
||||
stickLabelNameFilter: Subject<string> = new Subject<string> ();
|
||||
filterOnGoing: boolean;
|
||||
|
||||
stickName = ''
|
||||
filterName = '';
|
||||
initFilter = {
|
||||
name: '',
|
||||
description: '',
|
||||
|
@ -159,12 +160,12 @@ export class TagComponent implements OnInit, AfterViewInit {
|
|||
.debounceTime(500)
|
||||
.distinctUntilChanged()
|
||||
.subscribe((name: string) => {
|
||||
if (name && name.length) {
|
||||
if (this.filterName.length) {
|
||||
this.filterOnGoing = true;
|
||||
this.imageFilterLabels = [];
|
||||
|
||||
this.imageLabels.forEach(data => {
|
||||
if (data.label.name.indexOf(name) !== -1) {
|
||||
if (data.label.name.indexOf(this.filterName) !== -1) {
|
||||
this.imageFilterLabels.push(data);
|
||||
}
|
||||
})
|
||||
|
@ -178,12 +179,12 @@ export class TagComponent implements OnInit, AfterViewInit {
|
|||
.debounceTime(500)
|
||||
.distinctUntilChanged()
|
||||
.subscribe((name: string) => {
|
||||
if (name && name.length) {
|
||||
if (this.stickName.length) {
|
||||
this.filterOnGoing = true;
|
||||
this.imageStickLabels = [];
|
||||
|
||||
this.imageLabels.forEach(data => {
|
||||
if (data.label.name.indexOf(name) !== -1) {
|
||||
if (data.label.name.indexOf(this.stickName) !== -1) {
|
||||
this.imageStickLabels.push(data);
|
||||
}
|
||||
})
|
||||
|
@ -217,7 +218,13 @@ export class TagComponent implements OnInit, AfterViewInit {
|
|||
st.page.size = this.pageSize;
|
||||
st.page.from = 0;
|
||||
st.page.to = this.pageSize - 1;
|
||||
st.filters = [{property: "name", value: this.lastFilteredTagName}];
|
||||
let selectedLab = this.imageFilterLabels.find(label => label.iconsShow === true);
|
||||
if (selectedLab) {
|
||||
st.filters = [{property: 'name', value: this.lastFilteredTagName}, {property: 'labels.name', value: selectedLab.label}];
|
||||
}else {
|
||||
st.filters = [{property: 'name', value: this.lastFilteredTagName}];
|
||||
}
|
||||
|
||||
this.clrLoad(st);
|
||||
}
|
||||
|
||||
|
@ -379,17 +386,17 @@ export class TagComponent implements OnInit, AfterViewInit {
|
|||
}
|
||||
}
|
||||
|
||||
handleInputFilter($event: string) {
|
||||
if ($event && $event.length) {
|
||||
this.labelNameFilter.next($event);
|
||||
handleInputFilter() {
|
||||
if (this.filterName.length) {
|
||||
this.labelNameFilter.next(this.filterName);
|
||||
}else {
|
||||
this.imageFilterLabels = clone(this.imageLabels);
|
||||
}
|
||||
}
|
||||
|
||||
handleStickInputFilter($event: string) {
|
||||
if ($event && $event.length) {
|
||||
this.stickLabelNameFilter.next($event);
|
||||
handleStickInputFilter() {
|
||||
if (this.stickName.length) {
|
||||
this.stickLabelNameFilter.next(this.stickName);
|
||||
}else {
|
||||
this.imageStickLabels = clone(this.imageLabels);
|
||||
}
|
||||
|
@ -514,6 +521,13 @@ export class TagComponent implements OnInit, AfterViewInit {
|
|||
findedList = BathInfoChanges(findedList, res);
|
||||
});
|
||||
}).catch(error => {
|
||||
if (error.status === 503) {
|
||||
Observable.forkJoin(this.translateService.get('BATCH.DELETED_FAILURE'),
|
||||
this.translateService.get('REPOSITORY.TAGS_NO_DELETE')).subscribe(res => {
|
||||
findedList = BathInfoChanges(findedList, res[0], false, true, res[1]);
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.translateService.get("BATCH.DELETED_FAILURE").subscribe(res => {
|
||||
findedList = BathInfoChanges(findedList, res, false, true);
|
||||
});
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
"clarity-icons": "^0.10.17",
|
||||
"clarity-ui": "^0.10.27",
|
||||
"core-js": "^2.4.1",
|
||||
"harbor-ui": "0.6.61",
|
||||
"harbor-ui": "0.6.63",
|
||||
"intl": "^1.2.5",
|
||||
"mutationobserver-shim": "^0.3.2",
|
||||
"ngx-cookie": "^1.0.0",
|
||||
|
|
|
@ -27,6 +27,7 @@ export class AppConfig {
|
|||
clair_vulnerability_status?: ClairDBStatus;
|
||||
next_scan_all: number;
|
||||
registry_storage_provider_name: string;
|
||||
read_only: boolean;
|
||||
|
||||
constructor() {
|
||||
// Set default value
|
||||
|
@ -46,5 +47,6 @@ export class AppConfig {
|
|||
};
|
||||
this.next_scan_all = 0;
|
||||
this.registry_storage_provider_name = "";
|
||||
this.read_only = false;
|
||||
}
|
||||
}
|
|
@ -5,6 +5,9 @@
|
|||
.container-override {
|
||||
position: relative !important;
|
||||
}
|
||||
.content-container{
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.start-content-padding {
|
||||
padding: 0px !important;
|
||||
|
|
|
@ -69,12 +69,16 @@ export class NavigatorComponent implements OnInit {
|
|||
this.translate.onLangChange.subscribe((langChange: {lang: string}) => {
|
||||
this.selectedLang = langChange.lang;
|
||||
//Keep in cookie for next use
|
||||
let opt:CookieOptions = {path: '/', expires: new Date(Date.now() + 3600*1000*24*31)};
|
||||
let opt: CookieOptions = {path: '/', expires: new Date(Date.now() + 3600*1000*24*31)};
|
||||
this.cookie.put("harbor-lang", langChange.lang, opt);
|
||||
});
|
||||
if (this.appConfigService.isIntegrationMode()) {
|
||||
this.appTitle = 'APP_TITLE.VIC';
|
||||
}
|
||||
|
||||
if (this.appConfigService.getConfig().read_only) {
|
||||
this.msgHandler.handleReadOnly();
|
||||
}
|
||||
}
|
||||
|
||||
public get isSessionValid(): boolean {
|
||||
|
|
|
@ -18,6 +18,9 @@
|
|||
<li role="presentation" class="nav-item" *ngIf="withClair">
|
||||
<button id="config-vulnerability" class="btn btn-link nav-link" aria-controls="vulnerability" [class.active]='isCurrentTabLink("config-vulnerability")' type="button" (click)='tabLinkClick("config-vulnerability")'>{{'CONFIG.VULNERABILITY' | translate}}</button>
|
||||
</li>
|
||||
<li role="presentation" class="nav-item">
|
||||
<button id="config-repo" class="btn btn-link nav-link" aria-controls="repoReadOnly" [class.active]='isCurrentTabLink("config-repo")' type="button" (click)='tabLinkClick("config-repo")'>{{'CONFIG.REPO_READ_ONLY' | translate}}</button>
|
||||
</li>
|
||||
</ul>
|
||||
<section id="authentication" role="tabpanel" aria-labelledby="config-auth" [hidden]='!isCurrentTabContent("authentication")'>
|
||||
<config-auth [allConfig]="allConfig"></config-auth>
|
||||
|
@ -34,6 +37,9 @@
|
|||
<section id="vulnerability" *ngIf="withClair" role="tabpanel" aria-labelledby="config-vulnerability" [hidden]='!isCurrentTabContent("vulnerability")'>
|
||||
<vulnerability-config [(vulnerabilityConfig)]="allConfig"></vulnerability-config>
|
||||
</section>
|
||||
<section id="repoReadOnly" role="tabpanel" aria-labelledby="config-repo" [hidden]='!isCurrentTabContent("repoReadOnly")'>
|
||||
<repo-read-only [(repoConfig)]="allConfig"></repo-read-only>
|
||||
</section>
|
||||
<div>
|
||||
<button type="button" class="btn btn-primary" (click)="save()" [hidden]="hideBtn" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.SAVE' | translate}}</button>
|
||||
<button type="button" class="btn btn-outline" (click)="cancel()" [hidden]="hideBtn" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.CANCEL' | translate}}</button>
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
SystemSettingsComponent,
|
||||
VulnerabilityConfigComponent,
|
||||
} from 'harbor-ui';
|
||||
import {RepoReadOnlyComponent} from "./repo/repo-read-only.component";
|
||||
|
||||
const fakePass = 'aWpLOSYkIzJTTU4wMDkx';
|
||||
const TabLinkContentMap = {
|
||||
|
@ -39,7 +40,8 @@ const TabLinkContentMap = {
|
|||
'config-email': 'email',
|
||||
'config-system': 'system_settings',
|
||||
'config-vulnerability': 'vulnerability',
|
||||
'config-label': 'system_label'
|
||||
'config-label': 'system_label',
|
||||
'config-repo': 'repoReadOnly'
|
||||
};
|
||||
|
||||
@Component({
|
||||
|
@ -60,6 +62,7 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
|
|||
@ViewChild(VulnerabilityConfigComponent) vulnerabilityConfig: VulnerabilityConfigComponent;
|
||||
@ViewChild(ConfigurationEmailComponent) mailConfig: ConfigurationEmailComponent;
|
||||
@ViewChild(ConfigurationAuthComponent) authConfig: ConfigurationAuthComponent;
|
||||
@ViewChild(RepoReadOnlyComponent) repoConfig: RepoReadOnlyComponent;
|
||||
|
||||
constructor(
|
||||
private msgHandler: MessageHandlerService,
|
||||
|
@ -125,6 +128,9 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
|
|||
case 'config-vulnerability':
|
||||
properties = ['scan_all_policy'];
|
||||
break;
|
||||
case 'config-repo':
|
||||
properties = ['read_only'];
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
@ -262,6 +268,14 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
|
|||
// HERE we choose force way
|
||||
this.retrieveConfig();
|
||||
|
||||
if (changes['read_only']) {
|
||||
this.msgHandler.handleReadOnly();
|
||||
}
|
||||
|
||||
if (changes['read_only'].toString() === "false") {
|
||||
this.msgHandler.clear();
|
||||
}
|
||||
|
||||
// Reload bootstrap option
|
||||
this.appConfigService.load().catch(error => console.error('Failed to reload bootstrap option with error: ', error));
|
||||
|
||||
|
@ -273,7 +287,7 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
} else {
|
||||
// Inprop situation, should not come here
|
||||
console.error('Save obort becasue nothing changed');
|
||||
console.error('Save abort because nothing changed');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import { ConfigurationComponent } from './config.component';
|
|||
import { ConfigurationService } from './config.service';
|
||||
import { ConfigurationAuthComponent } from './auth/config-auth.component';
|
||||
import { ConfigurationEmailComponent } from './email/config-email.component';
|
||||
import {RepoReadOnlyComponent} from "./repo/repo-read-only.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@ -28,7 +29,9 @@ import { ConfigurationEmailComponent } from './email/config-email.component';
|
|||
declarations: [
|
||||
ConfigurationComponent,
|
||||
ConfigurationAuthComponent,
|
||||
ConfigurationEmailComponent],
|
||||
ConfigurationEmailComponent,
|
||||
RepoReadOnlyComponent
|
||||
],
|
||||
exports: [ConfigurationComponent],
|
||||
providers: [ConfigurationService]
|
||||
})
|
||||
|
|
25
src/ui_ng/src/app/config/repo/repo-read-only.component.ts
Normal file
25
src/ui_ng/src/app/config/repo/repo-read-only.component.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
|
||||
import {Component, Input, ViewChild} from "@angular/core";
|
||||
import {Configuration} from "harbor-ui";
|
||||
import {NgForm} from "@angular/forms";
|
||||
|
||||
@Component({
|
||||
selector: 'repo-read-only',
|
||||
templateUrl: 'repo-read-only.html',
|
||||
})
|
||||
export class RepoReadOnlyComponent {
|
||||
|
||||
@Input('repoConfig') currentConfig: Configuration = new Configuration();
|
||||
|
||||
@ViewChild('repoConfigFrom') repoForm: NgForm;
|
||||
|
||||
constructor() { }
|
||||
|
||||
disabled(prop: any) {
|
||||
return !(prop && prop.editable);
|
||||
}
|
||||
|
||||
setInsecureReadOnlyValue($event: any) {
|
||||
this.currentConfig.read_only.value = $event;
|
||||
}
|
||||
}
|
13
src/ui_ng/src/app/config/repo/repo-read-only.html
Normal file
13
src/ui_ng/src/app/config/repo/repo-read-only.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<form #repoConfigFrom="ngForm" class="form">
|
||||
<section class="form-block">
|
||||
<div class="form-group">
|
||||
<label for="repoReadOnly">{{'CONFIG.REPO_READ_ONLY' | translate}}</label>
|
||||
<clr-checkbox name="repoReadOnly" id="repoReadOnly" [clrChecked]="currentConfig.read_only.value" [clrDisabled]="disabled(currentConfig.read_only)" (clrCheckedChange)="setInsecureReadOnlyValue($event)">
|
||||
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right" style="top:-7px;">
|
||||
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
|
||||
<span class="tooltip-content">{{'CONFIG.REPO_TOOLTIP' | translate}}</span>
|
||||
</a>
|
||||
</clr-checkbox>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
|
@ -1,7 +1,7 @@
|
|||
.global-message-alert {
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
left: 0px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 999;
|
||||
}
|
||||
|
@ -11,4 +11,5 @@
|
|||
}
|
||||
:host >>> .alert-icon-wrapper{
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
:host >>> .alert.alert-app-level.alert-warning{padding: 0;}
|
|
@ -1,3 +1,3 @@
|
|||
<div>
|
||||
<hbr-tag-detail (backEvt)="goBack($event)" [tagId]="tagId" [withAdmiral]="withAdmiral" [repositoryId]="repositoryId"></hbr-tag-detail>
|
||||
<hbr-tag-detail (backEvt)="goBack($event)" [tagId]="tagId" [withClair]="withClair" [withAdmiral]="withAdmiral" [repositoryId]="repositoryId"></hbr-tag-detail>
|
||||
</div>
|
|
@ -41,6 +41,10 @@ export class TagDetailPageComponent implements OnInit {
|
|||
return this.appConfigService.getConfig().with_admiral;
|
||||
}
|
||||
|
||||
get withClair(): boolean {
|
||||
return this.appConfigService.getConfig().with_clair;
|
||||
}
|
||||
|
||||
goBack(tag: string): void {
|
||||
this.router.navigate(["harbor", "projects", this.projectId, "repositories", tag]);
|
||||
}
|
||||
|
|
|
@ -53,6 +53,10 @@ export class MessageHandlerService implements ErrorHandler{
|
|||
}
|
||||
}
|
||||
|
||||
public handleReadOnly(): void {
|
||||
this.msgService.announceAppLevelMessage(503, 'REPO_READ_ONLY', AlertType.WARNING);
|
||||
}
|
||||
|
||||
public showError(message: string, params: any): void {
|
||||
if (!params) {
|
||||
params = {};
|
||||
|
|
|
@ -48,6 +48,10 @@ export class AuthCheckGuard implements CanActivate, CanActivateChild {
|
|||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> | boolean {
|
||||
//When routing change, clear
|
||||
this.msgHandler.clear();
|
||||
if (this.appConfigService.getConfig().read_only.toString() === 'true') {
|
||||
this.msgHandler.handleReadOnly();
|
||||
}
|
||||
|
||||
this.searchTrigger.closeSearch(true);
|
||||
return new Promise((resolve, reject) => {
|
||||
//Before activating, we firstly need to confirm whether the route is coming from peer part - admiral
|
||||
|
|
|
@ -178,7 +178,7 @@
|
|||
"REPLICATION": "Replication",
|
||||
"USERS": "Members",
|
||||
"LOGS": "Logs",
|
||||
"LABELS": "labels",
|
||||
"LABELS": "Labels",
|
||||
"PROJECTS": "Projects",
|
||||
"CONFIG": "Configuration"
|
||||
},
|
||||
|
@ -390,6 +390,7 @@
|
|||
"DELETION_SUMMARY_TAG": "Do you want to delete tag {{param}}?",
|
||||
"DELETION_TITLE_TAG_DENIED": "Signed tag cannot be deleted",
|
||||
"DELETION_SUMMARY_TAG_DENIED": "The tag must be removed from the Notary before it can be deleted.\nDelete from Notary via this command:\n{{param}}",
|
||||
"TAGS_NO_DELETE": "Delete is prohibited in read only mode.",
|
||||
"FILTER_FOR_REPOSITORIES": "Filter Repositories",
|
||||
"TAG": "Tag",
|
||||
"SIZE": "Size",
|
||||
|
@ -414,9 +415,9 @@
|
|||
"INFO": "Info",
|
||||
"NO_INFO": "No description info for this repository",
|
||||
"IMAGE": "Images",
|
||||
"LABELS": ":labels",
|
||||
"LABELS": "Labels",
|
||||
"ADD_TO_IMAGE": "Add labels to this image",
|
||||
"FILTER_BY_LABEL": "Filter projects by label",
|
||||
"FILTER_BY_LABEL": "Filter images by label",
|
||||
"ADD_LABELS": "Add labels",
|
||||
"ACTION": "ACTION",
|
||||
"DEPLOY": "DEPLOY",
|
||||
|
@ -442,7 +443,7 @@
|
|||
"AUTH": "Authentication",
|
||||
"REPLICATION": "Replication",
|
||||
"EMAIL": "Email",
|
||||
"LABEL": "Label",
|
||||
"LABEL": "Labels",
|
||||
"REPOSITORY": "Repository",
|
||||
"REPO_READ_ONLY": "Repository Read Only",
|
||||
"SYSTEM": "System Settings",
|
||||
|
@ -487,7 +488,8 @@
|
|||
"PRO_CREATION_RESTRICTION": "The flag to define what users have permission to create projects. By default, everyone can create a project. Set to 'Admin Only' so that only an administrator can create a project.",
|
||||
"ROOT_CERT_DOWNLOAD": "Download the root certificate of registry.",
|
||||
"SCANNING_POLICY": "Set image scanning policy based on different requirements. 'None': No active policy; 'Daily At': Triggering scanning at the specified time everyday.",
|
||||
"VERIFY_CERT": "Verify Cert from LDAP Server"
|
||||
"VERIFY_CERT": "Verify Cert from LDAP Server",
|
||||
"REPO_TOOLTIP": "If true, means maintenance in Progress. During this period, you cannot delete repository, tag and push image."
|
||||
},
|
||||
"LDAP": {
|
||||
"URL": "LDAP URL",
|
||||
|
@ -620,6 +622,7 @@
|
|||
"OS": "OS",
|
||||
"SCAN_COMPLETION_TIME": "Scan Completed",
|
||||
"IMAGE_VULNERABILITIES": "Image Vulnerabilities",
|
||||
"LEVEL_VULNERABILITIES": "Level Vulnerabilities",
|
||||
"PLACEHOLDER": "We couldn't find any tags!",
|
||||
"COPY_ERROR": "Copy failed, please try to manually copy.",
|
||||
"FILTER_FOR_TAGS": "Filter Tags"
|
||||
|
@ -634,7 +637,9 @@
|
|||
"LABEL_NAME": "Label Name",
|
||||
"COLOR": "Color",
|
||||
"FILTER_LABEL_PLACEHOLDER": "Filter Labels",
|
||||
"NO_LABELS": "No labels"
|
||||
"NO_LABELS": "No labels",
|
||||
"DELETION_TITLE_TARGET": "Confirm Label Deletion",
|
||||
"DELETION_SUMMARY_TARGET": "Do you want to delete {{param}}?"
|
||||
},
|
||||
"WEEKLY": {
|
||||
"MONDAY": "Monday",
|
||||
|
@ -647,6 +652,7 @@
|
|||
},
|
||||
"UNKNOWN_ERROR": "Unknown errors have occurred. Please try again later.",
|
||||
"UNAUTHORIZED_ERROR": "Your session is invalid or has expired. You need to sign in to continue your action.",
|
||||
"REPO_READ_ONLY": "Maintenance in Progress: During this period, you cannot delete repository, tag and push image.",
|
||||
"FORBIDDEN_ERROR": "You do not have the proper privileges to perform the action.",
|
||||
"GENERAL_ERROR": "Errors have occurred when performing service call: {{param}}.",
|
||||
"BAD_REQUEST_ERROR": "We are unable to perform your action because of a bad request.",
|
||||
|
|
|
@ -178,7 +178,7 @@
|
|||
"REPLICATION": "Replicación",
|
||||
"USERS": "Miembros",
|
||||
"LOGS": "Logs",
|
||||
"LABELS": "labels",
|
||||
"LABELS": "Labels",
|
||||
"PROJECTS": "Proyectos",
|
||||
"CONFIG": "Configuración"
|
||||
},
|
||||
|
@ -390,6 +390,7 @@
|
|||
"DELETION_SUMMARY_TAG": "¿Quiere eliminar la etiqueta {{param}}?",
|
||||
"DELETION_TITLE_TAG_DENIED": "La etiqueta firmada no puede ser eliminada",
|
||||
"DELETION_SUMMARY_TAG_DENIED": "La etiqueta debe ser eliminada de la Notaría antes de eliminarla.\nEliminarla de la Notaría con este comando:\n{{param}}",
|
||||
"TAGS_NO_DELETE": "Delete is prohibited in read only mode.",
|
||||
"FILTER_FOR_REPOSITORIES": "Filtrar Repositorios",
|
||||
"TAG": "Etiqueta",
|
||||
"SIZE": "Size",
|
||||
|
@ -416,8 +417,8 @@
|
|||
"IMAGE": "Imágenes",
|
||||
"LABELS": "Labels",
|
||||
"ADD_TO_IMAGE": "Add labels to this image",
|
||||
"FILTER_BY_LABEL": "Filter images by label",
|
||||
"ADD_LABELS": "Add labels",
|
||||
"FILTER_BY_LABEL": "Filter projects by label",
|
||||
"ACTION": "ACTION",
|
||||
"DEPLOY": "DEPLOY",
|
||||
"ADDITIONAL_INFO": "Add Additional Info"
|
||||
|
@ -442,7 +443,7 @@
|
|||
"AUTH": "Autentificación",
|
||||
"REPLICATION": "Replicación",
|
||||
"EMAIL": "Email",
|
||||
"LABEL": "Label",
|
||||
"LABEL": "Labels",
|
||||
"REPOSITORY": "Repository",
|
||||
"REPO_READ_ONLY": "Repository Read Only",
|
||||
"SYSTEM": "Opciones del Sistema",
|
||||
|
@ -487,7 +488,8 @@
|
|||
"PRO_CREATION_RESTRICTION": "Marca para definir qué usuarios tienen permisos para crear proyectos. Por defecto, todos pueden crear proyectos. Seleccione 'Solo Administradores' para que solamente los administradores puedan crear proyectos.",
|
||||
"ROOT_CERT_DOWNLOAD": "Download the root certificate of registry.",
|
||||
"SCANNING_POLICY": "Set image scanning policy based on different requirements. 'None': No active policy; 'Daily At': Triggering scanning at the specified time everyday.",
|
||||
"VERIFY_CERT": "Verify Cert from LDAP Server"
|
||||
"VERIFY_CERT": "Verify Cert from LDAP Server",
|
||||
"REPO_TOOLTIP": "If true, means maintenance in Progress. During this period, you cannot delete repository, tag and push image."
|
||||
},
|
||||
"LDAP": {
|
||||
"URL": "LDAP URL",
|
||||
|
@ -637,7 +639,9 @@
|
|||
"LABEL_NAME": "Label Name",
|
||||
"COLOR": "Color",
|
||||
"FILTER_Label_PLACEHOLDER": "Filter Labels",
|
||||
"NO_LABELS": "No labels"
|
||||
"NO_LABELS": "No labels",
|
||||
"DELETION_TITLE_TARGET": "Confirm Label Deletion",
|
||||
"DELETION_SUMMARY_TARGET": "Do you want to delete {{param}}?"
|
||||
},
|
||||
"WEEKLY": {
|
||||
"MONDAY": "Monday",
|
||||
|
@ -650,6 +654,7 @@
|
|||
},
|
||||
"UNKNOWN_ERROR": "Ha ocurrido un error desconocido. Por favor, inténtelo de nuevo más tarde.",
|
||||
"UNAUTHORIZED_ERROR": "La sesión no es válida o ha caducado. Necesita identificarse de nuevo para llevar a cabo esa acción.",
|
||||
"REPO_READ_ONLY": "Maintenance in Progress: During this period, you cannot delete repository, tag and push image.",
|
||||
"FORBIDDEN_ERROR": "No tienes permisos para llevar a cabo esa acción.",
|
||||
"GENERAL_ERROR": "Han ocurrido errores cuando se llamaba al servicio: {{param}}.",
|
||||
"BAD_REQUEST_ERROR": "No hemos podido llevar la acción debido a una solicitud incorrecta.",
|
||||
|
|
|
@ -161,6 +161,7 @@
|
|||
"REPLICATION": "Réplication",
|
||||
"USERS": "Membres",
|
||||
"LOGS": "Logs",
|
||||
"LABELS": "Labels",
|
||||
"PROJECTS": "Projets",
|
||||
"CONFIG": "Configuration"
|
||||
},
|
||||
|
@ -345,6 +346,7 @@
|
|||
"DELETION_SUMMARY_TAG": "Voulez-vous supprimer le tag {{param}}?",
|
||||
"DELETION_TITLE_TAG_DENIED": "Un tag signé ne peut être supprimé",
|
||||
"DELETION_SUMMARY_TAG_DENIED": "La balise doit être supprimée du Résumé avant qu'elle ne puisse être supprimée. \nSupprimer du Résumé via cette commande: \n{{param}}",
|
||||
"TAGS_NO_DELETE": "Upload/Delete is prohibited in read only mode.",
|
||||
"FILTER_FOR_REPOSITORIES": "Filtrer les Dépôts",
|
||||
"TAG": "Tag",
|
||||
"SIZE": "Taille",
|
||||
|
@ -366,6 +368,10 @@
|
|||
"NOTARY_IS_UNDETERMINED": "Ne peut pas déterminer la signature de ce tag.",
|
||||
"PLACEHOLDER": "Nous ne trouvons aucun dépôt !",
|
||||
"IMAGE": "Images",
|
||||
"LABELS": "Labels",
|
||||
"ADD_TO_IMAGE": "Add labels to this image",
|
||||
"FILTER_BY_LABEL": "Filter images by label",
|
||||
"ADD_LABELS": "Add labels",
|
||||
"ACTION": "ACTION",
|
||||
"DEPLOY": "DEPLOY",
|
||||
"ADDITIONAL_INFO": "Add Additional Info"
|
||||
|
@ -390,7 +396,7 @@
|
|||
"AUTH": "Identification",
|
||||
"REPLICATION": "Réplication",
|
||||
"EMAIL": "Email",
|
||||
"LABEL": "Label",
|
||||
"LABEL": "Labels",
|
||||
"SYSTEM": "Réglages Système",
|
||||
"CONFIRM_TITLE": "Confirmer pour annuler",
|
||||
"CONFIRM_SUMMARY": "Certaines modifications n'ont pas été sauvegardées. Voulez-vous les défaire ?",
|
||||
|
@ -431,6 +437,7 @@
|
|||
"PRO_CREATION_RESTRICTION": "L'indicateur pour définir quels utilisateurs ont le droit de créer des projets. Par défaut, tout le monde peut créer un projet. Définissez sur 'Administrateur Seulement' pour que seul un administrateur puisse créer un projet.",
|
||||
"ROOT_CERT_DOWNLOAD": "Téléchargez le certificat racine du dépôt.",
|
||||
"SCANNING_POLICY": "Définissez la politique d'analyse des images en fonction des différentes exigences. 'Aucune' : pas de politique active; 'Tousles jours à' : déclenchement du balayage à l'heure spécifiée tous les jours."
|
||||
"REPO_TOOLTIP": "If true, means maintenance in Progress. During this period, you cannot delete repository, tag and push image."
|
||||
},
|
||||
"LDAP": {
|
||||
"URL": "URL LDAP",
|
||||
|
@ -562,10 +569,29 @@
|
|||
"LABEL": {
|
||||
"LABEL": "Label",
|
||||
"DESCRIPTION": "Description",
|
||||
"CREATION_TIME": "Creation Time"
|
||||
"CREATION_TIME": "Creation Time",
|
||||
"NEW_LABEL": "New Label",
|
||||
"EDIT": "Edit",
|
||||
"DELETE": "Delete",
|
||||
"LABEL_NAME": "Label Name",
|
||||
"COLOR": "Color",
|
||||
"FILTER_Label_PLACEHOLDER": "Filter Labels",
|
||||
"NO_LABELS": "No labels",
|
||||
"DELETION_TITLE_TARGET": "Confirm Label Deletion",
|
||||
"DELETION_SUMMARY_TARGET": "Do you want to delete {{param}}?"
|
||||
},
|
||||
"WEEKLY": {
|
||||
"MONDAY": "Monday",
|
||||
"TUESDAY": "Tuesday",
|
||||
"WEDNESDAY": "Wednesday",
|
||||
"THURSDAY": "Thursday",
|
||||
"FRIDAY": "Friday",
|
||||
"SATURDAY": "Saturday",
|
||||
"SUNDAY": "Sunday"
|
||||
},
|
||||
"UNKNOWN_ERROR": "Des erreurs inconnues sont survenues. Veuillez réessayer plus tard.",
|
||||
"UNAUTHORIZED_ERROR": "Votre session est invalide ou a expiré. Vous devez vous connecter pour continuer votre action.",
|
||||
"REPO_READ_ONLY": "Maintenance in Progress: During this period, you cannot delete repository, tag and push image.",
|
||||
"FORBIDDEN_ERROR": "Vous n'avez pas les privilèges appropriés pour effectuer l'action.",
|
||||
"GENERAL_ERROR": "Des erreurs sont survenues lors de l'appel à un service : {{param}}.",
|
||||
"BAD_REQUEST_ERROR": "Nous ne pouvons pas exécuter votre action à cause d'une mauvaise requête.",
|
||||
|
|
|
@ -390,6 +390,7 @@
|
|||
"DELETION_SUMMARY_TAG": "确认删除镜像标签 {{param}}?",
|
||||
"DELETION_TITLE_TAG_DENIED": "已签名的镜像不能被删除",
|
||||
"DELETION_SUMMARY_TAG_DENIED": "要删除此镜像标签必须首先从Notary中删除。\n请执行如下Notary命令删除:\n{{param}}",
|
||||
"TAGS_NO_DELETE": "在只读模式下删除是被禁止的",
|
||||
"FILTER_FOR_REPOSITORIES": "过滤镜像仓库",
|
||||
"TAG": "标签",
|
||||
"SIZE": "大小",
|
||||
|
@ -487,7 +488,8 @@
|
|||
"PRO_CREATION_RESTRICTION": "用来确定哪些用户有权限创建项目,默认为’所有人‘,设置为’仅管理员‘则只有管理员可以创建项目。",
|
||||
"ROOT_CERT_DOWNLOAD": "下载镜像库根证书.",
|
||||
"SCANNING_POLICY": "基于不同需求设置镜像扫描策略。‘无’:不设置任何策略;‘每日定时’:每天在设置的时间定时执行扫描。",
|
||||
"VERIFY_CERT": "检查来自LDAP服务端的证书"
|
||||
"VERIFY_CERT": "检查来自LDAP服务端的证书",
|
||||
"REPO_TOOLTIP": "选中,表示正在维护状态,不可删除仓库及标签,也不可以推送镜像。"
|
||||
},
|
||||
"LDAP": {
|
||||
"URL": "LDAP URL",
|
||||
|
@ -637,7 +639,9 @@
|
|||
"LABEL_NAME": "标签名字",
|
||||
"COLOR": "颜色",
|
||||
"FILTER_Label_PLACEHOLDER": "过滤标签",
|
||||
"NO_LABELS": "无标签"
|
||||
"NO_LABELS": "无标签",
|
||||
"DELETION_TITLE_TARGET":"删除标签确认",
|
||||
"DELETION_SUMMARY_TARGET": "确认删除标签 {{param}}?"
|
||||
},
|
||||
"WEEKLY": {
|
||||
"MONDAY": "周一",
|
||||
|
@ -650,6 +654,7 @@
|
|||
},
|
||||
"UNKNOWN_ERROR": "发生未知错误,请稍后再试。",
|
||||
"UNAUTHORIZED_ERROR": "会话无效或者已经过期, 请重新登录以继续。",
|
||||
"REPO_READ_ONLY": "正在进行维护,在这期间,不能删除仓库、标签,也不能推送镜像。",
|
||||
"FORBIDDEN_ERROR": "当前操作被禁止,请确认你有合法的权限。",
|
||||
"GENERAL_ERROR": "调用后台服务时出现错误: {{param}}。",
|
||||
"BAD_REQUEST_ERROR": "错误请求, 操作无法完成。",
|
||||
|
|
|
@ -48,3 +48,20 @@
|
|||
transform: rotate(-90deg);
|
||||
}
|
||||
.datagrid-spinner{margin-top: 24px;}
|
||||
|
||||
/* set overflow bar style */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;background:transparent;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 2px rgba(0,0,0,0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 4px;
|
||||
background: #ccc;
|
||||
-webkit-box-shadow: inset 0 0 2px rgba(0,0,0,0.5);
|
||||
}
|
||||
::-webkit-scrollbar-thumb:window-inactive {
|
||||
background: rgba(255,0,0,0.4);
|
||||
}
|
||||
|
|
|
@ -30,6 +30,9 @@ cn: harbor_users
|
|||
description: All users
|
||||
member: cn=mike,ou=people,dc=example,dc=com
|
||||
member: cn=mike02,ou=people,dc=example,dc=com
|
||||
member: cn=mike03,ou=people,dc=example,dc=com
|
||||
member: cn=mike04,ou=people,dc=example,dc=com
|
||||
member: cn=mike05,ou=people,dc=example,dc=com
|
||||
objectclass: groupOfNames
|
||||
objectclass: top
|
||||
|
||||
|
|
|
@ -221,6 +221,7 @@ Set Scan All To Daily
|
|||
Click Scan Now
|
||||
click element //vulnerability-config//button[contains(.,'SCAN')]
|
||||
|
||||
|
||||
Enable Read Only
|
||||
${rc} ${output}= Run And Return Rc And Output curl -u admin:Harbor12345 -s --insecure -H "Content-Type: application/json" -X PUT -d '{"read_only":true}' "https://${ip}/api/configurations"
|
||||
Log To Console ${output}
|
||||
|
@ -229,4 +230,46 @@ Enable Read Only
|
|||
Disable Read Only
|
||||
${rc} ${output}= Run And Return Rc And Output curl -u admin:Harbor12345 -s --insecure -H "Content-Type: application/json" -X PUT -d '{"read_only":false}' "https://${ip}/api/configurations"
|
||||
Log To Console ${output}
|
||||
Should Be Equal As Integers ${rc} 0
|
||||
Should Be Equal As Integers ${rc} 0
|
||||
|
||||
## System labels
|
||||
Switch To System Labels
|
||||
Sleep 1
|
||||
Click Element xpath=${configuration_xpath}
|
||||
Click Element xpath=//*[@id="config-label"]
|
||||
|
||||
Create New Labels
|
||||
[Arguments] ${labelname}
|
||||
Click Element xpath=//*[@id="system_label"]/hbr-label/div/div/div[2]/button[1]
|
||||
Sleep 1
|
||||
Input Text xpath=//*[@id="name"] ${labelname}
|
||||
Sleep 1
|
||||
Click Element xpath=//*[@id="system_label"]/hbr-label/div/div/div[2]/hbr-create-edit-label/div/form/section/label[2]/clr-dropdown/button
|
||||
Sleep 1
|
||||
Click Element xpath=//*[@id="system_label"]/hbr-label/div/div/div[2]/hbr-create-edit-label/div/form/section/label[2]/clr-dropdown/clr-dropdown-menu/label[1]
|
||||
Sleep 1
|
||||
Input Text xpath=//*[@id="description"] global
|
||||
Click Element xpath=//*[@id="system_label"]/hbr-label/div/div/div[2]/hbr-create-edit-label/div/form/section/label[4]/button[2]
|
||||
Capture Page Screenshot
|
||||
Wait Until Page Contains ${labelname}
|
||||
|
||||
Update A Label
|
||||
[Arguments] ${labelname}
|
||||
Click Element xpath=//*[@id="system_label"]/hbr-label/div/div/div[3]/clr-datagrid/div/div/div/clr-dg-table-wrapper/div[2]/clr-dg-row[1]/div/clr-dg-cell[1]/clr-checkbox
|
||||
Sleep 1
|
||||
Click ELement xpath=//*[@id="system_label"]/hbr-label/div/div/div[2]/button[2]
|
||||
Sleep 1
|
||||
Input Text xpath=//*[@id="name"] ${labelname}
|
||||
Sleep 1
|
||||
Click Element xpath=//*[@id="system_label"]/hbr-label/div/div/div[2]/hbr-create-edit-label/div/form/section/label[4]/button[2]
|
||||
Capture Page Screenshot
|
||||
Wait Until Page Contains ${labelname}
|
||||
|
||||
Delete A Label
|
||||
Click Element xpath=//*[@id="system_label"]/hbr-label/div/div/div[3]/clr-datagrid/div/div/div/clr-dg-table-wrapper/div[2]/clr-dg-row[1]/div/clr-dg-cell[1]/clr-checkbox
|
||||
Sleep 1
|
||||
Click ELement xpath=//*[@id="system_label"]/hbr-label/div/div/div[2]/button[3]
|
||||
Sleep 3
|
||||
Capture Page Screenshot
|
||||
Click Element xpath=//*[@id="system_label"]/hbr-label/div/confirmation-dialog/clr-modal/div/div[1]/div/div[1]/div/div[3]/button[2]
|
||||
Wait Until Page Contains Deleted successfully
|
||||
|
|
|
@ -215,4 +215,12 @@ Edit Repo Info
|
|||
Page Should Contain test_description_info
|
||||
Capture Page Screenshot RepoInfo.png
|
||||
|
||||
|
||||
Add Labels To Tag
|
||||
[Arguments] ${tagName} ${labelName}
|
||||
Click Element xpath=//clr-dg-row[contains(.,"${tagName}")]//label
|
||||
Sleep 1
|
||||
Click Element xpath=//clr-dg-action-bar//clr-dropdown//button
|
||||
Sleep 1
|
||||
Click Element xpath=//clr-dropdown//div//label[contains(.,"${labelName}")]
|
||||
Sleep 3
|
||||
Page Should Contain Element xpath=//clr-dg-row//label[contains(.,"${labelName}")]
|
||||
|
|
|
@ -55,12 +55,10 @@ Test Case - LDAP User On-borad New Member
|
|||
Init Chrome Driver
|
||||
${d}= Get Current Date result_format=%m%s
|
||||
Sign In Harbor ${HARBOR_URL} mike03 zhu88jie
|
||||
Switch To User Tag
|
||||
Page Should Not Contain mike04
|
||||
Back To Projects
|
||||
Create An New Project project${d}
|
||||
Go Into Project project${d}
|
||||
Switch To Member
|
||||
Page Should Not Contain mike04
|
||||
Add Guest Member To Project mike04
|
||||
Page Should Contain mike04
|
||||
Close Browser
|
||||
|
|
|
@ -48,7 +48,7 @@ Test Case - Read Only Mode
|
|||
Close Browser
|
||||
|
||||
Test Case - Create An New User
|
||||
Init Chrome Driver
|
||||
Init Chrome Driver
|
||||
${d}= Get Current Date result_format=%m%s
|
||||
Create An New User url=${HARBOR_URL} username=tester${d} email=tester${d}@vmware.com realname=harbortest newPassword=Test1@34 comment=harbortest
|
||||
Close Browser
|
||||
|
@ -73,7 +73,7 @@ Test Case - Update Password
|
|||
Logout Harbor
|
||||
Sign In Harbor ${HARBOR_URL} tester${d} Test12#4
|
||||
Close Browser
|
||||
|
||||
|
||||
Test Case - Create An New Project
|
||||
Init Chrome Driver
|
||||
${d}= Get Current Date result_format=%m%s
|
||||
|
@ -93,8 +93,8 @@ Test Case - User View Projects
|
|||
Wait Until Page Contains test${d}1
|
||||
Wait Until Page Contains test${d}2
|
||||
Wait Until Page Contains test${d}3
|
||||
Close Browser
|
||||
|
||||
Close Browser
|
||||
|
||||
Test Case - Push Image
|
||||
Init Chrome Driver
|
||||
${d}= Get Current Date result_format=%m%s
|
||||
|
@ -108,21 +108,21 @@ Test Case - Push Image
|
|||
Test Case - User View Logs
|
||||
Init Chrome Driver
|
||||
${d}= Get Current Date result_format=%m%s
|
||||
|
||||
|
||||
Create An New Project With New User url=${HARBOR_URL} username=tester${d} email=tester${d}@vmware.com realname=tester${d} newPassword=Test1@34 comment=harbor projectname=project${d} public=true
|
||||
|
||||
Push image ${ip} tester${d} Test1@34 project${d} busybox:latest
|
||||
Pull image ${ip} tester${d} Test1@34 project${d} busybox:latest
|
||||
|
||||
|
||||
Go Into Project project${d}
|
||||
Delete Repo project${d}
|
||||
|
||||
|
||||
Go To Project Log
|
||||
Advanced Search Should Display
|
||||
|
||||
|
||||
Do Log Advanced Search
|
||||
Close Browser
|
||||
|
||||
|
||||
Test Case - Manage project publicity
|
||||
Init Chrome Driver
|
||||
${d}= Get Current Date result_format=%m%s
|
||||
|
@ -172,12 +172,12 @@ Test Case - Project Level Policy Public
|
|||
Click Project Public
|
||||
Save Project Config
|
||||
# Verify
|
||||
Public Should Be Selected
|
||||
Public Should Be Selected
|
||||
Back To Projects
|
||||
# Project${d} default should be private
|
||||
# Here logout and login to try avoid a bug only in autotest
|
||||
Logout Harbor
|
||||
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD}
|
||||
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD}
|
||||
Filter Object project${d}
|
||||
Project Should Be Public project${d}
|
||||
Close Browser
|
||||
|
@ -266,6 +266,52 @@ Test Case - Edit Token Expire
|
|||
Modify Token Expiration 30
|
||||
Close Browser
|
||||
|
||||
Test Case - Create A New Labels
|
||||
Init Chrome Driver
|
||||
${d}= Get Current Date
|
||||
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD}
|
||||
Switch To System Labels
|
||||
Create New Labels label_${d}
|
||||
Close Browser
|
||||
|
||||
Test Case - Update Label
|
||||
Init Chrome Driver
|
||||
${d}= Get Current Date
|
||||
|
||||
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD}
|
||||
Switch To System Labels
|
||||
Create New Labels label_${d}
|
||||
Sleep 3
|
||||
${d1}= Get Current Date
|
||||
Update A Label label_${d}
|
||||
Close Browser
|
||||
|
||||
Test Case - Delete Label
|
||||
Init Chrome Driver
|
||||
${d}= Get Current Date
|
||||
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD}
|
||||
Switch To System Labels
|
||||
Create New Labels label_${d}
|
||||
Sleep 3
|
||||
Delete A Label
|
||||
Close Browser
|
||||
|
||||
TestCase - Add Labels To A Repo
|
||||
Init Chrome Driver
|
||||
${d}= Get Current Date result_format=%m%s
|
||||
Create An New User ${HARBOR_URL} test${d} test${d}@vmware.com test${d} Test1@34 harbor
|
||||
Create An New Project project${d}
|
||||
Push Image ${ip} test${d} Test1@34 project${d} vmware/photon:1.0
|
||||
Sleep 2
|
||||
#Add labels
|
||||
Switch To System Labels
|
||||
Create New Labels label_${d}
|
||||
Sleep 2
|
||||
Go Into Project project${d}
|
||||
Go Into Repo project${d}/vmware/photon
|
||||
Add Labels To Tag 1.0 label_${d}
|
||||
Close Browser
|
||||
|
||||
Test Case - Scan A Tag In The Repo
|
||||
Init Chrome Driver
|
||||
${d}= get current date result_format=%m%s
|
||||
|
@ -327,7 +373,7 @@ Test Case - Manual Scan All
|
|||
Summary Chart Should Display latest
|
||||
Close Browser
|
||||
#
|
||||
Test Case - Project Level Image Serverity Policy
|
||||
Test Case - Project Level Image Serverity Policy
|
||||
Init Chrome Driver
|
||||
Push Image ${ip} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} library haproxy
|
||||
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD}
|
||||
|
@ -346,7 +392,7 @@ Test Case - Scan Image On Push
|
|||
Go Into Project library
|
||||
Goto Project Config
|
||||
Enable Scan On Push
|
||||
Push Image ${ip} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} library memcached
|
||||
Push Image ${ip} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} library memcached
|
||||
Back To Projects
|
||||
Go Into Project library
|
||||
Go Into Repo memcached
|
||||
|
@ -382,7 +428,7 @@ Test Case - Delete A Project
|
|||
Init Chrome Driver
|
||||
${d}= Get Current Date result_format=%m%s
|
||||
Create An New Project With New User ${HARBOR_URL} tester${d} tester${d}@vmware.com tester${d} Test1@34 harobr project${d} false
|
||||
Push Image ${ip} tester${d} Test1@34 project${d} hello-world
|
||||
Push Image ${ip} tester${d} Test1@34 project${d} hello-world
|
||||
Project Should Not Be Deleted project${d}
|
||||
Go Into Project project${d}
|
||||
Delete Repo project${d}
|
||||
|
@ -392,7 +438,7 @@ Test Case - Delete A Project
|
|||
|
||||
Test Case - Delete Multi Project
|
||||
Init Chrome Driver
|
||||
${d}= Get Current Date result_format=%m%s
|
||||
${d}= Get Current Date result_format=%m%s
|
||||
Create An New User ${HARBOR_URL} test${d} test${d}@vmware.com test${d} Test1@34 harbor
|
||||
Create An New Project projecta${d}
|
||||
Create An New Project projectb${d}
|
||||
|
@ -418,8 +464,8 @@ Test Case - Delete Multi User
|
|||
Switch To User Tag
|
||||
Filter Object delete
|
||||
Multi-delete Object deletea deleteb deletec
|
||||
# Assert delete
|
||||
Delete Success
|
||||
# Assert delete
|
||||
Delete Success
|
||||
Sleep 1
|
||||
# Filter object delete
|
||||
Page Should Not Contain deletea
|
||||
|
@ -430,7 +476,7 @@ Test Case - Delete Multi Repo
|
|||
${d}= Get Current Date result_format=%m%s
|
||||
Create An New User ${HARBOR_URL} test${d} test${d}@vmware.com test${d} Test1@34 harbor
|
||||
Create An New Project project${d}
|
||||
Push Image ${ip} test${d} Test1@34 project${d} hello-world
|
||||
Push Image ${ip} test${d} Test1@34 project${d} hello-world
|
||||
Push Image ${ip} test${d} Test1@34 project${d} busybox
|
||||
Sleep 2
|
||||
Go Into Project project${d}
|
||||
|
@ -471,7 +517,7 @@ Test Case - Delete Multi Member
|
|||
Delete Success
|
||||
Page Should Not Contain testa${d}
|
||||
Close Browser
|
||||
|
||||
|
||||
Test Case - Assign Sys Admin
|
||||
Init Chrome Driver
|
||||
${d}= Get Current Date result_format=%m%s
|
||||
|
@ -490,7 +536,7 @@ Test Case - Admin Push Signed Image
|
|||
|
||||
${rc} ${output}= Run And Return Rc And Output docker pull hello-world:latest
|
||||
Log ${output}
|
||||
|
||||
|
||||
Push image ${ip} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} library hello-world:latest
|
||||
${rc} ${output}= Run And Return Rc And Output ./tests/robot-cases/Group9-Content-trust/notary-push-image.sh ${ip}
|
||||
Log ${output}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
Test 11-01 - System admins manage global level labels
|
||||
=======
|
||||
|
||||
# Purpose:
|
||||
|
||||
To verify that the system administrators can manage(CURD) the global level labels.
|
||||
|
||||
# References:
|
||||
User guide
|
||||
|
||||
# Environment:
|
||||
* This test requires that a Harbor instance is running and available.
|
||||
|
||||
# Test Steps:
|
||||
|
||||
1. The system admin user logs in to the UI.
|
||||
2. The admin user creates a global level label from the UI.
|
||||
3. The admin user edits the label created in Step1.
|
||||
4. The admin user deletes the label created in Step1.
|
||||
|
||||
# Expected Outcome:
|
||||
|
||||
* In Step2, the label can be created successfully.
|
||||
* In Step3, the label can be updated successfully.
|
||||
* In Step4, the label can be deleted successfully.
|
||||
|
||||
# Possible Problems:
|
||||
None
|
|
@ -0,0 +1,31 @@
|
|||
Test 11-02 - Project admins manage project level labels
|
||||
=======
|
||||
|
||||
# Purpose:
|
||||
|
||||
To verify that the project administrators can manage(CURD) the project level labels.
|
||||
|
||||
# References:
|
||||
User guide
|
||||
|
||||
# Environment:
|
||||
* This test requires that a Harbor instance is running and available.
|
||||
|
||||
# Test Steps:
|
||||
|
||||
1. The project admin user logs in to the UI.
|
||||
2. The project admin user creates a project level label under a project from the UI.
|
||||
3. The project admin user edits the label created in Step1.
|
||||
4. The project admin user deletes the label created in Step1.
|
||||
5. The project developer user logs in to the UI.
|
||||
6. The project developer user tries to create a project level label under a project from the UI.
|
||||
|
||||
# Expected Outcome:
|
||||
|
||||
* In Step2, the label can be created successfully.
|
||||
* In Step3, the label can be updated successfully.
|
||||
* In Step4, the label can be deleted successfully.
|
||||
* In Step6, the project developer user can not create project level labels.
|
||||
|
||||
# Possible Problems:
|
||||
None
|
|
@ -0,0 +1,37 @@
|
|||
Test 11-03 - Add/remove labels to/from images
|
||||
=======
|
||||
|
||||
# Purpose:
|
||||
|
||||
To verify that the users whose role >= project developer can add/remove labels to/from images.
|
||||
|
||||
# References:
|
||||
User guide
|
||||
|
||||
# Environment:
|
||||
* This test requires that a Harbor instance is running and available.
|
||||
* At least one global level label and one project level label are created.
|
||||
|
||||
# Test Steps:
|
||||
|
||||
1. The project developer user logs in to the UI.
|
||||
2. The user add a global level label to an image from the UI.
|
||||
3. The user add a project level label to an image from the UI.
|
||||
4. The user removes the global label from the image.
|
||||
5. The user removes the project label from the image.
|
||||
6. Login in to the UI as a project admin user and repeat the Step2-5.
|
||||
7. Login in to the UI as a system admin user and repeat the Step2-5.
|
||||
8. Login in to the UI as a project guest user and try to add a label to an image.
|
||||
|
||||
# Expected Outcome:
|
||||
|
||||
* In Step2, the global level label can be added to the image successfully.
|
||||
* In Step3, the project level label can be added to the image successfully.
|
||||
* In Step4, the global level label can be removed from the image successfully.
|
||||
* In Step5, the project level label can be removed from the image successfully.
|
||||
* In Step6, the project admin user can do the same operations as the project developer user.
|
||||
* In Step7, the system admin user can do the same operations as the project developer user.
|
||||
* In Step8, the project guest user can not add a label to an image.
|
||||
|
||||
# Possible Problems:
|
||||
None
|
|
@ -0,0 +1,27 @@
|
|||
Test 11-04 - Filter images by label
|
||||
=======
|
||||
|
||||
# Purpose:
|
||||
|
||||
To verify that the images can be filtered by labels.
|
||||
|
||||
# References:
|
||||
User guide
|
||||
|
||||
# Environment:
|
||||
* This test requires that a Harbor instance is running and available.
|
||||
* Create at least two labels and add one of them to an image.
|
||||
|
||||
# Test Steps:
|
||||
|
||||
1. The project guest user logs in to the UI.
|
||||
2. The user filters the images by the label that has been added to the image.
|
||||
3. The user filters the images by the label that has not been added to the image.
|
||||
|
||||
# Expected Outcome:
|
||||
|
||||
* In Step2, the image list contains the image which is labeled.
|
||||
* In Step3, the image list doesn't contain the image which is labeled.
|
||||
|
||||
# Possible Problems:
|
||||
None
|
14
tools/migration/Dockerfile
Normal file
14
tools/migration/Dockerfile
Normal file
|
@ -0,0 +1,14 @@
|
|||
FROM vmware/mariadb-photon:10.2.8
|
||||
|
||||
RUN tdnf distro-sync -y \
|
||||
&& tdnf install -y mariadb-devel python2 python2-devel python-pip gcc \
|
||||
linux-api-headers glibc-devel binutils zlib-devel openssl-devel \
|
||||
&& pip install mysqlclient alembic \
|
||||
&& tdnf clean all \
|
||||
&& mkdir -p /harbor-migration
|
||||
|
||||
WORKDIR /harbor-migration
|
||||
|
||||
COPY ./ ./
|
||||
|
||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
|
@ -36,6 +36,12 @@ log_rotate_count = $log_rotate_count
|
|||
#are all valid.
|
||||
log_rotate_size = $log_rotate_size
|
||||
|
||||
#Config http proxy for Clair, e.g. http://my.proxy.com:3128
|
||||
#Clair doesn't need to connect to harbor ui container via http proxy.
|
||||
http_proxy =
|
||||
https_proxy =
|
||||
no_proxy = 127.0.0.1,localhost,ui
|
||||
|
||||
#NOTES: The properties between BEGIN INITIAL PROPERTIES and END INITIAL PROPERTIES
|
||||
#only take effect in the first boot, the subsequent changes of these properties
|
||||
#should be performed on web ui
|
||||
|
@ -93,6 +99,18 @@ ldap_timeout = 5
|
|||
#Verify certificate from LDAP server
|
||||
ldap_verify_cert = true
|
||||
|
||||
#The base dn from which to lookup a group in LDAP/AD
|
||||
ldap_group_basedn = ou=group,dc=mydomain,dc=com
|
||||
|
||||
#filter to search LDAP/AD group
|
||||
ldap_group_filter = objectclass=group
|
||||
|
||||
#The attribute used to name a LDAP/AD group, it could be cn, name
|
||||
ldap_group_gid = cn
|
||||
|
||||
#The scope to search for ldap groups. 0-LDAP_SCOPE_BASE, 1-LDAP_SCOPE_ONELEVEL, 2-LDAP_SCOPE_SUBTREE
|
||||
ldap_group_scope = 2
|
||||
|
||||
#Turn on or off the self-registration feature
|
||||
self_registration = on
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import utils
|
|||
import importlib
|
||||
import glob
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
def main():
|
||||
target_version = '1.5.0'
|
|
@ -3,7 +3,7 @@ echo "
|
|||
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
script_location = migration_harbor
|
||||
script_location = /harbor-migration/db/migration_harbor
|
||||
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
|
|
@ -5,7 +5,7 @@ import sqlalchemy as sa
|
|||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker, relationship
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
import datetime
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
@ -27,6 +27,17 @@ class User(Base):
|
|||
update_time = sa.Column(mysql.TIMESTAMP)
|
||||
|
||||
|
||||
class UserGroup(Base):
|
||||
__tablename__ = 'user_group'
|
||||
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
group_name = sa.Column(sa.String(128), nullable = False)
|
||||
group_type = sa.Column(sa.Integer, server_default=sa.text("'0'"))
|
||||
ldap_group_dn = sa.Column(sa.String(512), nullable=False)
|
||||
creation_time = sa.Column(mysql.TIMESTAMP, server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
update_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"))
|
||||
|
||||
|
||||
class Properties(Base):
|
||||
__tablename__ = 'properties'
|
||||
|
||||
|
@ -38,14 +49,15 @@ class Properties(Base):
|
|||
class ProjectMember(Base):
|
||||
__tablename__ = 'project_member'
|
||||
|
||||
project_id = sa.Column(sa.Integer(), primary_key = True)
|
||||
user_id = sa.Column(sa.Integer(), primary_key = True)
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
project_id = sa.Column(sa.Integer(), nullable=False)
|
||||
entity_id = sa.Column(sa.Integer(), nullable=False)
|
||||
entity_type = sa.Column(sa.String(1), nullable=False)
|
||||
role = sa.Column(sa.Integer(), nullable = False)
|
||||
creation_time = sa.Column(mysql.TIMESTAMP, nullable = True)
|
||||
update_time = sa.Column(mysql.TIMESTAMP, nullable = True)
|
||||
sa.ForeignKeyConstraint(['project_id'], [u'project.project_id'], ),
|
||||
sa.ForeignKeyConstraint(['role'], [u'role.role_id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], [u'user.user_id'], ),
|
||||
creation_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP"))
|
||||
update_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"))
|
||||
|
||||
__table_args__ = (sa.UniqueConstraint('project_id', 'entity_id', 'entity_type', name='unique_name_and_scope'),)
|
||||
|
||||
|
||||
class UserProjectRole(Base):
|
||||
|
@ -150,6 +162,7 @@ class ReplicationJob(Base):
|
|||
repository = sa.Column(sa.String(256), nullable=False)
|
||||
operation = sa.Column(sa.String(64), nullable=False)
|
||||
tags = sa.Column(sa.String(16384))
|
||||
job_uuid = sa.Column(sa.String(64))
|
||||
creation_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP"))
|
||||
update_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"))
|
||||
|
||||
|
@ -207,6 +220,7 @@ class ImageScanJob(Base):
|
|||
repository = sa.Column(sa.String(256), nullable=False)
|
||||
tag = sa.Column(sa.String(128), nullable=False)
|
||||
digest = sa.Column(sa.String(128))
|
||||
job_uuid = sa.Column(sa.String(64))
|
||||
creation_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP"))
|
||||
update_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"))
|
||||
|
||||
|
@ -230,3 +244,33 @@ class ClairVulnTimestamp(Base):
|
|||
id = sa.Column(sa.Integer, nullable=False, primary_key=True)
|
||||
namespace = sa.Column(sa.String(128), nullable=False, unique=True)
|
||||
last_update = sa.Column(mysql.TIMESTAMP)
|
||||
|
||||
|
||||
class HarborLabel(Base):
|
||||
__tablename__ = "harbor_label"
|
||||
|
||||
id = sa.Column(sa.Integer, nullable=False, primary_key=True)
|
||||
name = sa.Column(sa.String(128), nullable=False)
|
||||
description = sa.Column(sa.Text)
|
||||
color = sa.Column(sa.String(16))
|
||||
level = sa.Column(sa.String(1), nullable=False)
|
||||
scope = sa.Column(sa.String(1), nullable=False)
|
||||
project_id = sa.Column(sa.Integer, nullable=False)
|
||||
creation_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP"))
|
||||
update_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"))
|
||||
|
||||
__table_args__ = (sa.UniqueConstraint('name', 'scope', name='unique_name_and_scope'),)
|
||||
|
||||
|
||||
class HarborResourceLabel(Base):
|
||||
__tablename__ = 'harbor_resource_label'
|
||||
|
||||
id = sa.Column(sa.Integer, nullable=False, primary_key=True)
|
||||
label_id = sa.Column(sa.Integer, nullable=False)
|
||||
resource_id = sa.Column(sa.Integer)
|
||||
resource_name = sa.Column(sa.String(256))
|
||||
resource_type = sa.Column(sa.String(1), nullable=False)
|
||||
creation_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP"))
|
||||
update_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"))
|
||||
|
||||
__table_args__ = (sa.UniqueConstraint('label_id', 'resource_id', 'resource_name', 'resource_type', name='unique_label_resource'),)
|
||||
|
|
|
@ -1,103 +0,0 @@
|
|||
## Configuration file of Harbor
|
||||
|
||||
#The IP address or hostname to access admin UI and registry service.
|
||||
#DO NOT use localhost or 127.0.0.1, because Harbor needs to be accessed by external clients.
|
||||
hostname = $hostname
|
||||
|
||||
#The protocol for accessing the UI and token/notification service, by default it is http.
|
||||
#It can be set to https if ssl is enabled on nginx.
|
||||
ui_url_protocol = $ui_url_protocol
|
||||
|
||||
#The password for the root user of mysql db, change this before any production use.
|
||||
db_password = $db_password
|
||||
|
||||
#Determine whether the UI should use compressed js files.
|
||||
#For production, set it to on. For development, set it to off.
|
||||
use_compressed_js = $use_compressed_js
|
||||
|
||||
#Maximum number of job workers in job service
|
||||
max_job_workers = $max_job_workers
|
||||
|
||||
#Determine whether or not to generate certificate for the registry's token.
|
||||
#If the value is on, the prepare script creates new root cert and private key
|
||||
#for generating token to access the registry. If the value is off the default key/cert will be used.
|
||||
#This flag also controls the creation of the notary signer's cert.
|
||||
customize_crt = $customize_crt
|
||||
|
||||
#The path of cert and key files for nginx, they are applied only the protocol is set to https
|
||||
ssl_cert = $ssl_cert
|
||||
ssl_cert_key = $ssl_cert_key
|
||||
|
||||
#The path of secretkey storage
|
||||
secretkey_path = /data
|
||||
|
||||
#Admiral's url, comment this attribute, or set its value to to NA when Harbor is standalone
|
||||
admiral_url = NA
|
||||
|
||||
#NOTES: The properties between BEGIN INITIAL PROPERTIES and END INITIAL PROPERTIES
|
||||
#only take effect in the first boot, the subsequent changes of these properties
|
||||
#should be performed on web ui
|
||||
#************************BEGIN INITIAL PROPERTIES************************
|
||||
|
||||
#Email account settings for sending out password resetting emails.
|
||||
|
||||
#Email server uses the given username and password to authenticate on TLS connections to host and act as identity.
|
||||
#Identity left blank to act as username.
|
||||
email_identity = $email_identity
|
||||
|
||||
email_server = $email_server
|
||||
email_server_port = $email_server_port
|
||||
email_username = $email_username
|
||||
email_password = $email_password
|
||||
email_from = $email_from
|
||||
email_ssl = $email_ssl
|
||||
|
||||
##The initial password of Harbor admin, only works for the first time when Harbor starts.
|
||||
#It has no effect after the first launch of Harbor.
|
||||
#Change the admin password from UI after launching Harbor.
|
||||
harbor_admin_password = $harbor_admin_password
|
||||
|
||||
##By default the auth mode is db_auth, i.e. the credentials are stored in a local database.
|
||||
#Set it to ldap_auth if you want to verify a user's credentials against an LDAP server.
|
||||
auth_mode = $auth_mode
|
||||
|
||||
#The url for an ldap endpoint.
|
||||
ldap_url = $ldap_url
|
||||
|
||||
#A user's DN who has the permission to search the LDAP/AD server.
|
||||
#If your LDAP/AD server does not support anonymous search, you should configure this DN and ldap_search_pwd.
|
||||
#ldap_searchdn = uid=searchuser,ou=people,dc=mydomain,dc=com
|
||||
|
||||
#the password of the ldap_searchdn
|
||||
#ldap_search_pwd = password
|
||||
|
||||
#The base DN from which to look up a user in LDAP/AD
|
||||
ldap_basedn = $ldap_basedn
|
||||
|
||||
#Search filter for LDAP/AD, make sure the syntax of the filter is correct.
|
||||
#ldap_filter = (objectClass=person)
|
||||
|
||||
# The attribute used in a search to match a user, it could be uid, cn, email, sAMAccountName or other attributes depending on your LDAP/AD
|
||||
ldap_uid = $ldap_uid
|
||||
|
||||
#the scope to search for users, 1-LDAP_SCOPE_BASE, 2-LDAP_SCOPE_ONELEVEL, 3-LDAP_SCOPE_SUBTREE
|
||||
ldap_scope = $ldap_scope
|
||||
|
||||
#Timeout (in seconds) when connecting to an LDAP Server. The default value (and most reasonable) is 5 seconds.
|
||||
ldap_timeout = 5
|
||||
|
||||
#Turn on or off the self-registration feature
|
||||
self_registration = $self_registration
|
||||
|
||||
#The expiration time (in minute) of token created by token service, default is 30 minutes
|
||||
token_expiration = $token_expiration
|
||||
|
||||
#The flag to control what users have permission to create projects
|
||||
#Be default everyone can create a project, set to "adminonly" such that only admin can create project.
|
||||
project_creation_restriction = $project_creation_restriction
|
||||
|
||||
#Determine whether the job service should verify the ssl cert when it connects to a remote registry.
|
||||
#Set this flag to off when the remote registry uses a self-signed or untrusted certificate.
|
||||
verify_remote_cert = $verify_remote_cert
|
||||
#************************BEGIN INITIAL PROPERTIES************************
|
||||
#############
|
|
@ -1,117 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
from string import Template
|
||||
import string
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
from io import open
|
||||
|
||||
if sys.version_info[:3][0] == 2:
|
||||
import ConfigParser as ConfigParser
|
||||
import StringIO as StringIO
|
||||
|
||||
if sys.version_info[:3][0] == 3:
|
||||
import configparser as ConfigParser
|
||||
import io as StringIO
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--source-loc', dest='source_loc', type=str,help="the path of Harbor 0.5.0 configuration file")
|
||||
parser.add_argument('--source-version', dest='source_ver', type=str,help="the Harbor instance is to be deployed with notary")
|
||||
parser.add_argument('--target-loc', dest='target_loc', type=str,help="the path of Harbor 1.1.x configuration file")
|
||||
parser.add_argument('--target-version', dest='target_ver', type=str, help="the Harbor instance is to be deployed with notary")
|
||||
upgrade_args = parser.parse_args()
|
||||
|
||||
# NOTE: the script only support to upgrade from 0.5.0.to 1.1.x.
|
||||
def validate():
|
||||
if upgrade_args.source_ver == '0.5.0' and upgrade_args.target_ver == '1.1.x':
|
||||
return
|
||||
raise Exception("Unable to support upgrade from %s to %s" % (upgrade_args.source_ver, upgrade_args.target_ver))
|
||||
|
||||
validate()
|
||||
|
||||
conf = StringIO.StringIO()
|
||||
conf.write("[configuration]\n")
|
||||
conf.write(open(upgrade_args.source_loc).read())
|
||||
conf.seek(0, os.SEEK_SET)
|
||||
rcp = ConfigParser.RawConfigParser()
|
||||
rcp.readfp(conf)
|
||||
|
||||
hostname = rcp.get("configuration", "hostname")
|
||||
ui_url_protocol = rcp.get("configuration", "ui_url_protocol")
|
||||
email_identity = rcp.get("configuration", "email_identity")
|
||||
email_server = rcp.get("configuration", "email_server")
|
||||
email_server_port = rcp.get("configuration", "email_server_port")
|
||||
email_username = rcp.get("configuration", "email_username")
|
||||
email_password = rcp.get("configuration", "email_password")
|
||||
email_from = rcp.get("configuration", "email_from")
|
||||
email_ssl = rcp.get("configuration", "email_ssl")
|
||||
harbor_admin_password = rcp.get("configuration", "harbor_admin_password")
|
||||
auth_mode = rcp.get("configuration", "auth_mode")
|
||||
ldap_url = rcp.get("configuration", "ldap_url")
|
||||
ldap_basedn = rcp.get("configuration", "ldap_basedn")
|
||||
ldap_uid = rcp.get("configuration", "ldap_uid")
|
||||
ldap_scope = rcp.get("configuration", "ldap_scope")
|
||||
db_password = rcp.get("configuration", "db_password")
|
||||
self_registration = rcp.get("configuration", "self_registration")
|
||||
use_compressed_js = rcp.get("configuration", "use_compressed_js")
|
||||
max_job_workers = rcp.get("configuration", "max_job_workers")
|
||||
token_expiration = rcp.get("configuration", "token_expiration")
|
||||
verify_remote_cert = rcp.get("configuration", "verify_remote_cert")
|
||||
customize_crt = rcp.get("configuration", "customize_crt")
|
||||
project_creation_restriction = rcp.get("configuration", "project_creation_restriction")
|
||||
ssl_cert = rcp.get("configuration", "ssl_cert")
|
||||
ssl_cert_key = rcp.get("configuration", "ssl_cert_key")
|
||||
|
||||
def delfile(src):
|
||||
if os.path.isfile(src):
|
||||
try:
|
||||
os.remove(src)
|
||||
print("Clearing the configuration file: %s" % src)
|
||||
except:
|
||||
pass
|
||||
elif os.path.isdir(src):
|
||||
for item in os.listdir(src):
|
||||
itemsrc=os.path.join(src,item)
|
||||
delfile(itemsrc)
|
||||
|
||||
def render(src, dest, **kw):
|
||||
t = Template(open(src, 'r').read())
|
||||
with open(dest, 'w') as f:
|
||||
f.write(t.substitute(**kw))
|
||||
print("Generated configuration file: %s" % dest)
|
||||
|
||||
delfile(upgrade_args.target_loc)
|
||||
|
||||
base_dir = os.path.dirname(__file__)
|
||||
config_template = os.path.join(base_dir, "harbor_1_1_0_template")
|
||||
|
||||
render(config_template,
|
||||
upgrade_args.target_loc,
|
||||
hostname=hostname,
|
||||
ui_url_protocol=ui_url_protocol,
|
||||
db_password=db_password,
|
||||
use_compressed_js=use_compressed_js,
|
||||
max_job_workers=max_job_workers,
|
||||
customize_crt=customize_crt,
|
||||
ssl_cert=ssl_cert,
|
||||
ssl_cert_key=ssl_cert_key,
|
||||
admiral_url='',
|
||||
email_identity=email_identity,
|
||||
email_server=email_server,
|
||||
email_server_port=email_server_port,
|
||||
email_username=email_username,
|
||||
email_password=email_password,
|
||||
email_from=email_from,
|
||||
email_ssl=email_ssl,
|
||||
harbor_admin_password=harbor_admin_password,
|
||||
auth_mode=auth_mode,
|
||||
ldap_url=ldap_url,
|
||||
ldap_basedn=ldap_basedn,
|
||||
ldap_uid=ldap_uid,
|
||||
ldap_scope=ldap_scope,
|
||||
self_registration=self_registration,
|
||||
token_expiration=token_expiration,
|
||||
project_creation_restriction=project_creation_restriction,
|
||||
verify_remote_cert=verify_remote_cert
|
||||
)
|
90
tools/migration/db/migration_harbor/versions/1_5_0.py
Normal file
90
tools/migration/db/migration_harbor/versions/1_5_0.py
Normal file
|
@ -0,0 +1,90 @@
|
|||
# Copyright (c) 2008-2018 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.
|
||||
|
||||
"""1.4.0 to 1.5.0
|
||||
|
||||
Revision ID: 1.5.0
|
||||
Revises:
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '1.5.0'
|
||||
down_revision = '1.4.0'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
from db_meta import *
|
||||
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
Session = sessionmaker()
|
||||
|
||||
def upgrade():
|
||||
"""
|
||||
update schema&data
|
||||
"""
|
||||
bind = op.get_bind()
|
||||
session = Session(bind=bind)
|
||||
|
||||
|
||||
# create table harbor_label
|
||||
HarborLabel.__table__.create(bind)
|
||||
|
||||
# create table harbor_resource_label
|
||||
HarborResourceLabel.__table__.create(bind)
|
||||
|
||||
# create user_group
|
||||
UserGroup.__table__.create(bind)
|
||||
|
||||
# project member
|
||||
op.drop_constraint('project_member_ibfk_1', 'project_member', type_='foreignkey')
|
||||
op.drop_constraint('project_member_ibfk_2', 'project_member', type_='foreignkey')
|
||||
op.drop_constraint('project_member_ibfk_3', 'project_member', type_='foreignkey')
|
||||
op.drop_constraint('PRIMARY', 'project_member', type_='primary')
|
||||
op.drop_index('user_id', 'project_member')
|
||||
op.drop_index('role', 'project_member')
|
||||
op.execute('ALTER TABLE project_member ADD id INT PRIMARY KEY AUTO_INCREMENT;')
|
||||
op.alter_column('project_member', 'user_id', existing_type=sa.Integer, existing_nullable=False, new_column_name='entity_id')
|
||||
op.alter_column('project_member', 'creation_time', existing_type=mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column('project_member', 'update_time', existing_type=mysql.TIMESTAMP, server_default=sa.text("CURRENT_TIMESTAMP"), onupdate=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.add_column('project_member', sa.Column('entity_type', sa.String(1)))
|
||||
|
||||
session.query(ProjectMember).update({
|
||||
ProjectMember.entity_type: 'u'
|
||||
})
|
||||
op.alter_column('project_member', 'entity_type', existing_type=sa.String(1), existing_nullable=True, nullable=False)
|
||||
|
||||
op.create_unique_constraint('unique_project_entity_type', 'project_member', ['project_id', 'entity_id', 'entity_type'])
|
||||
|
||||
# add job_uuid to replicationjob and img_scan_job
|
||||
op.add_column('replication_job', sa.Column('job_uuid', sa.String(64)))
|
||||
op.add_column('img_scan_job', sa.Column('job_uuid', sa.String(64)))
|
||||
|
||||
# add index to replication job
|
||||
op.create_index('poid_status', 'replication_job', ['policy_id', 'status'])
|
||||
|
||||
# add index to img_scan_job
|
||||
op.create_index('idx_status', 'img_scan_job', ['status'])
|
||||
op.create_index('idx_digest', 'img_scan_job', ['digest'])
|
||||
op.create_index('idx_uuid', 'img_scan_job', ['job_uuid'])
|
||||
op.create_index('idx_repository_tag', 'img_scan_job', ['repository', 'tag'])
|
||||
|
||||
session.commit()
|
||||
|
||||
def downgrade():
|
||||
"""
|
||||
Downgrade has been disabled.
|
||||
"""
|
|
@ -1,12 +1,12 @@
|
|||
#!/bin/bash
|
||||
|
||||
export PYTHONPATH=$PYTHONPATH:/harbor-migration
|
||||
export PYTHONPATH=$PYTHONPATH:/harbor-migration/db
|
||||
if [ -z "$DB_USR" -o -z "$DB_PWD" ]; then
|
||||
echo "DB_USR or DB_PWD not set, exiting..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
source ./alembic.tpl > ./alembic.ini
|
||||
source /harbor-migration/db/alembic.tpl > /harbor-migration/db/alembic.ini
|
||||
|
||||
DBCNF="-hlocalhost -u${DB_USR}"
|
||||
|
||||
|
@ -23,42 +23,41 @@ if [[ $1 = "help" || $1 = "h" || $# = 0 ]]; then
|
|||
exit 0
|
||||
fi
|
||||
|
||||
if [[ ( $1 = "up" || $1 = "upgrade" ) && ${SKIP_CONFIRM} != "y" ]]; then
|
||||
echo "Please backup before upgrade."
|
||||
read -p "Enter y to continue updating or n to abort:" ans
|
||||
case $ans in
|
||||
[Yy]* )
|
||||
;;
|
||||
[Nn]* )
|
||||
exit 0
|
||||
;;
|
||||
* ) echo "illegal answer: $ans. Upgrade abort!!"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
fi
|
||||
# if [[ ( $1 = "up" || $1 = "upgrade" ) && ${SKIP_CONFIRM} != "y" ]]; then
|
||||
# echo "Please backup before upgrade."
|
||||
# read -p "Enter y to continue updating or n to abort:" ans
|
||||
# case $ans in
|
||||
# [Yy]* )
|
||||
# ;;
|
||||
# [Nn]* )
|
||||
# exit 0
|
||||
# ;;
|
||||
# * ) echo "illegal answer: $ans. Upgrade abort!!"
|
||||
# exit 1
|
||||
# ;;
|
||||
# esac
|
||||
# fi
|
||||
|
||||
echo 'Trying to start mysql server...'
|
||||
chown -R 10000:10000 /var/lib/mysql
|
||||
mysqld &
|
||||
echo 'Waiting for MySQL start...'
|
||||
for i in {60..0}; do
|
||||
mysqladmin -u$DB_USR -p$DB_PWD processlist >/dev/null 2>&1
|
||||
if [ $? = 0 ]; then
|
||||
break
|
||||
fi
|
||||
echo 'Waiting for MySQL start...'
|
||||
sleep 1
|
||||
done
|
||||
if [ "$i" = 0 ]; then
|
||||
echo "timeout. Can't run mysql server."
|
||||
if [[ $1 = "test" ]]; then
|
||||
echo "test failed."
|
||||
echo "DB test failed."
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
if [[ $1 = "test" ]]; then
|
||||
echo "test passed."
|
||||
echo "DB test passed."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
@ -84,39 +83,40 @@ up|upgrade)
|
|||
mysql $DBCNF -e "insert into registry.alembic_version values ('0.1.1')"
|
||||
fi
|
||||
fi
|
||||
alembic -c ./alembic.ini current
|
||||
alembic -c ./alembic.ini upgrade ${VERSION}
|
||||
alembic -c /harbor-migration/db/alembic.ini current
|
||||
alembic -c /harbor-migration/db/alembic.ini upgrade ${VERSION}
|
||||
rc="$?"
|
||||
alembic -c ./alembic.ini current
|
||||
alembic -c /harbor-migration/db/alembic.ini current
|
||||
echo "Upgrade performed."
|
||||
echo $rc
|
||||
exit $rc
|
||||
;;
|
||||
backup)
|
||||
echo "Performing backup..."
|
||||
mysqldump $DBCNF --add-drop-database --databases registry > ./backup/registry.sql
|
||||
mysqldump $DBCNF --add-drop-database --databases registry > /harbor-migration/backup/registry.sql
|
||||
rc="$?"
|
||||
echo "Backup performed."
|
||||
exit $rc
|
||||
;;
|
||||
export)
|
||||
echo "Performing export..."
|
||||
./export --dbuser ${DB_USR} --dbpwd ${DB_PWD} --exportpath ${EXPORTPATH}
|
||||
/harbor-migration/db/export --dbuser ${DB_USR} --dbpwd ${DB_PWD} --exportpath ${EXPORTPATH}
|
||||
rc="$?"
|
||||
echo "Export performed."
|
||||
echo $rc
|
||||
exit $rc
|
||||
;;
|
||||
mapprojects)
|
||||
echo "Performing map projects..."
|
||||
./mapprojects --dbuser ${DB_USR} --dbpwd ${DB_PWD} --mapprojectsfile ${MAPPROJECTFILE}
|
||||
/harbor-migration/db/mapprojects --dbuser ${DB_USR} --dbpwd ${DB_PWD} --mapprojectsfile ${MAPPROJECTFILE}
|
||||
rc="$?"
|
||||
echo "Map projects performed."
|
||||
echo $rc
|
||||
exit $rc
|
||||
;;
|
||||
restore)
|
||||
echo "Performing restore..."
|
||||
mysql $DBCNF < ./backup/registry.sql
|
||||
mysql $DBCNF < /harbor-migration/backup/registry.sql
|
||||
rc="$?"
|
||||
echo "Restore performed."
|
||||
exit $rc
|
||||
;;
|
||||
*)
|
||||
echo "unknown option"
|
||||
|
|
4
tools/migration/docker-entrypoint.sh
Executable file
4
tools/migration/docker-entrypoint.sh
Executable file
|
@ -0,0 +1,4 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
python ./migrator.py "$@"
|
210
tools/migration/migrator.py
Normal file
210
tools/migration/migrator.py
Normal file
|
@ -0,0 +1,210 @@
|
|||
import abc
|
||||
import subprocess
|
||||
from optparse import OptionParser
|
||||
from shutil import copyfile
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
RC_VALIDATE = 101
|
||||
RC_UP = 102
|
||||
RC_DOWN = 103
|
||||
RC_BACKUP = 104
|
||||
RC_RESTORE = 105
|
||||
RC_UNNKNOW_TYPE = 106
|
||||
RC_GEN = 110
|
||||
|
||||
class DBMigrator():
|
||||
|
||||
def __init__(self, target):
|
||||
self.target = target
|
||||
self.script = "./db/run.sh"
|
||||
|
||||
def backup(self):
|
||||
return run_cmd(self.script + " backup") == 0
|
||||
|
||||
def restore(self):
|
||||
return run_cmd(self.script + " restore") == 0
|
||||
|
||||
def up(self):
|
||||
cmd = self.script + " up"
|
||||
if self.target != '':
|
||||
cmd = cmd + " " + self.target
|
||||
return run_cmd(cmd) == 0
|
||||
|
||||
def validate(self):
|
||||
return run_cmd(self.script + " test") == 0
|
||||
|
||||
class CfgMigrator():
|
||||
|
||||
def __init__(self, target, output):
|
||||
self.target = target
|
||||
self.output = output
|
||||
self.cfg_path = "/harbor-migration/harbor-cfg/harbor.cfg"
|
||||
self.backup_path = "/harbor-migration/backup"
|
||||
self.output_path = "/harbor-migration/output"
|
||||
|
||||
def backup(self):
|
||||
try:
|
||||
copyfile(self.cfg_path, self.backup_path+"/harbor.cfg")
|
||||
print ("Success to backup harbor.cfg.")
|
||||
return True
|
||||
except Exception, e:
|
||||
print ("Back up error: %s" % str(e))
|
||||
return False
|
||||
|
||||
def restore(self):
|
||||
if not os.path.exists(self.backup_path+"/harbor.cfg"):
|
||||
print ("Unable to restore as there is no harbor.cfg")
|
||||
return False
|
||||
try:
|
||||
copyfile(self.backup_path+"/harbor.cfg", self.cfg_path)
|
||||
print ("Success to restore harbor.cfg.")
|
||||
return True
|
||||
except Exception, e:
|
||||
print ("Restore error: %s" % str(e))
|
||||
return False
|
||||
|
||||
def up(self):
|
||||
if not os.path.exists(self.cfg_path):
|
||||
print ("Skip cfg up as no harbor.cfg in the path.")
|
||||
return True
|
||||
|
||||
if self.output and os.path.isdir(self.output_path):
|
||||
cmd = "python ./cfg/run.py --input " + self.cfg_path + " --output " + self.output_path + "/harbor.cfg"
|
||||
else:
|
||||
print ("The path of the migrated harbor.cfg is not set, the input file will be overwritten.")
|
||||
cmd = "python ./cfg/run.py --input " + self.cfg_path
|
||||
|
||||
if self.target != '':
|
||||
cmd = cmd + " --target " + self.target
|
||||
return run_cmd(cmd) == 0
|
||||
|
||||
def validate(self):
|
||||
if not os.path.exists(self.cfg_path):
|
||||
print ("Unable to loacte the harbor.cfg, please check.")
|
||||
return False
|
||||
print ("Success to validate harbor.cfg.")
|
||||
return True
|
||||
|
||||
class Parameters(object):
|
||||
def __init__(self):
|
||||
self.db_user = os.getenv('DB_USR', '')
|
||||
self.db_pwd = os.getenv('DB_PWD', '')
|
||||
self.skip_confirm = os.getenv('SKIP_CONFIRM', 'n')
|
||||
self.output = False
|
||||
self.is_migrate_db = True
|
||||
self.is_migrate_cfg = True
|
||||
self.target_version = ''
|
||||
self.action = ''
|
||||
self.init_from_input()
|
||||
|
||||
def is_action(self, action):
|
||||
if action == "test" or action == "backup" or action == "restore" or action == "up":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def parse_input(self):
|
||||
argv_len = len(sys.argv[1:])
|
||||
last_argv = sys.argv[argv_len:][0]
|
||||
if not self.is_action(last_argv):
|
||||
print ("Fail to parse input: the last parameter should in test:up:restore:backup")
|
||||
sys.exit(RC_GEN)
|
||||
|
||||
if last_argv == 'up':
|
||||
if self.skip_confirm != 'y':
|
||||
if not pass_skip_confirm():
|
||||
sys.exit(RC_GEN)
|
||||
|
||||
if argv_len == 1:
|
||||
return (True, True, '', False, last_argv)
|
||||
|
||||
parser = argparse.ArgumentParser(description='migrator of harbor')
|
||||
parser.add_argument('--db', action="store_true", dest='is_migrate_db', required=False, default=False, help='The flag to upgrade db.')
|
||||
parser.add_argument('--cfg', action="store_true", dest='is_migrate_cfg', required=False, default=False, help='The flag to upgrede cfg.')
|
||||
parser.add_argument('--version', action="store", dest='target_version', required=False, default='', help='The target version that the harbor will be migrated to.')
|
||||
parser.add_argument('--output', action="store_true", dest='output', required=False, default=False, help='The path of the migrated harbor.cfg, if not set the input file will be overwritten.')
|
||||
|
||||
args = parser.parse_args(sys.argv[1:argv_len])
|
||||
args.action = last_argv
|
||||
return (args.is_migrate_db, args.is_migrate_cfg, args.target_version, args.output, args.action)
|
||||
|
||||
def init_from_input(self):
|
||||
(self.is_migrate_db, self.is_migrate_cfg, self.target_version, self.output, self.action) = self.parse_input()
|
||||
|
||||
def run_cmd(cmd):
|
||||
return os.system(cmd)
|
||||
|
||||
def pass_skip_confirm():
|
||||
valid = {"yes": True, "y": True, "ye": True, "no": False, "n": False}
|
||||
message = "Please backup before upgrade, \nEnter y to continue updating or n to abort: "
|
||||
while True:
|
||||
sys.stdout.write(message)
|
||||
choice = raw_input().lower()
|
||||
if choice == '':
|
||||
return False
|
||||
elif choice in valid:
|
||||
return valid[choice]
|
||||
else:
|
||||
sys.stdout.write("Please respond with 'yes' or 'no' "
|
||||
"(or 'y' or 'n').\n")
|
||||
|
||||
def main():
|
||||
commandline_input = Parameters()
|
||||
|
||||
db_migrator = DBMigrator(commandline_input.target_version)
|
||||
cfg_migrator = CfgMigrator(commandline_input.target_version, commandline_input.output)
|
||||
|
||||
try:
|
||||
# test
|
||||
if commandline_input.action == "test":
|
||||
if commandline_input.is_migrate_db:
|
||||
if not db_migrator.validate():
|
||||
print ("Fail to validate: please make sure your DB auth is correct.")
|
||||
sys.exit(RC_VALIDATE)
|
||||
|
||||
if commandline_input.is_migrate_cfg:
|
||||
if not cfg_migrator.validate():
|
||||
print ("Fail to validate: please make sure your cfg path is correct.")
|
||||
sys.exit(RC_VALIDATE)
|
||||
|
||||
# backup
|
||||
elif commandline_input.action == "backup":
|
||||
if commandline_input.is_migrate_db:
|
||||
if not db_migrator.backup():
|
||||
sys.exit(RC_BACKUP)
|
||||
|
||||
if commandline_input.is_migrate_cfg:
|
||||
if not cfg_migrator.backup():
|
||||
sys.exit(RC_BACKUP)
|
||||
|
||||
# up
|
||||
elif commandline_input.action == "up":
|
||||
if commandline_input.is_migrate_db:
|
||||
if not db_migrator.up():
|
||||
sys.exit(RC_UP)
|
||||
|
||||
if commandline_input.is_migrate_cfg:
|
||||
if not cfg_migrator.up():
|
||||
sys.exit(RC_UP)
|
||||
|
||||
# restore
|
||||
elif commandline_input.action == "restore":
|
||||
if commandline_input.is_migrate_db:
|
||||
if not db_migrator.restore():
|
||||
sys.exit(RC_RESTORE)
|
||||
|
||||
if commandline_input.is_migrate_cfg:
|
||||
if not cfg_migrator.restore():
|
||||
sys.exit(RC_RESTORE)
|
||||
|
||||
else:
|
||||
print ("Unknow action type: " + str(commandline_input.action))
|
||||
sys.exit(RC_UNNKNOW_TYPE)
|
||||
except Exception as ex:
|
||||
print ("Migrator fail to execute, err: " + ex.message)
|
||||
sys.exit(RC_GEN)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
Loading…
Reference in New Issue
Block a user