Merge branch 'master' into job_service

This commit is contained in:
Steven Zou 2018-04-03 16:28:26 +08:00
commit 44808650be
54 changed files with 913 additions and 378 deletions

View File

@ -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),

View File

@ -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;}
`;

View File

@ -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;

View File

@ -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;}

View File

@ -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;
});
});

View File

@ -30,6 +30,7 @@ import {Label} from "../service/interface";
export class LabelPieceComponent implements OnInit {
@Input() label: Label;
@Input() labelWidth: number;
ngOnInit(): void {
}

View File

@ -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;}
`;

View File

@ -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,

View File

@ -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);
});

View File

@ -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>

View File

@ -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");
});
}));
});

View File

@ -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: "--",

View File

@ -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;
}
`;

View File

@ -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>&nbsp;{{'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>&nbsp;{{'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>&nbsp;{{'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>

View File

@ -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);
});

View File

@ -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",

View File

@ -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;
}
}

View File

@ -5,6 +5,9 @@
.container-override {
position: relative !important;
}
.content-container{
position: relative;
}
.start-content-padding {
padding: 0px !important;

View File

@ -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 {

View File

@ -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>

View File

@ -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');
}
}

View File

@ -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]
})

View 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;
}
}

View 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>

View File

@ -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;}

View File

@ -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>

View File

@ -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]);
}

View File

@ -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 = {};

View File

@ -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

View File

@ -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.",

View File

@ -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.",

View File

@ -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.",

View File

@ -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": "错误请求, 操作无法完成。",

View File

@ -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);
}

View File

@ -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

View File

@ -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

View File

@ -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}")]

View File

@ -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

View File

@ -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}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View 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"]

View File

@ -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

View File

@ -10,6 +10,7 @@ import utils
import importlib
import glob
import shutil
import sys
def main():
target_version = '1.5.0'

View File

@ -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

View File

@ -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'),)

View File

@ -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************************
#############

View File

@ -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
)

View 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.
"""

View File

@ -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"

View File

@ -0,0 +1,4 @@
#!/bin/bash
set -e
python ./migrator.py "$@"

210
tools/migration/migrator.py Normal file
View 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()