From b2fb33ba0d2ec8d281006026a9dd87b06c51cbae Mon Sep 17 00:00:00 2001 From: pfh Date: Wed, 7 Mar 2018 10:03:08 +0800 Subject: [PATCH 01/21] Modify replication rule from page to dialog #4296 --- src/ui_ng/lib/README.md | 5 +- .../create-edit-rule.component.css.ts | 73 +- .../create-edit-rule.component.html.ts | 204 ++-- .../create-edit-rule.component.spec.ts | 173 ++- .../create-edit-rule.component.ts | 1022 +++++++++++------ .../lib/src/endpoint/endpoint.component.ts | 14 +- src/ui_ng/lib/src/index.ts | 1 + .../list-replication-rule.component.html.ts | 3 +- .../list-replication-rule.component.spec.ts | 121 +- .../list-replication-rule.component.ts | 50 +- .../lib/src/project-policy-config/project.ts | 24 +- .../replication/replication.component.html.ts | 3 +- .../replication/replication.component.spec.ts | 243 ++-- .../src/replication/replication.component.ts | 32 +- src/ui_ng/lib/src/service/interface.ts | 47 +- src/ui_ng/lib/src/service/project.service.ts | 26 +- .../lib/src/service/replication.service.ts | 18 +- src/ui_ng/lib/src/shared/shared.module.ts | 6 +- src/ui_ng/package.json | 2 +- src/ui_ng/src/app/harbor-routing.module.ts | 16 - .../replication-page.component.html | 2 +- .../replication/replication-page.component.ts | 24 +- .../list-project-model.component.css | 5 - .../list-project-model.component.html | 33 - .../list-project-model.component.ts | 181 --- .../replication-rule.component.ts | 590 ---------- .../replication-rule/replication-rule.css | 43 - .../replication-rule/replication-rule.html | 128 --- .../replication-rule.service.ts | 75 -- .../replication-rule/replication-rule.ts | 60 - .../src/app/replication/replication.module.ts | 9 +- .../total-replication-page.component.html | 2 +- .../total-replication-page.component.ts | 16 +- .../leaving-new-rule-deactivate.service.ts | 65 -- src/ui_ng/src/app/shared/shared.module.ts | 2 - src/ui_ng/src/i18n/lang/en-us-lang.json | 4 +- src/ui_ng/src/i18n/lang/es-es-lang.json | 4 +- src/ui_ng/src/i18n/lang/zh-cn-lang.json | 4 +- 38 files changed, 1306 insertions(+), 2024 deletions(-) delete mode 100644 src/ui_ng/src/app/replication/replication-rule/list-project-model/list-project-model.component.css delete mode 100644 src/ui_ng/src/app/replication/replication-rule/list-project-model/list-project-model.component.html delete mode 100644 src/ui_ng/src/app/replication/replication-rule/list-project-model/list-project-model.component.ts delete mode 100644 src/ui_ng/src/app/replication/replication-rule/replication-rule.component.ts delete mode 100644 src/ui_ng/src/app/replication/replication-rule/replication-rule.css delete mode 100644 src/ui_ng/src/app/replication/replication-rule/replication-rule.html delete mode 100644 src/ui_ng/src/app/replication/replication-rule/replication-rule.service.ts delete mode 100644 src/ui_ng/src/app/replication/replication-rule/replication-rule.ts delete mode 100644 src/ui_ng/src/app/shared/route/leaving-new-rule-deactivate.service.ts diff --git a/src/ui_ng/lib/README.md b/src/ui_ng/lib/README.md index 89e903d13..47cd19d79 100644 --- a/src/ui_ng/lib/README.md +++ b/src/ui_ng/lib/README.md @@ -74,13 +74,14 @@ Use **withTitle** to set whether self-contained a header with title or not. Defa Support two different display scope mode: under specific project or whole system. If **projectId** is set to the id of specified project, then only show the replication rules bound with the project. Otherwise, show all the rules of the whole system. +On specific project mode, without need projectId, but also need to provide projectName for display. **withReplicationJob** is used to determine whether or not show the replication jobs which are relevant with the selected replication rule. -**readonly** is to disable all the create/edit/delete actions. +**isSystemAdmin** is for judgment if user has administrator privilege, if true, user can do the create/edit/delete/replicate actions. ``` - + ``` * **Endpoint Management View** diff --git a/src/ui_ng/lib/src/create-edit-rule/create-edit-rule.component.css.ts b/src/ui_ng/lib/src/create-edit-rule/create-edit-rule.component.css.ts index 83aaea83c..c0e232338 100644 --- a/src/ui_ng/lib/src/create-edit-rule/create-edit-rule.component.css.ts +++ b/src/ui_ng/lib/src/create-edit-rule/create-edit-rule.component.css.ts @@ -1,5 +1,70 @@ export const CREATE_EDIT_RULE_STYLE: string = ` -.form-group-label-override { - font-size: 14px; - font-weight: 400; -}`; \ No newline at end of file +/** + * Created by pengf on 9/28/2017. + */ + +.select{ + width: 186px; +} +.select .optionMore{ + background-color: #bfbaba; + height: 1.6em; + font-size: 1.2em; + cursor: pointer; + text-align: center; +} +.hideFilter{ display: none;} +h4{ + color: #666; +} +.colorRed{color: red;} +.colorRed a{text-decoration: underline;color: #007CBB;} +.alertLabel{display:block; margin-top:0; line-height:1em; font-size:12px;} + +.inputWidth{width: 270px;} +.endpointSelect{ width: 270px; margin-right: 20px;} +.filterSelect{width: 315px;} +.filterSelect clr-icon{margin-left: 15px;} +.filterSelect label{width: 136px;} +.filterSelect label input{width: 100%;} +.cursor{cursor: pointer;} +.pull-left{float: left;} +.padLeft0{padding-left: 0;} +.floatSetPar{display: inline-block; width: 120px;margin-right: 10px;} +.floatSet {display: inline-block; width: 82px;margin-right: 4px;} +.form-group{ min-height: 36px;} + +.projectInput{float: left;position: relative;} +.switchIcon{width:20px;height:20px; margin-top: 10px;margin-left: 10px; cursor: pointer;} +.addEndpoint{ margin-top: .25em !important;padding-left:2px;padding-right:2px;min-width:58px;margin-right:0} +.shadow{position: absolute;top: 8px;} +.is-solid{cursor: pointer;} +.selectBox{ + position: absolute; + width: 100%; + height: auto; + margin-top:-0.25rem; + border: 1px solid #ccc; + background-color: white; + border: 1px solid rgba(0,0,0,.15); + border-right-width: 2px; + border-bottom-width: 2px; + border-radius: 6px; + box-shadow: 0 5px 10px rgba(0,0,0,.2); + z-index: 100; +} +.selectBox ul li{ + list-style: none; + padding: 3px 20px +} +.selectBox ul li:hover{ + color: #262626; + background-image: linear-gradient(180deg,#f5f5f5 0,#e8e8e8); + background-repeat: repeat-x; +} +.form-group-override{ + padding-left: 170px !important; +} +.form-group>label:first-child{font-size:14px; width:6.5rem;} +.goLink{color:blue; border-bottom:1px solid blue; line-height:14px; cursor:pointer;} +`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/create-edit-rule/create-edit-rule.component.html.ts b/src/ui_ng/lib/src/create-edit-rule/create-edit-rule.component.html.ts index 046f9587b..8817523d7 100644 --- a/src/ui_ng/lib/src/create-edit-rule/create-edit-rule.component.html.ts +++ b/src/ui_ng/lib/src/create-edit-rule/create-edit-rule.component.html.ts @@ -1,100 +1,134 @@ export const CREATE_EDIT_RULE_TEMPLATE: string = ` - - + + +
+ +
+
`; diff --git a/src/ui_ng/lib/src/create-edit-rule/create-edit-rule.component.spec.ts b/src/ui_ng/lib/src/create-edit-rule/create-edit-rule.component.spec.ts index 5485cdd1a..06118b582 100644 --- a/src/ui_ng/lib/src/create-edit-rule/create-edit-rule.component.spec.ts +++ b/src/ui_ng/lib/src/create-edit-rule/create-edit-rule.component.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; @@ -25,52 +25,55 @@ import { JobLogDefaultService } from '../service/index'; import { EndpointService, EndpointDefaultService } from '../service/endpoint.service'; +import {ProjectDefaultService, ProjectService} from "../service/project.service"; import { JobLogViewerComponent } from '../job-log-viewer/job-log-viewer.component'; +import {Project} from "../project-policy-config/project"; describe('CreateEditRuleComponent (inline template)', ()=>{ let mockRules: ReplicationRule[] = [ { "id": 1, - "project_id": 1, - "project_name": "library", - "target_id": 1, - "target_name": "target_01", "name": "sync_01", - "enabled": 0, "description": "", - "cron_str": "", - "error_job_count": 2, - "deleted": 0 - }, - { - "id": 2, - "project_id": 1, - "project_name": "library", - "target_id": 3, - "target_name": "target_02", - "name": "sync_02", - "enabled": 1, - "description": "", - "cron_str": "", - "error_job_count": 1, - "deleted": 0 - }, - { - "id": 3, - "project_id": 1, - "project_name": "library", - "target_id": 2, - "target_name": "target_03", - "name": "sync_03", - "enabled": 0, - "description": "", - "cron_str": "", - "error_job_count": 0, - "deleted": 0 - } - ]; - + "projects": [{ "project_id": 1, + "owner_id": 0, + "name": 'project_01', + "creation_time": '', + "deleted": 0, + "owner_name": '', + "togglable": false, + "update_time": '', + "current_user_role_id": 0, + "repo_count": 0, + "has_project_admin_role": false, + "is_member": false, + "role_name": '', + "metadata": { + "public": '', + "enable_content_trust": '', + "prevent_vul": '', + "severity": '', + "auto_scan": '', + } + }], + "targets": [{ + "id": 1, + "endpoint": "https://10.117.4.151", + "name": "target_01", + "username": "admin", + "password": "", + "insecure": false, + "type": 0 + }], + "trigger": { + "kind": "Manual", + "schedule_param": null + }, + "filters": [], + "replicate_existing_image_now": false, + "replicate_deletion": false, + }] let mockJobs: ReplicationJobItem[] = [ { "id": 1, @@ -144,17 +147,68 @@ describe('CreateEditRuleComponent (inline template)', ()=>{ let mockRule: ReplicationRule = { "id": 1, - "project_id": 1, - "project_name": "library", - "target_id": 1, - "target_name": "target_01", "name": "sync_01", - "enabled": 0, "description": "", - "cron_str": "", - "error_job_count": 2, - "deleted": 0 - }; + "projects": [{ "project_id": 1, + "owner_id": 0, + "name": 'project_01', + "creation_time": '', + "deleted": 0, + "owner_name": '', + "togglable": false, + "update_time": '', + "current_user_role_id": 0, + "repo_count": 0, + "has_project_admin_role": false, + "is_member": false, + "role_name": '', + "metadata": { + "public": '', + "enable_content_trust": '', + "prevent_vul": '', + "severity": '', + "auto_scan": '', + } + }], + "targets": [{ + "id": 1, + "endpoint": "https://10.117.4.151", + "name": "target_01", + "username": "admin", + "password": "", + "insecure": false, + "type": 0 + }], + "trigger": { + "kind": "Manual", + "schedule_param": null + }, + "filters": [], + "replicate_existing_image_now": false, + "replicate_deletion": false, + } + let mockProjects: Project[] = [ + { "project_id": 1, + "owner_id": 0, + "name": 'project_01', + "creation_time": '', + "deleted": 0, + "owner_name": '', + "togglable": false, + "update_time": '', + "current_user_role_id": 0, + "repo_count": 0, + "has_project_admin_role": false, + "is_member": false, + "role_name": '', + "metadata": { + "public": '', + "enable_content_trust": '', + "prevent_vul": '', + "severity": '', + "auto_scan": '', + } + }]; let fixture: ComponentFixture; let fixtureCreate: ComponentFixture; @@ -164,7 +218,7 @@ describe('CreateEditRuleComponent (inline template)', ()=>{ let replicationService: ReplicationService; let endpointService: EndpointService; - + let spyRules: jasmine.Spy; let spyOneRule: jasmine.Spy; @@ -172,12 +226,11 @@ describe('CreateEditRuleComponent (inline template)', ()=>{ let spyEndpoint: jasmine.Spy; let config: IServiceConfig = { - replicationRuleEndpoint: '/api/policies/replication/testing', replicationJobEndpoint: '/api/jobs/replication/testing', targetBaseEndpoint: '/api/targets/testing' }; - beforeEach(async(()=>{ + beforeEach(async(() =>{ TestBed.configureTestingModule({ imports: [ SharedModule, @@ -198,6 +251,7 @@ describe('CreateEditRuleComponent (inline template)', ()=>{ { provide: SERVICE_CONFIG, useValue: config }, { provide: ReplicationService, useClass: ReplicationDefaultService }, { provide: EndpointService, useClass: EndpointDefaultService }, + { provide: ProjectService, useClass: ProjectDefaultService }, { provide: JobLogService, useClass: JobLogDefaultService } ] }); @@ -205,28 +259,27 @@ describe('CreateEditRuleComponent (inline template)', ()=>{ beforeEach(()=>{ fixture = TestBed.createComponent(ReplicationComponent); - + fixtureCreate = TestBed.createComponent(CreateEditRuleComponent); comp = fixture.componentInstance; + compCreate = fixtureCreate.componentInstance; comp.projectId = 1; comp.search.ruleId = 1; replicationService = fixture.debugElement.injector.get(ReplicationService); - + + + endpointService = fixtureCreate.debugElement.injector.get(EndpointService) ; + spyRules = spyOn(replicationService, 'getReplicationRules').and.returnValues(Promise.resolve(mockRules)); spyOneRule = spyOn(replicationService, 'getReplicationRule').and.returnValue(Promise.resolve(mockRule)); spyJobs = spyOn(replicationService, 'getJobs').and.returnValues(Promise.resolve(mockJob)); - fixture.detectChanges(); - }); - beforeEach(()=>{ - fixtureCreate = TestBed.createComponent(CreateEditRuleComponent); - - compCreate = fixtureCreate.componentInstance; - compCreate.projectId = 1; - endpointService = fixtureCreate.debugElement.injector.get(EndpointService); spyEndpoint = spyOn(endpointService, 'getEndpoints').and.returnValues(Promise.resolve(mockEndpoints)); + fixture.detectChanges(); + + }); it('Should open creation modal and load endpoints', async(()=>{ diff --git a/src/ui_ng/lib/src/create-edit-rule/create-edit-rule.component.ts b/src/ui_ng/lib/src/create-edit-rule/create-edit-rule.component.ts index 64c367732..f15a8a810 100644 --- a/src/ui_ng/lib/src/create-edit-rule/create-edit-rule.component.ts +++ b/src/ui_ng/lib/src/create-edit-rule/create-edit-rule.component.ts @@ -11,437 +11,699 @@ // 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. -import { Component, Input, Output, EventEmitter, ViewChild, AfterViewChecked } from '@angular/core'; - -import { NgForm } from '@angular/forms'; - -import { ReplicationService } from '../service/replication.service'; -import { EndpointService } from '../service/endpoint.service'; - -import { ErrorHandler } from '../error-handler/error-handler'; -import { ActionType } from '../shared/shared.const'; - +import {Component, OnInit, OnDestroy, ViewChild, ChangeDetectorRef, Input, EventEmitter, Output} from '@angular/core'; +import {Filter, ReplicationRule, Endpoint} from "../service/interface"; +import {Subject} from "rxjs/Subject"; +import {Subscription} from "rxjs/Subscription"; +import {FormArray, FormBuilder, FormGroup, Validators} from "@angular/forms"; +import {CreateEditEndpointComponent} from "../create-edit-endpoint/create-edit-endpoint.component"; +import {Router, ActivatedRoute} from "@angular/router"; +import {compareValue, isEmptyObject, toPromise} from "../utils"; import { InlineAlertComponent } from '../inline-alert/inline-alert.component'; +import {ReplicationService} from "../service/replication.service"; +import {CREATE_EDIT_RULE_TEMPLATE} from "./create-edit-rule.component.html"; +import {CREATE_EDIT_RULE_STYLE} from "./create-edit-rule.component.css"; +import {ErrorHandler} from "../error-handler/error-handler"; +import {TranslateService} from "@ngx-translate/core"; +import {EndpointService} from "../service/endpoint.service"; +import {ProjectService} from "../service/project.service"; +import {Project} from "../project-policy-config/project"; -import { ReplicationRule } from '../service/interface'; -import { Endpoint } from '../service/interface'; +const ONE_HOUR_SECONDS = 3600; +const ONE_DAY_SECONDS: number = 24 * ONE_HOUR_SECONDS; -import { TranslateService } from '@ngx-translate/core'; - -import { CREATE_EDIT_RULE_STYLE } from './create-edit-rule.component.css'; -import { CREATE_EDIT_RULE_TEMPLATE } from './create-edit-rule.component.html'; - -import { toPromise } from '../utils'; - -/** - * Rule form model. - */ -export interface CreateEditRule { - ruleId?: number | string; - name?: string; - description?: string; - enable?: boolean; - endpointId?: number | string; - endpointName?: string; - endpointUrl?: string; - username?: string; - password?: string; - insecure?: boolean; -} - -const FAKE_PASSWORD: string = 'ywJZnDTM'; - -@Component({ - selector: 'create-edit-rule', +@Component ({ + selector: 'hbr-create-edit-rule', template: CREATE_EDIT_RULE_TEMPLATE, - styles: [ CREATE_EDIT_RULE_STYLE ] + styles: [CREATE_EDIT_RULE_STYLE] + }) -export class CreateEditRuleComponent implements AfterViewChecked { - modalTitle: string; +export class CreateEditRuleComponent implements OnInit, OnDestroy { + _localTime: Date = new Date(); + targetList: Endpoint[] = []; + projectList: Project[] = []; + selectedProjectList: Project[] = []; + isFilterHide = false; + weeklySchedule: boolean; + isScheduleOpt: boolean; + isImmediate = false; + noProjectInfo = ""; + noEndpointInfo = ""; + noSelectedProject = true; + noSelectedEndpoint = true; + filterCount = 0; + triggerNames: string[] = ['Manual', 'Immediate', 'Scheduled']; + scheduleNames: string[] = ['Daily', 'Weekly']; + weekly: string[] = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; + filterSelect: string[] = ['repository', 'tag']; + ruleNameTooltip = 'TOOLTIP.EMPTY'; + headerTitle = 'REPLICATION.ADD_POLICY'; + createEditRuleOpened: boolean; - createEditRule: CreateEditRule = this.initCreateEditRule; - initVal: CreateEditRule = this.initCreateEditRule; - - actionType: ActionType; - - isCreateEndpoint: boolean; + filterListData: {[key: string]: any}[] = []; + inProgress = false; + inNameChecking = false; + isRuleNameExist = false; + nameChecker: Subject = new Subject(); + proNameChecker: Subject = new Subject(); + firstClick = 0; + policyId: number; + + confirmSub: Subscription; + ruleForm: FormGroup; + copyUpdateForm: ReplicationRule; + @Input() projectId: number; + @Input() projectName: string; - @Output() reload = new EventEmitter(); - - endpoints: Endpoint[]; - - pingTestMessage: string; - testOngoing: boolean; - pingStatus: boolean; - - btnAbled:boolean; - - - ruleForm: NgForm; - - staticBackdrop: boolean = true; - closable: boolean = false; - - @ViewChild('ruleForm') - currentForm: NgForm; - - hasChanged: boolean; - - editable: boolean; - - get initCreateEditRule(): CreateEditRule { - return { - endpointId: '', - name: '', - enable: false, - description: '', - endpointName: '', - endpointUrl: '', - username: '', - password: '', - insecure: false - }; - } - - get initReplicationRule(): ReplicationRule { - return { - project_id: '', - project_name: '', - target_id: '', - target_name: '', - enabled: 0, - description: '', - cron_str: '', - error_job_count: 0, - deleted: 0 - }; - } - - get initEndpoint(): Endpoint { - return { - endpoint: '', - name: '', - username: '', - password: '', - insecure: false, - type: 0 - }; - } + @Output() goToRegistry = new EventEmitter(); + @Output() reload = new EventEmitter(); @ViewChild(InlineAlertComponent) inlineAlert: InlineAlertComponent; - get readonly(): boolean { - return this.actionType === ActionType.EDIT && (this.createEditRule.enable || false); + emptyProject = { + project_id: -1, + name: '', } - - get untoggleable(): boolean { - return this.actionType === ActionType.EDIT && (this.initVal.enable || false); + emptyEndpoint = { + id: -1, + endpoint: '', + name: '', + username: '', + password: '', + insecure: true, + type: 0, } - - - get showNewDestination(): boolean { - return this.actionType === ActionType.ADD_NEW || (!this.createEditRule.enable || false); - } - get connectAbled():boolean{ - return !this.createEditRule.endpointId && !this.isCreateEndpoint; - - } - constructor( - private replicationService: ReplicationService, - private endpointService: EndpointService, - private errorHandler: ErrorHandler, - private translateService: TranslateService) {} - - prepareTargets(endpointId?: number | string) { + private fb: FormBuilder, + private repService: ReplicationService, + private endpointService: EndpointService, + private errorHandler: ErrorHandler, + private proService: ProjectService, + private translateService: TranslateService, + public ref: ChangeDetectorRef) { + this.createForm(); + } + + baseFilterData(name: string, option: string[], state: boolean) { + return { + name: name, + options: option, + state: state, + isValid: true + }; + } + + ngOnInit(): void { toPromise(this.endpointService .getEndpoints()) - .then(endpoints=>{ - this.endpoints = endpoints; - if(this.endpoints && this.endpoints.length > 0) { - let initialEndpoint: Endpoint | undefined; - (endpointId) ? initialEndpoint = this.endpoints.find(t=>t.id===endpointId) : initialEndpoint = this.endpoints[0]; - if(!initialEndpoint) { - return; - } - this.createEditRule.endpointId = initialEndpoint.id; - this.createEditRule.endpointName = initialEndpoint.name; - this.createEditRule.endpointUrl = initialEndpoint.endpoint; - this.createEditRule.username = initialEndpoint.username; - this.createEditRule.insecure = initialEndpoint.insecure; - this.createEditRule.password = FAKE_PASSWORD; + .then(targets => { + this.targetList = targets || []; + }).catch((error: any) => this.errorHandler.error(error)); - this.initVal.endpointId = this.createEditRule.endpointId; - this.initVal.endpointUrl = this.createEditRule.endpointUrl; - this.initVal.username = this.createEditRule.username; - this.initVal.password = this.createEditRule.password; - this.initVal.insecure = this.createEditRule.insecure; + if (!this.projectId) { + toPromise(this.proService.listProjects("", undefined)) + .then(targets => { + this.projectList = targets || []; + }).catch(error => this.errorHandler.error(error)); + } + + this.nameChecker.debounceTime(500).distinctUntilChanged().subscribe((ruleName: string) => { + this.isRuleNameExist = false; + this.inNameChecking = true; + toPromise(this.repService.getReplicationRules(0, ruleName)) + .then(response => { + if (response.some(rule => rule.name === ruleName)) { + this.ruleNameTooltip = 'TOOLTIP.RULE_USER_EXISTING'; + this.isRuleNameExist = true; } - }) - .catch(error=>{ - this.errorHandler.error(error); - this.createEditRuleOpened = false; - }); + this.inNameChecking = false; + }).catch(() => { + this.inNameChecking = false; + }); + }); + + this.proNameChecker + .debounceTime(500) + .distinctUntilChanged() + .subscribe((name: string) => { + this.noProjectInfo = ''; + this.selectedProjectList = []; + toPromise(this.proService.listProjects(name, undefined)).then((res: any) => { + if (res) { + this.selectedProjectList = res.slice(0, 10); + // 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.noSelectedProject = true; + } else { + this.noProjectInfo = ''; + this.noSelectedProject = false; + this.setProject([pro]) + } + } else { + this.noProjectInfo = 'REPLICATION.PROJECT_NOT_EXIST_INFO'; + this.noSelectedProject = true; + } + }).catch((error: any) => { + this.errorHandler.error(error); + this.noProjectInfo = 'REPLICATION.PROJECT_NOT_EXIST_INFO'; + this.noSelectedProject = true; + }); + }); } - openCreateEditRule(editable: boolean, ruleId?: number | string): void { + ngOnDestroy(): void { + if (this.confirmSub) { + this.confirmSub.unsubscribe(); + } + if (this.nameChecker) { + this.nameChecker.unsubscribe(); + } + if (this.proNameChecker) { + this.proNameChecker.unsubscribe(); + } + } - this.createEditRule = this.initCreateEditRule; - this.editable = editable; + get isValid() { + return !(this.isRuleNameExist || this.noSelectedProject || this.noSelectedEndpoint || this.inProgress ); + } - this.isCreateEndpoint = false; - this.hasChanged = false; + createForm() { + this.ruleForm = this.fb.group({ + name: ['', Validators.required], + description: '', + projects: this.fb.array([]), + targets: this.fb.array([]), + trigger: this.fb.group({ + kind: this.triggerNames[0], + schedule_param: this.fb.group({ + type: this.scheduleNames[0], + weekday: 1, + offtime: '08:00' + }), + }), + filters: this.fb.array([]), + replicate_existing_image_now: true, + replicate_deletion: false + }); + } - this.pingTestMessage = ''; - this.pingStatus = true; - this.testOngoing = false; + initForm(): void { + this.ruleForm.reset({ + name: '', + description: '', + trigger: {kind: this.triggerNames[0], schedule_param: { + type: this.scheduleNames[0], + weekday: 1, + offtime: '08:00' + }}, + replicate_existing_image_now: true, + replicate_deletion: false + }); + this.setProject([this.emptyProject]); + this.setTarget([this.emptyEndpoint]); + this.setFilter([]); - if(ruleId) { - this.actionType = ActionType.EDIT; - this.translateService.get('REPLICATION.EDIT_POLICY_TITLE').subscribe(res=>this.modalTitle=res); - toPromise(this.replicationService - .getReplicationRule(ruleId)) - .then(rule=>{ - if(rule) { - this.createEditRule.ruleId = ruleId; - this.createEditRule.name = rule.name; - this.createEditRule.description = rule.description; - this.createEditRule.enable = rule.enabled === 1? true : false; - this.prepareTargets(rule.target_id); + this.copyUpdateForm = Object.assign({}, this.ruleForm.value); + } - this.initVal.name = this.createEditRule.name; - this.initVal.description = this.createEditRule.description; - this.initVal.enable = this.createEditRule.enable; + updateForm(rule: ReplicationRule): void { + rule.trigger = this.updateTrigger(rule.trigger); + this.ruleForm.reset({ + name: rule.name, + description: rule.description, + trigger: rule.trigger, + replicate_existing_image_now: rule.replicate_existing_image_now, + replicate_deletion: rule.replicate_deletion + }); + this.setProject(rule.projects); + this.noSelectedProject = false; + this.setTarget(rule.targets); + this.noSelectedEndpoint = false; - this.createEditRuleOpened = true; - } - }).catch(err=>this.errorHandler.error(err)); - } else { - if(!this.projectId) { - this.errorHandler.error('Project ID cannot be unset'); + if (rule.filters) { + this.setFilter(rule.filters); + this.updateFilter(rule.filters); + } + + // Force refresh view + let hnd = setInterval(() => this.ref.markForCheck(), 100); + setTimeout(() => clearInterval(hnd), 2000); + } + + get projects(): FormArray { + return this.ruleForm.get('projects') as FormArray; + } + setProject(projects: Project[]) { + const projectFGs = projects.map(project => this.fb.group(project)); + const projectFormArray = this.fb.array(projectFGs); + this.ruleForm.setControl('projects', projectFormArray); + } + + get filters(): FormArray { + return this.ruleForm.get('filters') as FormArray; + } + setFilter(filters: Filter[]) { + const filterFGs = filters.map(filter => this.fb.group(filter)); + const filterFormArray = this.fb.array(filterFGs); + this.ruleForm.setControl('filters', filterFormArray); + } + + get targets(): FormArray { + return this.ruleForm.get('targets') as FormArray; + } + setTarget(targets: Endpoint[]) { + const targetFGs = targets.map(target => this.fb.group(target)); + const targetFormArray = this.fb.array(targetFGs); + this.ruleForm.setControl('targets', targetFormArray); + } + + initFilter(name: string) { + return this.fb.group({ + kind: name, + pattern: ['', Validators.required] + }); + } + + filterChange($event: any) { + if ($event && $event.target['value']) { + let id: number = $event.target.id; + let name: string = $event.target.name; + let value: string = $event.target['value']; + + this.filterListData.forEach((data, index) => { + if (index === +id) { + data.name = $event.target.name = value; + }else { + data.options.splice(data.options.indexOf(value), 1); + } + if (data.options.indexOf(name) === -1) { + data.options.push(name); + } + }); + } + } + + targetChange($event: any) { + if ($event && $event.target) { + if ($event.target['value'] === '-1') { + this.noSelectedEndpoint = true; return; } - this.actionType = ActionType.ADD_NEW; - this.translateService.get('REPLICATION.ADD_POLICY').subscribe(res=>this.modalTitle=res); - this.prepareTargets(); - this.createEditRuleOpened = true; - } - } - - newEndpoint(checkedAddNew: boolean): void { - this.isCreateEndpoint = checkedAddNew; - if(this.isCreateEndpoint) { - this.createEditRule.endpointName = ''; - this.createEditRule.endpointUrl = ''; - this.createEditRule.username = ''; - this.createEditRule.password = ''; - this.createEditRule.insecure = false; - } else { - this.prepareTargets(); + let selecedTarget: Endpoint = this.targetList.find(target => target.id === +$event.target['value']); + this.setTarget([selecedTarget]); + this.noSelectedEndpoint = false; } } - selectEndpoint(): void { - let result: Endpoint | undefined = this.endpoints.find(target=>target.id == this.createEditRule.endpointId); - if(result) { - this.createEditRule.endpointId = result.id; - this.createEditRule.endpointUrl = result.endpoint; - this.createEditRule.username = result.username; - this.createEditRule.insecure = result.insecure; - this.createEditRule.password = FAKE_PASSWORD; + // Handle the form validation + handleValidation(): void { + let cont = this.ruleForm.controls["projects"]; + if (cont && cont.valid) { + this.proNameChecker.next(cont.value[0].name); + } } - } - - getRuleByForm(): ReplicationRule { - let rule: ReplicationRule = this.initReplicationRule; - rule.project_id = this.projectId; - rule.id = this.createEditRule.ruleId; - rule.name = this.createEditRule.name; - rule.description = this.createEditRule.description; - rule.enabled = this.createEditRule.enable ? 1 : 0; - rule.target_id = this.createEditRule.endpointId || ''; - return rule; - } - getEndpointByForm(): Endpoint { - let endpoint: Endpoint = this.initEndpoint; - endpoint.id = this.createEditRule.ruleId; - endpoint.name = this.createEditRule.endpointName || ''; - endpoint.endpoint = this.createEditRule.endpointUrl || ''; - endpoint.username = this.createEditRule.username; - endpoint.password = this.createEditRule.password; - endpoint.insecure = this.createEditRule.insecure; - return endpoint; - } - - createReplicationRule(): void { - toPromise(this.replicationService - .createReplicationRule(this.getRuleByForm())) - .then(response=>{ - this.translateService.get('REPLICATION.CREATED_SUCCESS') - .subscribe(res=>this.errorHandler.info(res)); - this.createEditRuleOpened = false; - this.reload.emit(true); - }) - .catch(error=>{ - if (error.status === 409) { - this.inlineAlert.showInlineError('REPLICATION.POLICY_ALREADY_EXISTS'); - } else { - this.inlineAlert.showInlineError(error); - } - }); - } - - updateReplicationRule(): void { - toPromise(this.replicationService - .updateReplicationRule(this.getRuleByForm())) - .then(()=>{ - this.translateService.get('REPLICATION.UPDATED_SUCCESS') - .subscribe(res=>this.errorHandler.info(res)); - this.createEditRuleOpened = false; - this.reload.emit(true); - }) - .catch(error=>{ - if (error.status === 409) { - this.inlineAlert.showInlineError('REPLICATION.POLICY_ALREADY_EXISTS'); - } else { - this.inlineAlert.showInlineError(error); - } - } - ); - } - - createWithEndpoint(actionType: ActionType): void { - toPromise(this.endpointService - .createEndpoint(this.getEndpointByForm())) - .then(()=>{ - toPromise(this.endpointService - .getEndpoints(this.createEditRule.endpointName)) - .then(endpoints=>{ - if(endpoints && endpoints.length > 0) { - let addedEndpoint: Endpoint = endpoints[0]; - this.createEditRule.endpointId = addedEndpoint.id; - switch(actionType) { - case ActionType.ADD_NEW: - this.createReplicationRule(); - break; - case ActionType.EDIT: - this.updateReplicationRule(); - break; - } - } - }) - .catch(error=>{ - this.inlineAlert.showInlineError(error); - this.errorHandler.error(error); - }); - }) - .catch(error=>{ - this.inlineAlert.showInlineError(error); - this.errorHandler.error(error); - }); - } - - onSubmit() { - if(this.isCreateEndpoint) { - this.createWithEndpoint(this.actionType); - } else { - switch(this.actionType) { - case ActionType.ADD_NEW: - this.createReplicationRule(); - break; - case ActionType.EDIT: - this.updateReplicationRule(); - break; + focusClear($event: any): void { + if (this.policyId < 0 && this.firstClick === 0) { + if ($event && $event.target && $event.target['value']) { + $event.target['value'] = ''; } + this.firstClick ++; } } - onCancel() { - if(this.hasChanged) { - this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'}); - } else { - this.createEditRuleOpened = false; - this.ruleForm.reset(); + leaveInput() { + this.selectedProjectList = []; + } + + selectedProjectName(projectName: string) { + this.noSelectedProject = false; + let pro: Project = this.selectedProjectList.find(data => data.name === projectName); + this.setProject([pro]); + this.selectedProjectList = []; + this.noProjectInfo = ""; + } + + selectedProject(project: Project): void { + if (!project) { + this.noSelectedProject = true; + }else { + this.noSelectedProject = false; + this.setProject([project]); } } - setInsecureValue($event: any) { - this.createEditRule.insecure = !$event; + addNewFilter(): void { + if (this.filterCount === 0) { + this.filterListData.push(this.baseFilterData(this.filterSelect[0], this.filterSelect.slice(), true)); + this.filters.push(this.initFilter(this.filterSelect[0])); + + }else { + let nameArr: string[] = this.filterSelect.slice(); + this.filterListData.forEach(data => { + nameArr.splice(nameArr.indexOf(data.name), 1); + }); + // when add a new filter,the filterListData should change the options + this.filterListData.filter((data) => { + data.options.splice(data.options.indexOf(nameArr[0]), 1); + }); + this.filterListData.push(this.baseFilterData(nameArr[0], nameArr, true)); + this.filters.push(this.initFilter(nameArr[0])); + } + this.filterCount += 1; + if (this.filterCount >= this.filterSelect.length) { + this.isFilterHide = true; + } } - confirmCancel(confirmed: boolean) { - this.createEditRuleOpened = false; - this.inlineAlert.close(); - this.ruleForm.reset(); - } - - ngAfterViewChecked(): void { - this.ruleForm = this.currentForm; - if(this.ruleForm) { - let comparison: {[key: string]: any} = { - name: this.initVal.name, - description: this.initVal.description, - enable: this.initVal.enable, - endpointId: this.initVal.endpointId, - targetName: this.initVal.name, - endpointUrl: this.initVal.endpointUrl, - username: this.initVal.username, - password: this.initVal.password, - insecure: this.initVal.insecure - }; - let self: CreateEditRuleComponent | any = this; - if(self) { - self.ruleForm.valueChanges.subscribe((data: any)=>{ - for(let key in data) { - let current = data[key]; - let origin: string = comparison[key]; - if(((self.actionType === ActionType.EDIT && !self.readonly && !current ) || current) && current !== origin) { - self.hasChanged = true; - break; - } else { - self.hasChanged = false; - self.inlineAlert.close(); - } + // delete a filter + deleteFilter(i: number): void { + if (i || i === 0) { + let delfilter = this.filterListData.splice(i, 1)[0]; + if (this.filterCount === this.filterSelect.length) { + this.isFilterHide = false; + } + this.filterCount -= 1; + if (this.filterListData.length) { + let optionVal = delfilter.name; + this.filterListData.filter(data => { + if (data.options.indexOf(optionVal) === -1) { + data.options.push(optionVal); } }); } + const control = this.ruleForm.controls['filters']; + control.removeAt(i); } } - testConnection() { - this.pingStatus = true; - this.btnAbled=true; - this.translateService.get('REPLICATION.TESTING_CONNECTION').subscribe(res=>this.pingTestMessage=res); - this.testOngoing = !this.testOngoing; - let pingTarget: Endpoint = this.initEndpoint; - if(this.isCreateEndpoint) { - pingTarget.endpoint = this.createEditRule.endpointUrl || ''; - pingTarget.username = this.createEditRule.username; - pingTarget.password = this.createEditRule.password; - pingTarget.insecure = this.createEditRule.insecure; - } else { - for (let prop in pingTarget) { - delete pingTarget[prop]; + selectTrigger($event: any): void { + if ($event && $event.target && $event.target['value']) { + let val: string = $event.target['value']; + if (val === this.triggerNames[2]) { + this.isScheduleOpt = true; + this.isImmediate = false; + } + if (val === this.triggerNames[1]) { + this.isScheduleOpt = false; + this.isImmediate = true; + } + if (val === this.triggerNames[0]) { + this.isScheduleOpt = false; + this.isImmediate = false; } - pingTarget.id = this.createEditRule.endpointId; } - toPromise(this.endpointService - .pingEndpoint(pingTarget)) - .then(()=>{ - this.testOngoing = !this.testOngoing; - this.translateService.get('REPLICATION.TEST_CONNECTION_SUCCESS').subscribe(res=>this.pingTestMessage=res); - this.pingStatus = true; - this.btnAbled=false; - }) - .catch(error=>{ - this.testOngoing = !this.testOngoing; - this.translateService.get('REPLICATION.TEST_CONNECTION_FAILURE').subscribe(res=>this.pingTestMessage=res); - this.pingStatus = false; - this.btnAbled=false; - }); } + + // Replication Schedule select value exchange + selectSchedule($event: any): void { + if ($event && $event.target && $event.target['value']) { + switch ($event.target['value']) { + case this.scheduleNames[1]: + this.weeklySchedule = true; + this.ruleForm.patchValue({ + trigger: { + schedule_param: { + weekday: 1, + } + } + }); + break; + case this.scheduleNames[0]: + this.weeklySchedule = false; + break; + } + } + } + + checkRuleName(): void { + let ruleName: string = this.ruleForm.controls['name'].value; + if (ruleName) { + this.nameChecker.next(ruleName); + } else { + this.ruleNameTooltip = 'TOOLTIP.EMPTY'; + } + } + + updateFilter(filters: any) { + let opt: string[] = this.filterSelect.slice(); + filters.forEach((filter: any) => { + opt.splice(opt.indexOf(filter.kind), 1); + }); + filters.forEach((filter: any) => { + let option: string [] = opt.slice(); + option.unshift(filter.kind); + this.filterListData.push(this.baseFilterData(filter.kind, option, true)); + }); + this.filterCount = filters.length; + if (filters.length === this.filterSelect.length) { + this.isFilterHide = true; + } + } + + updateTrigger(trigger: any) { + if (trigger['schedule_param']) { + this.isScheduleOpt = true; + this.isImmediate = false; + trigger['schedule_param']['offtime'] = this.getOfftime(trigger['schedule_param']['offtime']); + if (trigger['schedule_param']['weekday']) { + this.weeklySchedule = true; + }else { + // set default + trigger['schedule_param']['weekday'] = 1; + } + }else { + if (trigger['kind'] === this.triggerNames[0]) { + this.isImmediate = false; + } + if (trigger['kind'] === this.triggerNames[1]) { + this.isImmediate = true; + } + trigger['schedule_param'] = { type: this.scheduleNames[0], + weekday: this.weekly[0], + offtime: '08:00'}; + } + return trigger; + } + + setTriggerVaule(trigger: any) { + if (!this.isScheduleOpt) { + delete trigger['schedule_param']; + return trigger; + }else { + if (!this.weeklySchedule) { + delete trigger['schedule_param']['weekday']; + }else { + trigger['schedule_param']['weekday'] = +trigger['schedule_param']['weekday']; + } + trigger['schedule_param']['offtime'] = this.setOfftime(trigger['schedule_param']['offtime']); + return trigger; + } + } + + public hasFormChange(): boolean { + return !isEmptyObject(this.getChanges()); + } + + onSubmit() { + // add new Replication rule + this.inProgress = true; + let copyRuleForm: ReplicationRule = this.ruleForm.value; + copyRuleForm.trigger = this.setTriggerVaule(copyRuleForm.trigger); + if (this.policyId < 0) { + this.repService.createReplicationRule(copyRuleForm) + .then(() => { + this.translateService.get('REPLICATION.CREATED_SUCCESS') + .subscribe(res => this.errorHandler.info(res)); + this.inProgress = false; + this.reload.emit(true); + this.close(); + }).catch((error: any) => { + this.inProgress = false; + this.inlineAlert.showInlineError(error); + }); + } else { + this.repService.updateReplicationRule(this.policyId, this.ruleForm.value) + .then(() => { + this.translateService.get('REPLICATION.UPDATED_SUCCESS') + .subscribe(res => this.errorHandler.info(res)); + this.inProgress = false; + this.reload.emit(true); + this.close(); + }).catch((error: any) => { + this.inProgress = false; + this.inlineAlert.showInlineError(error); + }); + } + } + + openCreateEditRule(ruleId?: number | string): void { + this.initForm(); + this.selectedProjectList = []; + this.filterCount = 0; + this.filterListData = []; + this.firstClick = 0; + this.noSelectedProject = true; + this.noSelectedEndpoint = true; + this.isRuleNameExist = false; + + this.weeklySchedule = false; + this.isScheduleOpt = false; + this.isImmediate = false; + this.policyId = -1; + this.createEditRuleOpened = true; + + this.noProjectInfo = ""; + this.noEndpointInfo = ""; + if (this.targetList.length === 0) { + this.noEndpointInfo = 'REPLICATION.NO_ENDPOINT_INFO'; + } + if (this.projectList.length === 0 && !this.projectName) { + this.noProjectInfo = 'REPLICATION.NO_PROJECT_INFO'; + } + + if (ruleId) { + this.policyId = +ruleId; + this.headerTitle = 'REPLICATION.EDIT_POLICY_TITLE'; + toPromise(this.repService.getReplicationRule(ruleId)) + .then((response) => { + this.copyUpdateForm = Object.assign({}, response); + // set filter value is [] if callback fiter value is null. + this.copyUpdateForm.filters = response.filters ? response.filters : []; + this.updateForm(response); + }).catch((error: any) => { + this.inlineAlert.showInlineError(error); + }); + }else { + this.headerTitle = 'REPLICATION.ADD_POLICY'; + if (this.projectId) { + this.setProject([{project_id: this.projectId, name: this.projectName}]); + this.noSelectedProject = false; + } + + this.copyUpdateForm = Object.assign({}, this.ruleForm.value); + } + } + + close(): void { + this.createEditRuleOpened = false; + } + + confirmCancel(confirmed: boolean) { + this.inlineAlert.close(); + this.close(); + } + + onCancel(): void { + if (this.hasFormChange()) { + this.inlineAlert.showInlineConfirmation({ message: 'ALERT.FORM_CHANGE_CONFIRMATION' }); + }else { + this.close(); + } + } + + goRegistry(): void { + this.goToRegistry.emit(); + } + + // UTC time + public getOfftime(daily_time: any): string { + + let timeOffset = 0; // seconds + if (daily_time && typeof daily_time === 'number') { + timeOffset = +daily_time; + } + + // Convert to current time + let timezoneOffset: number = this._localTime.getTimezoneOffset(); + // Local time + timeOffset = timeOffset - timezoneOffset * 60; + if (timeOffset < 0) { + timeOffset = timeOffset + ONE_DAY_SECONDS; + } + + if (timeOffset >= ONE_DAY_SECONDS) { + timeOffset -= ONE_DAY_SECONDS; + } + + // To time string + let hours: number = Math.floor(timeOffset / ONE_HOUR_SECONDS); + let minutes: number = Math.floor((timeOffset - hours * ONE_HOUR_SECONDS) / 60); + + let timeStr: string = '' + hours; + if (hours < 10) { + timeStr = '0' + timeStr; + } + if (minutes < 10) { + timeStr += ':0'; + } else { + timeStr += ':'; + } + timeStr += minutes; + + return timeStr; + } + public setOfftime(v: string) { + if (!v || v === '') { + return; + } + + let values: string[] = v.split(':'); + if (!values || values.length !== 2) { + return; + } + + let hours: number = +values[0]; + let minutes: number = +values[1]; + // Convert to UTC time + let timezoneOffset: number = this._localTime.getTimezoneOffset(); + let utcTimes: number = hours * ONE_HOUR_SECONDS + minutes * 60; + utcTimes += timezoneOffset * 60; + if (utcTimes < 0) { + utcTimes += ONE_DAY_SECONDS; + } + + if (utcTimes >= ONE_DAY_SECONDS) { + utcTimes -= ONE_DAY_SECONDS; + } + + return utcTimes; + } + + getChanges(): { [key: string]: any | any[] } { + let changes: { [key: string]: any | any[] } = {}; + let ruleValue: { [key: string]: any | any[] } = this.ruleForm.value; + if (!ruleValue || !this.copyUpdateForm) { + return changes; + } + for (let prop in ruleValue) { + let field: any = this.copyUpdateForm[prop]; + if (!compareValue(field, ruleValue[prop])) { + if (ruleValue[prop][0] && ruleValue[prop][0].project_id && (ruleValue[prop][0].project_id === field[0].project_id)) { + break; + } + if (ruleValue[prop][0] && ruleValue[prop][0].id && (ruleValue[prop][0].id === field[0].id)) { + break; + } + changes[prop] = ruleValue[prop]; + // Number + if (typeof field === "number") { + changes[prop] = +changes[prop]; + } + + // Trim string value + if (typeof field === "string") { + changes[prop] = ('' + changes[prop]).trim(); + } + } + } + + return changes; + } + } \ No newline at end of file diff --git a/src/ui_ng/lib/src/endpoint/endpoint.component.ts b/src/ui_ng/lib/src/endpoint/endpoint.component.ts index f500ffd7f..53dd65343 100644 --- a/src/ui_ng/lib/src/endpoint/endpoint.component.ts +++ b/src/ui_ng/lib/src/endpoint/endpoint.component.ts @@ -137,23 +137,13 @@ export class EndpointComponent implements OnInit, OnDestroy { editTargets(targets: Endpoint[]) { if (targets && targets.length === 1) { - let target= targets[0]; + let target = targets[0]; let editable = true; if (!target.id) { return; } let id: number | string = target.id; - toPromise(this.endpointService - .getEndpointWithReplicationRules(id)) - .then( - rules => { - if (rules && rules.length > 0) { - rules.forEach((rule) => editable = (rule && rule.enabled !== 1)); - } - this.createEditEndpointComponent.openCreateEditTarget(editable, id); - this.forceRefreshView(1000); - }) - .catch(error => this.errorHandler.error(error)); + this.createEditEndpointComponent.openCreateEditTarget(editable, id); } } diff --git a/src/ui_ng/lib/src/index.ts b/src/ui_ng/lib/src/index.ts index 79858a122..878e2d428 100644 --- a/src/ui_ng/lib/src/index.ts +++ b/src/ui_ng/lib/src/index.ts @@ -8,6 +8,7 @@ export * from './filter/index'; export * from './endpoint/index'; export * from './repository/index'; export * from './create-edit-endpoint/index'; +export * from './create-edit-rule/index'; export * from './repository-stackview/index'; export * from './tag/index'; export * from './list-replication-rule/index'; diff --git a/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.html.ts b/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.html.ts index 9faec0912..47c006bad 100644 --- a/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.html.ts +++ b/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.html.ts @@ -1,7 +1,7 @@ export const LIST_REPLICATION_RULE_TEMPLATE: string = `
- + @@ -35,7 +35,6 @@ export const LIST_REPLICATION_RULE_TEMPLATE: string = ` -
`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.spec.ts b/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.spec.ts index 512391a18..39b2689e6 100644 --- a/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.spec.ts +++ b/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.spec.ts @@ -14,66 +14,91 @@ import { ErrorHandler } from '../error-handler/error-handler'; import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; import { ReplicationService, ReplicationDefaultService } from '../service/replication.service'; - describe('ListReplicationRuleComponent (inline template)', ()=>{ let mockRules: ReplicationRule[] = [ { "id": 1, - "project_id": 1, - "project_name": "library", - "target_id": 1, - "target_name": "target_01", + "projects": [{ + "project_id": 33, + "owner_id": 1, + "name": "aeas", + "deleted": 0, + "togglable": false, + "current_user_role_id": 0, + "repo_count": 0, + "metadata": { + "public": false, + "enable_content_trust": "", + "prevent_vul": "", + "severity": "", + "auto_scan": ""}, + "owner_name": "", + "creation_time": null, + "update_time": null, + "has_project_admin_role": true, + "is_member": true, + "role_name": "" + }], + "targets": [{ + "endpoint": "", + "id": 0, + "insecure": false, + "name": "khans3", + "username": "", + "password": "", + "type": 0, + }], "name": "sync_01", - "enabled": 0, "description": "", - "cron_str": "", + "filters": null, + "trigger": {"kind": "Manual", "schedule_param": null}, "error_job_count": 2, - "deleted": 0 + "replicate_deletion": false, + "replicate_existing_image_now": false, }, { - "id": 2, - "project_id": 1, - "project_name": "library", - "target_id": 3, - "target_name": "target_02", - "name": "sync_02", - "enabled": 1, - "description": "", - "cron_str": "", - "error_job_count": 1, - "deleted": 0 - }, - { - "id": 3, - "project_id": 1, - "project_name": "library", - "target_id": 2, - "target_name": "target_03", - "name": "sync_03", - "enabled": 0, - "description": "", - "cron_str": "", - "error_job_count": 0, - "deleted": 0 - } + "id": 2, + "projects": [{ + "project_id": 33, + "owner_id": 1, + "name": "aeas", + "deleted": 0, + "togglable": false, + "current_user_role_id": 0, + "repo_count": 0, + "metadata": { + "public": false, + "enable_content_trust": "", + "prevent_vul": "", + "severity": "", + "auto_scan": ""}, + "owner_name": "", + "creation_time": null, + "update_time": null, + "has_project_admin_role": true, + "is_member": true, + "role_name": "" + }], + "targets": [{ + "endpoint": "", + "id": 0, + "insecure": false, + "name": "khans3", + "username": "", + "password": "", + "type": 0, + }], + "name": "sync_02", + "description": "", + "filters": null, + "trigger": {"kind": "Manual", "schedule_param": null}, + "error_job_count": 2, + "replicate_deletion": false, + "replicate_existing_image_now": false, + }, ]; - - let mockRule: ReplicationRule = { - "id": 1, - "project_id": 1, - "project_name": "library", - "target_id": 1, - "target_name": "target_01", - "name": "sync_01", - "enabled": 0, - "description": "", - "cron_str": "", - "error_job_count": 2, - "deleted": 0 - }; - let fixture: ComponentFixture; let comp: ListReplicationRuleComponent; diff --git a/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.ts b/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.ts index eb3d99334..ad8f33677 100644 --- a/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.ts +++ b/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.ts @@ -60,9 +60,8 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges { @Input() isSystemAdmin: boolean; @Input() selectedId: number | string; @Input() withReplicationJob: boolean; - @Input() readonly: boolean; - @Input() loading: boolean = false; + @Input() loading = false; @Output() reload = new EventEmitter(); @Output() selectOne = new EventEmitter(); @@ -100,10 +99,6 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges { setInterval(() => ref.markForCheck(), 500); } - public get opereateAvailable(): boolean { - return !this.readonly && !this.projectId ? true : false; - } - trancatedDescription(desc: string): string { if (desc.length > 35 ) { return desc.substr(0, 35); @@ -135,7 +130,7 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges { retrieveRules(ruleName: string = ''): void { this.loading = true; - this.selectedRow = null; + /*this.selectedRow = null;*/ toPromise(this.replicationService .getReplicationRules(this.projectId, ruleName)) .then(rules => { @@ -156,36 +151,6 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges { }); } - filterRuleStatus(status: string) { - if (status === 'all') { - this.changedRules = this.rules; - } else { - this.changedRules = this.rules.filter(policy => policy.enabled === +status); - } - } - - toggleConfirm(message: ConfirmationAcknowledgement) { - if (message && - message.source === ConfirmationTargets.TOGGLE_CONFIRM && - message.state === ConfirmationState.CONFIRMED) { - this.batchDelectionInfos = []; - let rule: ReplicationRule = message.data; - let initBatchMessage = new BatchInfo (); - initBatchMessage.name = rule.name; - this.batchDelectionInfos.push(initBatchMessage); - - if (rule) { - rule.enabled = rule.enabled === 0 ? 1 : 0; - toPromise(this.replicationService - .enableReplicationRule(rule.id || '', rule.enabled)) - .then(() => - this.translateService.get('REPLICATION.TOGGLED_SUCCESS') - .subscribe(res => this.batchDelectionInfos[0].status = res)) - .catch(error => this.batchDelectionInfos[0].status = error); - } - } - } - replicateRule(rules: ReplicationRule[]): void { this.replicateManual.emit(rules); } @@ -216,17 +181,6 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges { this.editOne.emit(rule); } - toggleRule(rule: ReplicationRule) { - let toggleConfirmMessage: ConfirmationMessage = new ConfirmationMessage( - rule.enabled === 1 ? 'REPLICATION.TOGGLE_DISABLE_TITLE' : 'REPLICATION.TOGGLE_ENABLE_TITLE', - rule.enabled === 1 ? 'REPLICATION.CONFIRM_TOGGLE_DISABLE_POLICY' : 'REPLICATION.CONFIRM_TOGGLE_ENABLE_POLICY', - rule.name || '', - rule, - ConfirmationTargets.TOGGLE_CONFIRM - ); - this.toggleConfirmDialog.open(toggleConfirmMessage); - } - jobList(id: string | number): Promise { let ruleData: ReplicationJobItem[]; this.canDeleteRule = true; diff --git a/src/ui_ng/lib/src/project-policy-config/project.ts b/src/ui_ng/lib/src/project-policy-config/project.ts index 1f4a4fa6d..ed1d26e01 100644 --- a/src/ui_ng/lib/src/project-policy-config/project.ts +++ b/src/ui_ng/lib/src/project-policy-config/project.ts @@ -1,18 +1,18 @@ export class Project { project_id: number; - owner_id: number; + owner_id?: number; name: string; - creation_time: Date | string; - deleted: number; - owner_name: string; - togglable: boolean; - update_time: Date | string; - current_user_role_id: number; - repo_count: number; - has_project_admin_role: boolean; - is_member: boolean; - role_name: string; - metadata: { + creation_time?: Date | string; + deleted?: number; + owner_name?: string; + togglable?: boolean; + update_time?: Date | string; + current_user_role_id?: number; + repo_count?: number; + has_project_admin_role?: boolean; + is_member?: boolean; + role_name?: string; + metadata?: { public: string | boolean; enable_content_trust: string | boolean; prevent_vul: string | boolean; diff --git a/src/ui_ng/lib/src/replication/replication.component.html.ts b/src/ui_ng/lib/src/replication/replication.component.html.ts index d396fffb4..91af4e7aa 100644 --- a/src/ui_ng/lib/src/replication/replication.component.html.ts +++ b/src/ui_ng/lib/src/replication/replication.component.html.ts @@ -11,7 +11,7 @@ export const REPLICATION_TEMPLATE: string = `
- +
@@ -73,5 +73,6 @@ export const REPLICATION_TEMPLATE: string = `
+ `; \ No newline at end of file diff --git a/src/ui_ng/lib/src/replication/replication.component.spec.ts b/src/ui_ng/lib/src/replication/replication.component.spec.ts index 7b720ec15..d349a809b 100644 --- a/src/ui_ng/lib/src/replication/replication.component.spec.ts +++ b/src/ui_ng/lib/src/replication/replication.component.spec.ts @@ -12,7 +12,7 @@ import { DatePickerComponent } from '../datetime-picker/datetime-picker.componen import { DateValidatorDirective } from '../datetime-picker/date-validator.directive'; import { FilterComponent } from '../filter/filter.component'; import { InlineAlertComponent } from '../inline-alert/inline-alert.component'; -import { ReplicationRule, ReplicationJob, Endpoint } from '../service/interface'; +import {ReplicationRule, ReplicationJob, Endpoint} from '../service/interface'; import { ErrorHandler } from '../error-handler/error-handler'; import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; @@ -20,49 +20,92 @@ import { ReplicationService, ReplicationDefaultService } from '../service/replic import { EndpointService, EndpointDefaultService } from '../service/endpoint.service'; import { JobLogViewerComponent } from '../job-log-viewer/job-log-viewer.component'; import { JobLogService, JobLogDefaultService, ReplicationJobItem } from '../service/index'; +import {Project} from "../project-policy-config/project"; +import {ProjectDefaultService, ProjectService} from "service/project.service"; -describe('Replication Component (inline template)', ()=>{ +describe('Replication Component (inline template)', () => { let mockRules: ReplicationRule[] = [ - { - "id": 1, - "project_id": 1, - "project_name": "library", - "target_id": 1, - "target_name": "target_01", - "name": "sync_01", - "enabled": 0, - "description": "", - "cron_str": "", - "error_job_count": 2, - "deleted": 0 - }, - { - "id": 2, - "project_id": 1, - "project_name": "library", - "target_id": 3, - "target_name": "target_02", - "name": "sync_02", - "enabled": 1, - "description": "", - "cron_str": "", - "error_job_count": 1, - "deleted": 0 - }, - { - "id": 3, - "project_id": 1, - "project_name": "library", - "target_id": 2, - "target_name": "target_03", - "name": "sync_03", - "enabled": 0, - "description": "", - "cron_str": "", - "error_job_count": 0, - "deleted": 0 - } + { + "id": 1, + "projects": [{ + "project_id": 33, + "owner_id": 1, + "name": "aeas", + "deleted": 0, + "togglable": false, + "current_user_role_id": 0, + "repo_count": 0, + "metadata": { + "public": false, + "enable_content_trust": "", + "prevent_vul": "", + "severity": "", + "auto_scan": ""}, + "owner_name": "", + "creation_time": null, + "update_time": null, + "has_project_admin_role": true, + "is_member": true, + "role_name": "" + }], + "targets": [{ + "id": 1, + "endpoint": "https://10.117.4.151", + "name": "target_01", + "username": "admin", + "password": "", + "insecure": false, + "type": 0 + }], + "name": "sync_01", + "description": "", + "filters": null, + "trigger": {"kind": "Manual", "schedule_param": null}, + "error_job_count": 2, + "replicate_deletion": false, + "replicate_existing_image_now": false, + }, + { + "id": 2, + "projects": [{ + "project_id": 33, + "owner_id": 1, + "name": "aeas", + "deleted": 0, + "togglable": false, + "current_user_role_id": 0, + "repo_count": 0, + "metadata": { + "public": false, + "enable_content_trust": "", + "prevent_vul": "", + "severity": "", + "auto_scan": ""}, + "owner_name": "", + "creation_time": null, + "update_time": null, + "has_project_admin_role": true, + "is_member": true, + "role_name": "" + }], + "targets": [{ + "id": 1, + "endpoint": "https://10.117.4.151", + "name": "target_01", + "username": "admin", + "password": "", + "insecure": false, + "type": 0 + }], + "name": "sync_02", + "description": "", + "filters": null, + "trigger": {"kind": "Manual", "schedule_param": null}, + "error_job_count": 2, + "replicate_deletion": false, + "replicate_existing_image_now": false, + } ]; let mockJobs: ReplicationJobItem[] = [ @@ -81,7 +124,7 @@ describe('Replication Component (inline template)', ()=>{ "repository": "library/mysql", "policy_id": 1, "operation": "transfer", - "update_time": new Date("2017-05-27 12:20:33"), + "update_time": new Date("2017-05-27 12:20:33"), "tags": null }, { @@ -95,71 +138,66 @@ describe('Replication Component (inline template)', ()=>{ } ]; + let mockEndpoints: Endpoint[] = [ + { + "id": 1, + "endpoint": "https://10.117.4.151", + "name": "target_01", + "username": "admin", + "password": "", + "insecure": false, + "type": 0 + }, + { + "id": 2, + "endpoint": "https://10.117.5.142", + "name": "target_02", + "username": "AAA", + "password": "", + "insecure": false, + "type": 0 + }, + ]; + + let mockProjects: Project[] = [ + { "project_id": 1, + "owner_id": 0, + "name": 'project_01', + "creation_time": '', + "deleted": 0, + "owner_name": '', + "togglable": false, + "update_time": '', + "current_user_role_id": 0, + "repo_count": 0, + "has_project_admin_role": false, + "is_member": false, + "role_name": '', + "metadata": { + "public": '', + "enable_content_trust": '', + "prevent_vul": '', + "severity": '', + "auto_scan": '', + } + }]; + let mockJob: ReplicationJob = { metadata: {xTotalCount: 3}, data: mockJobs }; - let mockEndpoints: Endpoint[] = [ - { - "id": 1, - "endpoint": "https://10.117.4.151", - "name": "target_01", - "username": "admin", - "password": "", - "insecure": false, - "type": 0 - }, - { - "id": 2, - "endpoint": "https://10.117.5.142", - "name": "target_02", - "username": "AAA", - "password": "", - "insecure": false, - "type": 0 - }, - { - "id": 3, - "endpoint": "https://101.1.11.111", - "name": "target_03", - "username": "admin", - "password": "", - "insecure": false, - "type": 0 - }, - { - "id": 4, - "endpoint": "http://4.4.4.4", - "name": "target_04", - "username": "", - "password": "", - "insecure": false, - "type": 0 - } - ]; - - let mockRule: ReplicationRule = { - "id": 1, - "project_id": 1, - "project_name": "library", - "target_id": 1, - "target_name": "target_01", - "name": "sync_01", - "enabled": 0, - "description": "", - "cron_str": "", - "error_job_count": 2, - "deleted": 0 - }; - let fixture: ComponentFixture; + let fixtureCreate: ComponentFixture; let comp: ReplicationComponent; + let compCreate: CreateEditRuleComponent; let replicationService: ReplicationService; + let endpointService: EndpointService; let spyRules: jasmine.Spy; let spyJobs: jasmine.Spy; + let spyEndpoint: jasmine.Spy; let deGrids: DebugElement[]; let deRules: DebugElement; @@ -194,23 +232,29 @@ describe('Replication Component (inline template)', ()=>{ { provide: SERVICE_CONFIG, useValue: config }, { provide: ReplicationService, useClass: ReplicationDefaultService }, { provide: EndpointService, useClass: EndpointDefaultService }, + { provide: ProjectService, useClass: ProjectDefaultService }, { provide: JobLogService, useClass: JobLogDefaultService } ] }); })); - beforeEach(()=>{ + beforeEach(() => { fixture = TestBed.createComponent(ReplicationComponent); - + fixtureCreate = TestBed.createComponent(CreateEditRuleComponent); comp = fixture.componentInstance; + compCreate = fixtureCreate.componentInstance; comp.projectId = 1; comp.search.ruleId = 1; replicationService = fixture.debugElement.injector.get(ReplicationService); - + + endpointService = fixtureCreate.debugElement.injector.get(EndpointService); + spyRules = spyOn(replicationService, 'getReplicationRules').and.returnValues(Promise.resolve(mockRules)); spyJobs = spyOn(replicationService, 'getJobs').and.returnValues(Promise.resolve(mockJob)); - + + spyEndpoint = spyOn(endpointService, 'getEndpoints').and.returnValues(Promise.resolve(mockEndpoints)); + fixture.detectChanges(); fixture.whenStable().then(()=>{ fixture.detectChanges(); @@ -221,6 +265,7 @@ describe('Replication Component (inline template)', ()=>{ }); }); + it('Should load replication rules', async(()=>{ fixture.detectChanges(); fixture.whenStable().then(()=>{ diff --git a/src/ui_ng/lib/src/replication/replication.component.ts b/src/ui_ng/lib/src/replication/replication.component.ts index 7a8de6fba..5479f9ad5 100644 --- a/src/ui_ng/lib/src/replication/replication.component.ts +++ b/src/ui_ng/lib/src/replication/replication.component.ts @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. import { Component, OnInit, ViewChild, Input, Output, OnDestroy, EventEmitter } from '@angular/core'; -import { ResponseOptions, RequestOptions } from '@angular/http'; -import { NgModel } from '@angular/forms'; import { TranslateService } from '@ngx-translate/core'; @@ -23,7 +21,7 @@ import { ErrorHandler } from '../error-handler/error-handler'; import { ReplicationService } from '../service/replication.service'; import { RequestQueryParams } from '../service/RequestQueryParams'; -import { ReplicationRule, ReplicationJob, Endpoint, ReplicationJobItem } from '../service/interface'; +import { ReplicationRule, ReplicationJob, ReplicationJobItem } from '../service/interface'; import { toPromise, @@ -89,13 +87,14 @@ export class SearchOption { export class ReplicationComponent implements OnInit, OnDestroy { @Input() projectId: number | string; + @Input() projectName: string; @Input() isSystemAdmin: boolean; @Input() withReplicationJob: boolean; - @Input() readonly: boolean; @Output() redirect = new EventEmitter(); @Output() openCreateRule = new EventEmitter(); @Output() openEdit = new EventEmitter(); + @Output() goToRegistry = new EventEmitter(); search: SearchOption = new SearchOption(); @@ -106,7 +105,6 @@ export class ReplicationComponent implements OnInit, OnDestroy { currentJobStatus: { key: string, description: string }; changedRules: ReplicationRule[]; - initSelectedId: number | string; rules: ReplicationRule[]; loading: boolean; @@ -121,8 +119,8 @@ export class ReplicationComponent implements OnInit, OnDestroy { @ViewChild(ListReplicationRuleComponent) listReplicationRule: ListReplicationRuleComponent; -/* @ViewChild(CreateEditRuleComponent) - createEditPolicyComponent: CreateEditRuleComponent;*/ + @ViewChild(CreateEditRuleComponent) + createEditPolicyComponent: CreateEditRuleComponent; @ViewChild("replicationLogViewer") replicationLogViewer: JobLogViewerComponent; @@ -164,20 +162,22 @@ export class ReplicationComponent implements OnInit, OnDestroy { } } + // open replication rule openModal(): void { - this.openCreateRule.emit(); + this.createEditPolicyComponent.openCreateEditRule(); } + // edit replication rule openEditRule(rule: ReplicationRule) { if (rule) { - let editable = true; - if (rule.enabled === 1) { - editable = false; - } - this.openEdit.emit(rule.id); + this.createEditPolicyComponent.openCreateEditRule(rule.id); } } + goRegistry(): void { + this.goToRegistry.emit(); + } + //Server driven data loading clrLoadJobs(state: State): void { if (!state || !state.page || !this.search.ruleId) { @@ -209,6 +209,12 @@ export class ReplicationComponent implements OnInit, OnDestroy { } this.jobsLoading = true; + + //Do filtering and sorting + this.jobs = doFiltering(this.jobs, state); + this.jobs = doSorting(this.jobs, state); + + this.jobsLoading = false; toPromise(this.replicationService .getJobs(this.search.ruleId, params)) .then( diff --git a/src/ui_ng/lib/src/service/interface.ts b/src/ui_ng/lib/src/service/interface.ts index 6b876154d..5aa592b98 100644 --- a/src/ui_ng/lib/src/service/interface.ts +++ b/src/ui_ng/lib/src/service/interface.ts @@ -1,3 +1,4 @@ +import {Project} from "../project-policy-config/project"; /** * The base interface contains the general properties * @@ -83,18 +84,40 @@ export interface Endpoint extends Base { * * @export * @interface ReplicationRule + * @interface Filter + * @interface Trigger */ export interface ReplicationRule extends Base { - project_id: number | string; - project_name: string; - target_id: number | string; - target_name: string; - enabled: number; - description?: string; - cron_str?: string; - start_time?: Date; - error_job_count?: number; - deleted: number; + [key: string]: any; + id?: number; + name: string; + description: string; + projects: Project[]; + targets: Endpoint[] ; + trigger: Trigger ; + filters: Filter[] ; + replicate_existing_image_now?: boolean; + replicate_deletion?: boolean; +} + +export class Filter { + kind: string; + pattern: string; + constructor(kind: string, pattern: string) { + this.kind = kind; + this.pattern = pattern; + } +} + +export class Trigger { + kind: string; + schedule_param: any | { + [key: string]: any | any[]; + }; + constructor(kind: string, param: any | { [key: string]: any | any[]; }) { + this.kind = kind; + this.schedule_param = param; + } } /** @@ -115,7 +138,7 @@ export interface ReplicationJob { * @interface ReplicationJob */ export interface ReplicationJobItem extends Base { - [key: string]: any | any[] + [key: string]: any | any[]; status: string; repository: string; policy_id: number; @@ -151,7 +174,7 @@ export interface AccessLog { * @interface AccessLogItem */ export interface AccessLogItem { - [key: string]: any | any[] + [key: string]: any | any[]; log_id: number; project_id: number; repo_name: string; diff --git a/src/ui_ng/lib/src/service/project.service.ts b/src/ui_ng/lib/src/service/project.service.ts index 341123dd7..6d90a20f4 100644 --- a/src/ui_ng/lib/src/service/project.service.ts +++ b/src/ui_ng/lib/src/service/project.service.ts @@ -6,7 +6,8 @@ import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; import { Project } from '../project-policy-config/project'; import { ProjectPolicy } from '../project-policy-config/project-policy-config.component'; -import {HTTP_JSON_OPTIONS, HTTP_GET_OPTIONS} from "../utils"; +import {HTTP_JSON_OPTIONS, HTTP_GET_OPTIONS, buildHttpRequestOptions} from "../utils"; +import {RequestQueryParams} from "./RequestQueryParams"; /** * Define the service methods to handle the Prject related things. @@ -38,6 +39,8 @@ export abstract class ProjectService { * @memberOf EndpointService */ abstract updateProjectPolicy(projectId: number | string, projectPolicy: ProjectPolicy): Observable | Promise | any; + + abstract listProjects(name: string, isPublic: number, page?: number, pageSize?: number): Observable | Promise | Project[]; } /** @@ -68,6 +71,27 @@ export class ProjectDefaultService extends ProjectService { .catch(error => Observable.throw(error)); } + listProjects(name: string, isPublic: number, page?: number, pageSize?: number): Observable | Promise | Project[] { + let params = new RequestQueryParams(); + if (page && pageSize) { + params.set('page', page + ''); + params.set('page_size', pageSize + ''); + } + if (name && name.trim() !== "") { + params.set('name', name); + + } + if (isPublic !== undefined) { + params.set('public', '' + isPublic); + } + + // let options = new RequestOptions({ headers: this.getHeaders, search: params }); + return this.http + .get(`/api/projects`, buildHttpRequestOptions(params)) + .map(response => response.json()) + .catch(error => Observable.throw(error)); + } + public updateProjectPolicy(projectId: number | string, projectPolicy: ProjectPolicy): any { return this.http .put(`/api/projects/${projectId}`, { 'metadata': { diff --git a/src/ui_ng/lib/src/service/replication.service.ts b/src/ui_ng/lib/src/service/replication.service.ts index ee111c279..ecb5b1797 100644 --- a/src/ui_ng/lib/src/service/replication.service.ts +++ b/src/ui_ng/lib/src/service/replication.service.ts @@ -1,6 +1,6 @@ import { Observable } from 'rxjs/Observable'; import { RequestQueryParams } from './RequestQueryParams'; -import { ReplicationJob, ReplicationRule, ReplicationJobItem } from './interface'; +import {ReplicationJob, ReplicationRule, ReplicationJobItem} from './interface'; import { Injectable, Inject } from "@angular/core"; import 'rxjs/add/observable/of'; import { Http, RequestOptions } from '@angular/http'; @@ -62,7 +62,7 @@ export abstract class ReplicationService { * * @memberOf ReplicationService */ - abstract updateReplicationRule(replicationRule: ReplicationRule): Observable | Promise | any; + abstract updateReplicationRule(id: number, rep: ReplicationRule): Observable | Promise | any; /** * Delete the specified replication rule. @@ -159,7 +159,7 @@ export class ReplicationDefaultService extends ReplicationService { //Private methods //Check if the rule object is valid _isValidRule(rule: ReplicationRule): boolean { - return rule !== undefined && rule != null && rule.name !== undefined && rule.name.trim() !== '' && rule.target_id !== 0; + return rule !== undefined && rule != null && rule.name !== undefined && rule.name.trim() !== '' && rule.targets.length !== 0; } public getReplicationRules(projectId?: number | string, ruleName?: string, queryParams?: RequestQueryParams): Observable | Promise | ReplicationRule[] { @@ -177,7 +177,7 @@ export class ReplicationDefaultService extends ReplicationService { return this.http.get(this._ruleBaseUrl, buildHttpRequestOptions(queryParams)).toPromise() .then(response => response.json() as ReplicationRule[]) - .catch(error => Promise.reject(error)) + .catch(error => Promise.reject(error)); } public getReplicationRule(ruleId: number | string): Observable | Promise | ReplicationRule { @@ -201,13 +201,13 @@ export class ReplicationDefaultService extends ReplicationService { .catch(error => Promise.reject(error)); } - public updateReplicationRule(replicationRule: ReplicationRule): Observable | Promise | any { - if (!this._isValidRule(replicationRule) || !replicationRule.id) { + public updateReplicationRule(id: number, rep: ReplicationRule): Observable | Promise | any { + if (!this._isValidRule(rep)) { return Promise.reject('Bad argument'); } - let url: string = `${this._ruleBaseUrl}/${replicationRule.id}`; - return this.http.put(url, JSON.stringify(replicationRule), HTTP_JSON_OPTIONS).toPromise() + let url = `${this._ruleBaseUrl}/${id}`; + return this.http.put(url, JSON.stringify(rep), HTTP_JSON_OPTIONS).toPromise() .then(response => response) .catch(error => Promise.reject(error)); } @@ -298,7 +298,7 @@ export class ReplicationDefaultService extends ReplicationService { return Promise.reject('Bad argument'); } - let logUrl: string = `${this._jobBaseUrl}/${jobId}/log`; + let logUrl = `${this._jobBaseUrl}/${jobId}/log`; return this.http.get(logUrl, HTTP_GET_OPTIONS).toPromise() .then(response => response.text()) .catch(error => Promise.reject(error)); diff --git a/src/ui_ng/lib/src/shared/shared.module.ts b/src/ui_ng/lib/src/shared/shared.module.ts index 365b14ee9..63d85fa16 100644 --- a/src/ui_ng/lib/src/shared/shared.module.ts +++ b/src/ui_ng/lib/src/shared/shared.module.ts @@ -2,8 +2,8 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { HttpModule, Http } from '@angular/http'; import { ClarityModule } from 'clarity-angular'; -import { FormsModule } from '@angular/forms'; -import { TranslateModule, TranslateLoader, TranslateService, MissingTranslationHandler } from '@ngx-translate/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { TranslateModule, TranslateLoader, MissingTranslationHandler } from '@ngx-translate/core'; import { MyMissingTranslationHandler } from '../i18n/missing-trans.handler'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { TranslatorJsonLoader } from '../i18n/local-json.loader'; @@ -41,6 +41,7 @@ export function GeneralTranslatorLoader(http: Http, config: IServiceConfig) { CommonModule, HttpModule, FormsModule, + ReactiveFormsModule, ClipboardModule, CookieModule.forRoot(), ClarityModule.forRoot(), @@ -60,6 +61,7 @@ export function GeneralTranslatorLoader(http: Http, config: IServiceConfig) { CommonModule, HttpModule, FormsModule, + ReactiveFormsModule, CookieModule, ClipboardModule, ClarityModule, diff --git a/src/ui_ng/package.json b/src/ui_ng/package.json index 2afe1600e..88b5993f2 100644 --- a/src/ui_ng/package.json +++ b/src/ui_ng/package.json @@ -31,7 +31,7 @@ "clarity-icons": "^0.10.17", "clarity-ui": "^0.10.17", "core-js": "^2.4.1", - "harbor-ui": "0.6.45", + "harbor-ui": "0.6.46", "intl": "^1.2.5", "mutationobserver-shim": "^0.3.2", "ngx-cookie": "^1.0.0", diff --git a/src/ui_ng/src/app/harbor-routing.module.ts b/src/ui_ng/src/app/harbor-routing.module.ts index 42e083df2..9b84013cd 100644 --- a/src/ui_ng/src/app/harbor-routing.module.ts +++ b/src/ui_ng/src/app/harbor-routing.module.ts @@ -50,8 +50,6 @@ import { LeavingConfigRouteDeactivate } from './shared/route/leaving-config-deac import { MemberGuard } from './shared/route/member-guard-activate.service'; import { TagDetailPageComponent } from './repository/tag-detail/tag-detail-page.component'; -import { ReplicationRuleComponent} from "./replication/replication-rule/replication-rule.component"; -import {LeavingNewRuleRouteDeactivate} from "./shared/route/leaving-new-rule-deactivate.service"; import { LeavingRepositoryRouteDeactivate } from './shared/route/leaving-repository-deactivate.service'; const harborRoutes: Routes = [ @@ -92,20 +90,6 @@ const harborRoutes: Routes = [ canActivate: [SystemAdminGuard], canActivateChild: [SystemAdminGuard], }, - { - path: 'replications/:id/rule', - component: ReplicationRuleComponent, - canActivate: [SystemAdminGuard], - canActivateChild: [SystemAdminGuard], - canDeactivate: [LeavingNewRuleRouteDeactivate] - }, - { - path: 'replications/new-rule', - component: ReplicationRuleComponent, - canActivate: [SystemAdminGuard], - canActivateChild: [SystemAdminGuard], - canDeactivate: [LeavingNewRuleRouteDeactivate] - }, { path: 'tags/:id/:repo', component: TagRepositoryComponent, diff --git a/src/ui_ng/src/app/replication/replication-page.component.html b/src/ui_ng/src/app/replication/replication-page.component.html index 83bea8c9b..e61451484 100644 --- a/src/ui_ng/src/app/replication/replication-page.component.html +++ b/src/ui_ng/src/app/replication/replication-page.component.html @@ -1,3 +1,3 @@
- +
\ No newline at end of file diff --git a/src/ui_ng/src/app/replication/replication-page.component.ts b/src/ui_ng/src/app/replication/replication-page.component.ts index 69c25e83d..863c36e0e 100644 --- a/src/ui_ng/src/app/replication/replication-page.component.ts +++ b/src/ui_ng/src/app/replication/replication-page.component.ts @@ -15,6 +15,8 @@ import { Component, OnInit, ViewChild, AfterViewInit } from '@angular/core'; import {ActivatedRoute, Router} from '@angular/router'; import { ReplicationComponent } from 'harbor-ui'; import {SessionService} from "../shared/session.service"; +import {Project} from "../project/project"; +import {ProjectService} from "../project/project.service"; @Component({ selector: 'replicaton', @@ -23,13 +25,25 @@ import {SessionService} from "../shared/session.service"; export class ReplicationPageComponent implements OnInit, AfterViewInit { projectIdentify: string | number; @ViewChild("replicationView") replicationView: ReplicationComponent; + projectName: string; constructor(private route: ActivatedRoute, - private router: Router, + private proService: ProjectService, private session: SessionService) { } ngOnInit(): void { this.projectIdentify = +this.route.snapshot.parent.params['id']; + + this.proService.listProjects("", undefined).toPromise() + .then(response => { + let projects = response.json() as Project[]; + if (projects.length) { + let project = projects.find(data => data.project_id === this.projectIdentify); + if (project) { + this.projectName = project.name; + } + } + }); } public get isSystemAdmin(): boolean { @@ -37,14 +51,6 @@ export class ReplicationPageComponent implements OnInit, AfterViewInit { return account != null && account.has_admin_role > 0; } - openEditPage(id: number): void { - this.router.navigate(['harbor', 'replications', id, 'rule', { projectId: this.projectIdentify}]); - } - - openCreatePage(): void { - this.router.navigate(['harbor', 'replications', 'new-rule', { projectId: this.projectIdentify}] ); - } - ngAfterViewInit(): void { let isCreated: boolean = this.route.snapshot.queryParams['is_create']; if (isCreated) { diff --git a/src/ui_ng/src/app/replication/replication-rule/list-project-model/list-project-model.component.css b/src/ui_ng/src/app/replication/replication-rule/list-project-model/list-project-model.component.css deleted file mode 100644 index ab5ec9250..000000000 --- a/src/ui_ng/src/app/replication/replication-rule/list-project-model/list-project-model.component.css +++ /dev/null @@ -1,5 +0,0 @@ -.datagrid .datagrid-head{border: 0;} -.option-right{ position: absolute; right: 30px; top: 55px;} -:host >>> .datagrid-head{height: 0;border-width: 0;} -:host >>> .datagrid-scroll-wrapper .datagrid{margin-top: 0;} -.modal-body{height: 30em; overflow-y: auto; margin-top: 20px;} diff --git a/src/ui_ng/src/app/replication/replication-rule/list-project-model/list-project-model.component.html b/src/ui_ng/src/app/replication/replication-rule/list-project-model/list-project-model.component.html deleted file mode 100644 index 0af247b58..000000000 --- a/src/ui_ng/src/app/replication/replication-rule/list-project-model/list-project-model.component.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - diff --git a/src/ui_ng/src/app/replication/replication-rule/list-project-model/list-project-model.component.ts b/src/ui_ng/src/app/replication/replication-rule/list-project-model/list-project-model.component.ts deleted file mode 100644 index bf9738c99..000000000 --- a/src/ui_ng/src/app/replication/replication-rule/list-project-model/list-project-model.component.ts +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright (c) 2017 VMware, Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -import { - Component, - Output, - Input, - ChangeDetectionStrategy, - ChangeDetectorRef, - OnDestroy, EventEmitter -} from '@angular/core'; -import { Router, NavigationExtras } from '@angular/router'; - -import { SessionService } from '../../../shared/session.service'; -import { SearchTriggerService } from '../../../base/global-search/search-trigger.service'; -import { ProjectTypes, RoleInfo} from '../../../shared/shared.const'; -import { CustomComparator, doFiltering, doSorting, calculatePage } from '../../../shared/shared.utils'; - -import { Comparator, State } from 'clarity-angular'; -import { MessageHandlerService } from '../../../shared/message-handler/message-handler.service'; -import { StatisticHandler } from '../../../shared/statictics/statistic-handler.service'; -import { Subscription } from 'rxjs/Subscription'; -import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; -import { ConfirmationMessage } from '../../../shared/confirmation-dialog/confirmation-message'; -import { ConfirmationTargets, ConfirmationState, ConfirmationButtons } from '../../../shared/shared.const'; -import {ProjectService} from "../../../project/project.service"; -import {Project} from "../../../project/project"; - -@Component({ - selector: 'list-project-model', - templateUrl: 'list-project-model.component.html', - styleUrls: ['list-project-model.component.css'], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class ListProjectModelComponent { - projectTypes = ProjectTypes; - loading: boolean = true; - projects: Project[] = []; - filteredType: number = 0;//All projects - searchKeyword: string = ""; - ismodelOpen: boolean ; - currentFilteredType: number = 0;//all projects - projectName: string = ""; - selectedProject: Project; - - roleInfo = RoleInfo; - repoCountComparator: Comparator = new CustomComparator("repo_count", "number"); - timeComparator: Comparator = new CustomComparator("creation_time", "date"); - accessLevelComparator: Comparator = new CustomComparator("public", "number"); - roleComparator: Comparator = new CustomComparator("current_user_role_id", "number"); - currentPage: number = 1; - totalCount: number = 0; - pageSize: number = 10; - currentState: State; - @Output() selectedPro = new EventEmitter(); - - constructor( - private session: SessionService, - private router: Router, - private searchTrigger: SearchTriggerService, - private proService: ProjectService, - private msgHandler: MessageHandlerService, - private statisticHandler: StatisticHandler, - private deletionDialogService: ConfirmationDialogService, - private ref: ChangeDetectorRef) { - } - - get selecteType(): number { - return this.currentFilteredType; - } - set selecteType(_project: number) { - this.currentFilteredType = _project; - if (window.sessionStorage) { - window.sessionStorage['projectTypeValue'] = _project; - } - } - - - clrLoad(state: State) { - this.selectedProject = null; - //Keep state for future filtering and sorting - this.currentState = state; - - let pageNumber: number = calculatePage(state); - if (pageNumber <= 0) { pageNumber = 1; } - - this.loading = true; - - let passInFilteredType: number = undefined; - if (this.filteredType > 0) { - passInFilteredType = this.filteredType - 1; - } - this.proService.listProjects(this.searchKeyword, passInFilteredType, pageNumber, this.pageSize).toPromise() - .then(response => { - //Get total count - if (response.headers) { - let xHeader: string = response.headers.get("X-Total-Count"); - if (xHeader) { - this.totalCount = parseInt(xHeader, 0); - } - } - - this.projects = response.json() as Project[]; - //Do customising filtering and sorting - this.projects = doFiltering(this.projects, state); - this.projects = doSorting(this.projects, state); - - this.loading = false; - }) - .catch(error => { - this.loading = false; - this.msgHandler.handleError(error); - }); - - //Force refresh view - let hnd = setInterval(() => this.ref.markForCheck(), 100); - setTimeout(() => clearInterval(hnd), 3000); - } - - openModel(): void { - this.selectedProject = null; - this.ismodelOpen = true; - //Force refresh view - let hnd = setInterval(() => this.ref.markForCheck(), 100); - setTimeout(() => clearInterval(hnd), 2000); - } - - refresh(): void { - this.currentPage = 1; - this.filteredType = 0; - this.searchKeyword = ''; - - this.reload(); - } - - doFilterProject(): void { - this.currentPage = 1; - this.filteredType = this.selecteType; - this.reload(); - } - - doSearchProject(proName: string): void { - this.projectName = proName; - this.currentPage = 1; - this.searchKeyword = proName; - this.reload(); - } - - reload(): void { - let st: State = this.currentState; - if (!st) { - st = { - page: {} - }; - } - st.page.from = 0; - st.page.to = this.pageSize - 1; - st.page.size = this.pageSize; - - this.clrLoad(st); - } - - oKModel() { - this.ismodelOpen = false; - this.selectedPro.emit(this.selectedProject); - } - - closeModel(): void { - this.ismodelOpen = false; - } -} diff --git a/src/ui_ng/src/app/replication/replication-rule/replication-rule.component.ts b/src/ui_ng/src/app/replication/replication-rule/replication-rule.component.ts deleted file mode 100644 index f45431806..000000000 --- a/src/ui_ng/src/app/replication/replication-rule/replication-rule.component.ts +++ /dev/null @@ -1,590 +0,0 @@ -import {Component, OnInit, OnDestroy, ViewChild, ChangeDetectorRef, AfterViewInit} from '@angular/core'; -import {ProjectService} from '../../project/project.service'; -import {Project} from '../../project/project'; -import {ActivatedRoute, Router} from '@angular/router'; -import {FormArray, FormBuilder, FormGroup, Validators} from "@angular/forms"; -import {ReplicationRuleServie} from "./replication-rule.service"; -import {MessageHandlerService} from "../../shared/message-handler/message-handler.service"; -import {Target, Filter, ReplicationRule} from "./replication-rule"; -import {ConfirmationDialogService} from "../../shared/confirmation-dialog/confirmation-dialog.service"; -import { ConfirmationTargets, ConfirmationState } from '../../shared/shared.const'; -import {Subscription} from "rxjs/Subscription"; -import {ConfirmationMessage} from "../../shared/confirmation-dialog/confirmation-message"; -import {Subject} from "rxjs/Subject"; -import {ListProjectModelComponent} from "./list-project-model/list-project-model.component"; -import {toPromise, isEmptyObject, compareValue} from "harbor-ui/src/utils"; -import {CreateEditEndpointComponent} from "harbor-ui/src/create-edit-endpoint/create-edit-endpoint.component"; - -const ONE_HOUR_SECONDS: number = 3600; -const ONE_DAY_SECONDS: number = 24 * ONE_HOUR_SECONDS; - -@Component ({ - selector: 'repliction-rule', - templateUrl: 'replication-rule.html', - styleUrls: ['replication-rule.css'] - -}) - -export class ReplicationRuleComponent implements OnInit, OnDestroy { - _localTime: Date = new Date(); - policyId: number; - projectId: number; - targetList: Target[] = []; - isFilterHide: boolean = false; - weeklySchedule: boolean; - isScheduleOpt: boolean; - isImmediate: boolean = false; - noProjectInfo: string = ""; - noSelectedProject: boolean = true; - noSelectedEndpoint: boolean = true; - filterCount: number = 0; - selectedprojectList: Project[] = []; - triggerNames: string[] = ['Manual', 'Immediate', 'Scheduled']; - scheduleNames: string[] = ['Daily', 'Weekly']; - weekly: string[] = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; - filterSelect: string[] = ['repository', 'tag']; - ruleNameTooltip: string = 'TOOLTIP.EMPTY'; - headerTitle: string = 'REPLICATION.ADD_POLICY'; - - filterListData: {[key: string]: any}[] = []; - inProgress: boolean = false; - inNameChecking: boolean = false; - isRuleNameExist: boolean = false; - isSubmitOver: boolean = false; - nameChecker: Subject = new Subject(); - - confirmSub: Subscription; - ruleForm: FormGroup; - copyUpdateForm: ReplicationRule; - emptyEndpoint = new Target(); - - @ViewChild(ListProjectModelComponent) - projectListModel: ListProjectModelComponent; - - @ViewChild(CreateEditEndpointComponent) - createEditEndpointComponent: CreateEditEndpointComponent; - - baseFilterData(name: string, option: string[], state: boolean) { - return { - name: name, - options: option, - state: state, - isValid: true - }; - } - - constructor(public projectService: ProjectService, - private router: Router, - private fb: FormBuilder, - private repService: ReplicationRuleServie, - private route: ActivatedRoute, - private msgHandler: MessageHandlerService, - private confirmService: ConfirmationDialogService, - public ref: ChangeDetectorRef) { - this.createForm(); - Promise.all([this.repService.getEndpoints(), this.repService.listProjects()]) - .then(res => { - if (!res[0]) { - this.noSelectedEndpoint = true; - }else { - this.targetList = res[0]; - if (!this.policyId) { - res[0].unshift(this.emptyEndpoint); - this.setTarget([res[0][0]]); - } - } - if (!res[1]) { - this.noProjectInfo = 'REPLICATION.NO_PROJECT_INFO'; - }else { - if (!this.policyId && !this.projectId) { - this.setProject([res[1][0]]); - } - if (!this.policyId && this.projectId) { - this.setProject( res[1].filter(rule => rule.project_id === this.projectId)); - this.noSelectedProject = false; - } - } - if (!this.policyId) { - this.copyUpdateForm = Object.assign({}, this.ruleForm.value); - } - }); - } - - ngOnInit(): void { - this.policyId = +this.route.snapshot.params['id']; - this.projectId = +this.route.snapshot.params['projectId']; - if (this.policyId) { - this.headerTitle = 'REPLICATION.EDIT_POLICY_TITLE'; - this.repService.getReplicationRule(this.policyId) - .then((response) => { - this.copyUpdateForm = Object.assign({}, response); - // set filter value is [] if callback fiter value is null. - this.copyUpdateForm.filters = response.filters ? response.filters : []; - this.updateForm(response); - }).catch(error => { - this.msgHandler.handleError(error); - }); - } - - this.nameChecker.debounceTime(500).distinctUntilChanged().subscribe((ruleName: string) => { - this.isRuleNameExist = false; - this.inNameChecking = true; - toPromise(this.repService.getReplicationRules(0, ruleName)) - .then(response => { - if (response.some(rule => rule.name === ruleName)) { - this.ruleNameTooltip = 'TOOLTIP.RULE_USER_EXISTING'; - this.isRuleNameExist = true; - } - this.inNameChecking = false; - }).catch(() => { - this.inNameChecking = false; - }); - }); - } - - ngOnDestroy(): void { - if (this.confirmSub) { - this.confirmSub.unsubscribe(); - } - if (this.nameChecker) { - this.nameChecker.unsubscribe(); - } - } - - get isVaild() { - return !(this.isRuleNameExist || this.noSelectedProject || this.noSelectedEndpoint || this.inProgress || this.isSubmitOver); - } - - createForm() { - this.ruleForm = this.fb.group({ - name: ['', Validators.required], - description: '', - projects: this.fb.array([]), - targets: this.fb.array([]), - trigger: this.fb.group({ - kind: this.triggerNames[0], - schedule_param: this.fb.group({ - type: this.scheduleNames[0], - weekday: 1, - offtime: '08:00' - }), - }), - filters: this.fb.array([]), - replicate_existing_image_now: true, - replicate_deletion: false - }); - } - - updateForm(rule: ReplicationRule): void { - rule.trigger = this.updateTrigger(rule.trigger); - this.ruleForm.reset({ - name: rule.name, - description: rule.description, - trigger: rule.trigger, - replicate_existing_image_now: rule.replicate_existing_image_now, - replicate_deletion: rule.replicate_deletion - }); - this.setProject(rule.projects); - this.noSelectedProject = false; - this.setTarget(rule.targets); - this.noSelectedEndpoint = false; - - if (rule.filters) { - this.setFilter(rule.filters); - this.updateFilter(rule.filters); - } - - // Force refresh view - let hnd = setInterval(() => this.ref.markForCheck(), 100); - setTimeout(() => clearInterval(hnd), 2000); - } - - get projects(): FormArray { - return this.ruleForm.get('projects') as FormArray; - } - setProject(projects: Project[]) { - const projectFGs = projects.map(project => this.fb.group(project)); - const projectFormArray = this.fb.array(projectFGs); - this.ruleForm.setControl('projects', projectFormArray); - } - - get filters(): FormArray { - return this.ruleForm.get('filters') as FormArray; - } - setFilter(filters: Filter[]) { - const filterFGs = filters.map(filter => this.fb.group(filter)); - const filterFormArray = this.fb.array(filterFGs); - this.ruleForm.setControl('filters', filterFormArray); - } - - get targets(): FormArray { - return this.ruleForm.get('targets') as FormArray; - } - setTarget(targets: Target[]) { - const targetFGs = targets.map(target => this.fb.group(target)); - const targetFormArray = this.fb.array(targetFGs); - this.ruleForm.setControl('targets', targetFormArray); - } - - initFilter(name: string) { - return this.fb.group({ - kind: name, - pattern: ['', Validators.required] - }); - } - - filterChange($event: any) { - if ($event && $event.target['value']) { - let id: number = $event.target.id; - let name: string = $event.target.name; - let value: string = $event.target['value']; - - this.filterListData.forEach((data, index) => { - if (index === +id) { - data.name = $event.target.name = value; - }else { - data.options.splice(data.options.indexOf(value), 1); - } - if (data.options.indexOf(name) === -1) { - data.options.push(name); - } - }); - } - } - - targetChange($event: any) { - if ($event && $event.target && event.target['value']) { - if ($event.target['value'] === '-1') { - this.noSelectedEndpoint = true; - return; - } - let selecedTarget: Target = this.targetList.find(target => target.id === +$event.target['value']); - this.setTarget([selecedTarget]); - this.noSelectedEndpoint = false; - } - } - - openProjectModel(): void { - this.projectListModel.openModel(); - } - - selectedProject(project: Project): void { - if (!project) { - this.noSelectedProject = true; - }else { - this.noSelectedProject = false; - this.setProject([project]); - } - } - - addNewFilter(): void { - if (this.filterCount === 0) { - this.filterListData.push(this.baseFilterData(this.filterSelect[0], this.filterSelect.slice(), true)); - this.filters.push(this.initFilter(this.filterSelect[0])); - - }else { - let nameArr: string[] = this.filterSelect.slice(); - this.filterListData.forEach(data => { - nameArr.splice(nameArr.indexOf(data.name), 1); - }); - // when add a new filter,the filterListData should change the options - this.filterListData.filter((data) => { - data.options.splice(data.options.indexOf(nameArr[0]), 1); - }); - this.filterListData.push(this.baseFilterData(nameArr[0], nameArr, true)); - this.filters.push(this.initFilter(nameArr[0])); - } - this.filterCount += 1; - if (this.filterCount >= this.filterSelect.length) { - this.isFilterHide = true; - } - } - - // delete a filter - deleteFilter(i: number): void { - if (i || i === 0) { - let delfilter = this.filterListData.splice(i, 1)[0]; - if (this.filterCount === this.filterSelect.length) { - this.isFilterHide = false; - } - this.filterCount -= 1; - if (this.filterListData.length) { - let optionVal = delfilter.name; - this.filterListData.filter(data => { - if (data.options.indexOf(optionVal) === -1) { - data.options.push(optionVal); - } - }); - } - const control = this.ruleForm.controls['filters']; - control.removeAt(i); - } - } - - selectTrigger($event: any): void { - if ($event && $event.target && $event.target['value']) { - let val: string = $event.target['value']; - if (val === this.triggerNames[2]) { - this.isScheduleOpt = true; - this.isImmediate = false; - } - if (val === this.triggerNames[1]) { - this.isScheduleOpt = false; - this.isImmediate = true; - } - if (val === this.triggerNames[0]) { - this.isScheduleOpt = false; - this.isImmediate = false; - } - } - } - - // Replication Schedule select value exchange - selectSchedule($event: any): void { - if ($event && $event.target && $event.target['value']) { - switch ($event.target['value']) { - case this.scheduleNames[1]: - this.weeklySchedule = true; - this.ruleForm.patchValue({ - trigger: { - schedule_param: { - weekday: 1, - } - } - }) - break; - case this.scheduleNames[0]: - this.weeklySchedule = false; - break; - } - } - } - - checkRuleName(): void { - let ruleName: string = this.ruleForm.controls['name'].value; - if (ruleName) { - this.nameChecker.next(ruleName); - } else { - this.ruleNameTooltip = 'TOOLTIP.EMPTY'; - } - } - - updateFilter(filters: any) { - let opt: string[] = this.filterSelect.slice(); - filters.forEach((filter: any) => { - opt.splice(opt.indexOf(filter.kind), 1); - }) - filters.forEach((filter: any) => { - let option: string [] = opt.slice(); - option.unshift(filter.kind); - this.filterListData.push(this.baseFilterData(filter.kind, option, true)); - }); - this.filterCount = filters.length; - if (filters.length === this.filterSelect.length) { - this.isFilterHide = true; - } - } - - updateTrigger(trigger: any) { - if (trigger['schedule_param']) { - this.isScheduleOpt = true; - this.isImmediate = false; - trigger['schedule_param']['offtime'] = this.getOfftime(trigger['schedule_param']['offtime']); - if (trigger['schedule_param']['weekday']) { - this.weeklySchedule = true; - }else { - // set default - trigger['schedule_param']['weekday'] = 1; - } - }else { - if (trigger['kind'] === this.triggerNames[0]) { - this.isImmediate = false; - } - trigger['schedule_param'] = { type: this.scheduleNames[0], - weekday: this.weekly[0], - offtime: '08:00'}; - } - return trigger; - } - - setTriggerVaule(trigger: any) { - if (!this.isScheduleOpt) { - delete trigger['schedule_param']; - return trigger; - }else { - if (!this.weeklySchedule) { - delete trigger['schedule_param']['weekday']; - }else { - trigger['schedule_param']['weekday'] = +trigger['schedule_param']['weekday']; - } - trigger['schedule_param']['offtime'] = this.setOfftime(trigger['schedule_param']['offtime']); - return trigger; - } - } - - public hasFormChange(): boolean { - return !isEmptyObject(this.getChanges()); - } - - onSubmit() { - // add new Replication rule - this.inProgress = true; - let copyRuleForm: ReplicationRule = this.ruleForm.value; - copyRuleForm.trigger = this.setTriggerVaule(copyRuleForm.trigger); - if (!this.policyId) { - this.repService.createReplicationRule(copyRuleForm) - .then(() => { - this.msgHandler.showSuccess('REPLICATION.CREATED_SUCCESS'); - this.inProgress = false; - this.isSubmitOver = true; - setTimeout(() => { - this.copyUpdateForm = Object.assign({}, this.ruleForm.value); - if (this.projectId) { - this.router.navigate(['harbor/projects', this.projectId, 'replications']); - }else { - this.router.navigate(['/harbor/replications']); - } - }, 2000); - - }).catch((error: any) => { - this.inProgress = false; - this.msgHandler.handleError(error); - }); - } else { - this.repService.updateReplicationRule(this.policyId, this.ruleForm.value) - .then(() => { - this.msgHandler.showSuccess('REPLICATION.UPDATED_SUCCESS'); - this.inProgress = false; - this.isSubmitOver = true; - setTimeout(() => { - this.copyUpdateForm = Object.assign({}, this.ruleForm.value); - if (this.projectId) { - this.router.navigate(['harbor/projects', this.projectId, 'replications']); - }else { - this.router.navigate(['/harbor/replications']); - } - }, 2000); - - }).catch((error: any) => { - this.inProgress = false; - this.msgHandler.handleError(error); - }); - } - } - - openModal() { - this.createEditEndpointComponent.openCreateEditTarget(true); - } - - reload($event: boolean) { - if ($event) { - Promise.all([this.repService.getEndpoints()]).then(res => { - this.targetList = res[0]; - this.setTarget([this.targetList[this.targetList.length - 1]]); - this.noSelectedEndpoint = false; - }); - } - } - - onCancel(): void { - this.router.navigate(['/harbor/replications']); - } - - // UTC time - public getOfftime(daily_time: any): string { - - let timeOffset: number = 0; // seconds - if (daily_time && typeof daily_time === 'number') { - timeOffset = +daily_time; - } - - // Convert to current time - let timezoneOffset: number = this._localTime.getTimezoneOffset(); - // Local time - timeOffset = timeOffset - timezoneOffset * 60; - if (timeOffset < 0) { - timeOffset = timeOffset + ONE_DAY_SECONDS; - } - - if (timeOffset >= ONE_DAY_SECONDS) { - timeOffset -= ONE_DAY_SECONDS; - } - - // To time string - let hours: number = Math.floor(timeOffset / ONE_HOUR_SECONDS); - let minutes: number = Math.floor((timeOffset - hours * ONE_HOUR_SECONDS) / 60); - - let timeStr: string = '' + hours; - if (hours < 10) { - timeStr = '0' + timeStr; - } - if (minutes < 10) { - timeStr += ':0'; - } else { - timeStr += ':'; - } - timeStr += minutes; - - return timeStr; - } - public setOfftime(v: string) { - if (!v || v === '') { - return; - } - - let values: string[] = v.split(':'); - if (!values || values.length !== 2) { - return; - } - - let hours: number = +values[0]; - let minutes: number = +values[1]; - // Convert to UTC time - let timezoneOffset: number = this._localTime.getTimezoneOffset(); - let utcTimes: number = hours * ONE_HOUR_SECONDS + minutes * 60; - utcTimes += timezoneOffset * 60; - if (utcTimes < 0) { - utcTimes += ONE_DAY_SECONDS; - } - - if (utcTimes >= ONE_DAY_SECONDS) { - utcTimes -= ONE_DAY_SECONDS; - } - - return utcTimes; - } - - backReplication(): void { - this.router.navigate(['/harbor/replications']); - } - backProjectReplication(): void { - this.router.navigate(['harbor/projects', this.projectId, 'replications']); - } - - - getChanges(): { [key: string]: any | any[] } { - let changes: { [key: string]: any | any[] } = {}; - let ruleValue: { [key: string]: any | any[] } = this.ruleForm.value; - if (!ruleValue || !this.copyUpdateForm) { - return changes; - } - for (let prop in ruleValue) { - let field = this.copyUpdateForm[prop]; - if (!compareValue(field, ruleValue[prop])) { - changes[prop] = ruleValue[prop]; - //Number - if (typeof field === "number") { - changes[prop] = +changes[prop]; - } - - //Trim string value - if (typeof field === "string") { - changes[prop] = ('' + changes[prop]).trim(); - } - } - } - - return changes; - } - -} diff --git a/src/ui_ng/src/app/replication/replication-rule/replication-rule.css b/src/ui_ng/src/app/replication/replication-rule/replication-rule.css deleted file mode 100644 index 4942247e5..000000000 --- a/src/ui_ng/src/app/replication/replication-rule/replication-rule.css +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Created by pengf on 9/28/2017. - */ - -.select{ - width: 186px; -} -.select .optionMore{ - background-color: #bfbaba; - height: 1.6em; - font-size: 1.2em; - cursor: pointer; - text-align: center; -} -.hideFilter{ display: none;} -h4{ - color: #666; -} -.colorRed{color: red;} -.colorRed a{text-decoration: underline;color: #007CBB;} - -label:first-child { - font-size: 15px; - left: -10px !important; -} -.inputWidth{width: 310px;} -.endpointSelect{ width: 290px; margin-right: 34px;} -.filterSelect{width: 350px;} -.filterSelect clr-icon{margin-left: 10px;} -.filterSelect label{width: 175px;} -.filterSelect label input{width: 100%;} -.cursor{cursor: pointer;} -.pull-left{float: left;} -.padLeft0{padding-left: 0;} -.floatSet {display: inline-block; width: 120px;margin-right: 10px;} -.form-group{ min-height: 36px;} - -.projectInput{float: left;} -.projectInput input{background-color: white;} -.switchIcon{width:20px;height:20px; margin-top: 10px;margin-left: 10px; cursor: pointer;} -.addEndpoint{ margin-top: .25em !important;} -.shadow{position: absolute;top: 8px;} -.is-solid{cursor: pointer;} \ No newline at end of file diff --git a/src/ui_ng/src/app/replication/replication-rule/replication-rule.html b/src/ui_ng/src/app/replication/replication-rule/replication-rule.html deleted file mode 100644 index b8a1b2bd6..000000000 --- a/src/ui_ng/src/app/replication/replication-rule/replication-rule.html +++ /dev/null @@ -1,128 +0,0 @@ -
- < {{'SIDE_NAV.SYSTEM_MGMT.REPLICATION' | translate}} - <{{'SIDE_NAV.PROJECTS' | translate}}   {{'SIDE_NAV.SYSTEM_MGMT.REPLICATION' | translate | lowercase}} -

{{headerTitle | translate}}

-
-
-
- - -
- -
- - -
- -
- -
-
- -
-
- - -
-
- - -
- -
-
-
-
- -
- - -
-
-
- -
- -
- -
-
- -
- -
-
- - -
- -
- -
- -
- -
-
- -
- - on    -
- -
- - at    - -
-
-
- - {{'REPLICATION.DELETE_REMOTE_IMAGES' | translate}} - -
-
- - {{'REPLICATION.REPLICATE_IMMEDIATE' | translate}} - -
-
- -
- -
- - -
-
-
- - -
\ No newline at end of file diff --git a/src/ui_ng/src/app/replication/replication-rule/replication-rule.service.ts b/src/ui_ng/src/app/replication/replication-rule/replication-rule.service.ts deleted file mode 100644 index fbe5a1c4c..000000000 --- a/src/ui_ng/src/app/replication/replication-rule/replication-rule.service.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Created by pengf on 12/5/2017. - */ - -import {Injectable} from "@angular/core"; -import {Http, RequestOptions, Headers, URLSearchParams} from "@angular/http"; -import {Observable} from "rxjs/Observable"; -import {ReplicationRule, Target} from "./replication-rule"; -import {HTTP_GET_OPTIONS, HTTP_JSON_OPTIONS} from "../../shared/shared.utils"; -import {Project} from "../../project/project"; - -@Injectable() -export class ReplicationRuleServie { - headers = new Headers({'Content-type': 'application/json'}); - options = new RequestOptions({'headers': this.headers}); - baseurl = '/api/policies/replication'; - targetUrl= '/api/targets'; - - constructor(private http: Http) {} - - public createReplicationRule(replicationRule: ReplicationRule): Observable | Promise | any { - /*if (!this._isValidRule(replicationRule)) { - return Promise.reject('Bad argument'); - }*/ - - return this.http.post(this.baseurl, JSON.stringify(replicationRule), this.options).toPromise() - .then(response => response) - .catch(error => Promise.reject(error)); - } - - public getReplicationRules(projectId?: number | string, ruleName?: string): Promise | ReplicationRule[] { - let queryParams = new URLSearchParams(); - if (projectId) { - queryParams.set('project_id', '' + projectId); - } - - if (ruleName) { - queryParams.set('name', ruleName); - } - - return this.http.get(this.baseurl, {search: queryParams}).toPromise() - .then(response => response.json() as ReplicationRule[]) - .catch(error => Promise.reject(error)); - } - - public getReplicationRule(policyId: number): Promise { - let url: string = `${this.baseurl}/${policyId}`; - return this.http.get(url, HTTP_GET_OPTIONS).toPromise() - .then(response => response.json() as ReplicationRule) - .catch(error => Promise.reject(error)); - } - - - public getEndpoints(): Promise | Target[] { - return this.http - .get(this.targetUrl) - .toPromise() - .then(response => response.json()) - .catch(error => Promise.reject(error)); - } - - public listProjects(): Promise | Project[] { - return this.http.get(`/api/projects`, HTTP_GET_OPTIONS).toPromise() - .then(response => response.json()) - .catch(error => Promise.reject(error)); - } - - public updateReplicationRule(id: number, rep: {[key: string]: any | any[] }): Observable | Promise | any { - let url: string = `${this.baseurl}/${id}`; - return this.http.put(url, JSON.stringify(rep), HTTP_JSON_OPTIONS).toPromise() - .then(response => response) - .catch(error => Promise.reject(error)); - } - -} diff --git a/src/ui_ng/src/app/replication/replication-rule/replication-rule.ts b/src/ui_ng/src/app/replication/replication-rule/replication-rule.ts deleted file mode 100644 index bd320c9fa..000000000 --- a/src/ui_ng/src/app/replication/replication-rule/replication-rule.ts +++ /dev/null @@ -1,60 +0,0 @@ -import {Project} from "../../project/project"; -/** - * Created by pengf on 12/7/2017. - */ - -export class Target { - id: number; - endpoint: string; - name: string; - username: string; - password: string; - type: number; - insecure: true; - creation_time: string; - update_time: string; - constructor() { - this.id = -1; - this.endpoint = ""; - this.name = ""; - this.username = ""; - this.password = ""; - this.type = 0; - this.insecure = true; - this.creation_time = ""; - this.update_time = ""; - } -} - -export class Filter { - kind: string; - pattern: string; - constructor(kind: string, pattern: string) { - this.kind = kind; - this.pattern = pattern; - } -} - -export class Trigger { - kind: string; - schedule_param: any | { - [key: string]: any | any[]; - }; - constructor(kind: string, param: any | { [key: string]: any | any[]; }) { - this.kind = kind; - this.schedule_param = param; - } -} - -export interface ReplicationRule { - id?: number; - name: string; - description: string; - projects: Project[]; - targets: Target[] ; - trigger: Trigger ; - filters: Filter[] ; - replicate_existing_image_now?: boolean; - replicate_deletion?: boolean; -} - diff --git a/src/ui_ng/src/app/replication/replication.module.ts b/src/ui_ng/src/app/replication/replication.module.ts index 06cfca749..b8ca09742 100644 --- a/src/ui_ng/src/app/replication/replication.module.ts +++ b/src/ui_ng/src/app/replication/replication.module.ts @@ -20,10 +20,7 @@ import { TotalReplicationPageComponent } from './total-replication/total-replica import { DestinationPageComponent } from './destination/destination-page.component'; import { SharedModule } from '../shared/shared.module'; -import {ReplicationRuleComponent} from "./replication-rule/replication-rule.component"; import {ReactiveFormsModule} from "@angular/forms"; -import {ReplicationRuleServie} from "./replication-rule/replication-rule.service"; -import {ListProjectModelComponent} from "./replication-rule/list-project-model/list-project-model.component"; @NgModule({ imports: [ @@ -36,15 +33,11 @@ import {ListProjectModelComponent} from "./replication-rule/list-project-model/l ReplicationManagementComponent, TotalReplicationPageComponent, DestinationPageComponent, - ReplicationRuleComponent, - ListProjectModelComponent, ], exports: [ ReplicationPageComponent, DestinationPageComponent, TotalReplicationPageComponent, - ReplicationRuleComponent, - ], - providers: [ReplicationRuleServie] + ] }) export class ReplicationModule { } \ No newline at end of file diff --git a/src/ui_ng/src/app/replication/total-replication/total-replication-page.component.html b/src/ui_ng/src/app/replication/total-replication/total-replication-page.component.html index c4ef6f6e5..799257e08 100644 --- a/src/ui_ng/src/app/replication/total-replication/total-replication-page.component.html +++ b/src/ui_ng/src/app/replication/total-replication/total-replication-page.component.html @@ -1,4 +1,4 @@

{{'SIDE_NAV.SYSTEM_MGMT.REPLICATION' | translate}}

- +
\ No newline at end of file diff --git a/src/ui_ng/src/app/replication/total-replication/total-replication-page.component.ts b/src/ui_ng/src/app/replication/total-replication/total-replication-page.component.ts index c9e951046..2d38707a7 100644 --- a/src/ui_ng/src/app/replication/total-replication/total-replication-page.component.ts +++ b/src/ui_ng/src/app/replication/total-replication/total-replication-page.component.ts @@ -13,9 +13,10 @@ // limitations under the License. import { Component } from '@angular/core'; -import {Router,ActivatedRoute} from "@angular/router"; -import {ReplicationRule} from "../replication-rule/replication-rule"; +import {Router, ActivatedRoute} from "@angular/router"; + import {SessionService} from "../../shared/session.service"; +import {ReplicationRule} from "harbor-ui"; @Component({ selector: 'total-replication', @@ -31,17 +32,12 @@ export class TotalReplicationPageComponent { this.router.navigate(['../projects', rule.projects[0].project_id, 'replications'], { relativeTo: this.activeRoute }); } } + goRegistry(): void { + this.router.navigate(['../registries'], { relativeTo: this.activeRoute }); + } public get isSystemAdmin(): boolean { let account = this.session.getCurrentUser(); return account != null && account.has_admin_role > 0; } - - openEditPage(id: number): void { - this.router.navigate([id, 'rule'], { relativeTo: this.activeRoute }); - } - - openCreatePage(): void { - this.router.navigate(['new-rule'], { relativeTo: this.activeRoute }); - } } diff --git a/src/ui_ng/src/app/shared/route/leaving-new-rule-deactivate.service.ts b/src/ui_ng/src/app/shared/route/leaving-new-rule-deactivate.service.ts deleted file mode 100644 index 60ea8de74..000000000 --- a/src/ui_ng/src/app/shared/route/leaving-new-rule-deactivate.service.ts +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) 2017 VMware, Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -import { Injectable } from '@angular/core'; -import { - CanDeactivate, Router, - ActivatedRouteSnapshot, - RouterStateSnapshot -} from '@angular/router'; - -import { ConfirmationDialogService } from '../confirmation-dialog/confirmation-dialog.service'; - -import { ConfigurationComponent } from '../../config/config.component'; -import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message'; -import { ConfirmationState, ConfirmationTargets } from '../shared.const'; -import {ReplicationRuleComponent} from "../../replication/replication-rule/replication-rule.component"; - -@Injectable() -export class LeavingNewRuleRouteDeactivate implements CanDeactivate { - constructor( - private router: Router, - private confirmation: ConfirmationDialogService) { } - - canDeactivate( - replicateRule: ReplicationRuleComponent, - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot): Promise | boolean { - //Confirmation before leaving config route - return new Promise((resolve, reject) => { - if (replicateRule && replicateRule.hasFormChange()) { - let msg: ConfirmationMessage = new ConfirmationMessage( - "CONFIG.LEAVING_CONFIRMATION_TITLE", - "CONFIG.LEAVING_CONFIRMATION_SUMMARY", - '', - {}, - ConfirmationTargets.CONFIG_ROUTE - ); - this.confirmation.openComfirmDialog(msg); - return this.confirmation.confirmationConfirm$.subscribe(msg => { - if (msg && msg.source === ConfirmationTargets.CONFIG_ROUTE) { - if (msg.state === ConfirmationState.CONFIRMED) { - return resolve(true); - } else { - return resolve(false);//Prevent leading route - } - } else { - return resolve(true);//Should go on - } - }); - } else { - return resolve(true); - } - }); - } -} diff --git a/src/ui_ng/src/app/shared/shared.module.ts b/src/ui_ng/src/app/shared/shared.module.ts index 8d0e34a13..7de6142ae 100644 --- a/src/ui_ng/src/app/shared/shared.module.ts +++ b/src/ui_ng/src/app/shared/shared.module.ts @@ -58,7 +58,6 @@ import { ErrorHandler, HarborLibraryModule } from 'harbor-ui'; -import {LeavingNewRuleRouteDeactivate} from "./route/leaving-new-rule-deactivate.service"; import { LeavingRepositoryRouteDeactivate } from './route/leaving-repository-deactivate.service'; const uiLibConfig: IServiceConfig = { @@ -125,7 +124,6 @@ const uiLibConfig: IServiceConfig = { AuthCheckGuard, SignInGuard, LeavingConfigRouteDeactivate, - LeavingNewRuleRouteDeactivate, LeavingRepositoryRouteDeactivate, MemberGuard, MessageHandlerService, diff --git a/src/ui_ng/src/i18n/lang/en-us-lang.json b/src/ui_ng/src/i18n/lang/en-us-lang.json index 0d109d8f5..a8392613d 100644 --- a/src/ui_ng/src/i18n/lang/en-us-lang.json +++ b/src/ui_ng/src/i18n/lang/en-us-lang.json @@ -322,8 +322,8 @@ "PLACEHOLDER": "We couldn't find any replication rules!", "JOB_PLACEHOLDER": "We couldn't find any replication jobs!", "JOB_LOG_VIEWER": "View Replication Job Log", - "NO_ENDPOINT_INFO": "Please go to registries and add an endpoint first", - "NO_PROJECT_INFO": "Please go to projects and add a project name first", + "NO_ENDPOINT_INFO": "Please add an endpoint first", + "NO_PROJECT_INFO": "Please add a project name first", "SOURCE_IMAGES_FILTER": "Source images filter", "SCHEDULE": "Scheduled", "MANUAL": "Manual", diff --git a/src/ui_ng/src/i18n/lang/es-es-lang.json b/src/ui_ng/src/i18n/lang/es-es-lang.json index 010f956c6..a7d8ea389 100644 --- a/src/ui_ng/src/i18n/lang/es-es-lang.json +++ b/src/ui_ng/src/i18n/lang/es-es-lang.json @@ -322,8 +322,8 @@ "PLACEHOLDER": "We couldn't find any replication rules!", "JOB_PLACEHOLDER": "We couldn't find any replication jobs!", "JOB_LOG_VIEWER": "View Replication Job Log", - "NO_ENDPOINT_INFO": "Please go to registries and add an endpoint first", - "NO_PROJECT_INFO": "Please go to projects and add a project name first", + "NO_ENDPOINT_INFO": "Please add an endpoint first", + "NO_PROJECT_INFO": "Please add a project name first", "SOURCE_IMAGES_FILTER": "Source images filter", "SCHEDULE": "Scheduled", "MANUAL": "Manual", diff --git a/src/ui_ng/src/i18n/lang/zh-cn-lang.json b/src/ui_ng/src/i18n/lang/zh-cn-lang.json index 68e8a7b42..a9464381b 100644 --- a/src/ui_ng/src/i18n/lang/zh-cn-lang.json +++ b/src/ui_ng/src/i18n/lang/zh-cn-lang.json @@ -322,8 +322,8 @@ "PLACEHOLDER": "未发现任何复制规则!", "JOB_PLACEHOLDER": "未发现任何复制任务!", "JOB_LOG_VIEWER": "查看复制任务日志", - "NO_ENDPOINT_INFO": "请先添加目标", - "NO_PROJECT_INFO": "请先去项目添加一个新的项目名称", + "NO_ENDPOINT_INFO": "请先添加一个目标", + "NO_PROJECT_INFO": "请先添加一个项目名称", "SOURCE_IMAGES_FILTER": "源镜像过滤器", "SCHEDULE": "定时", "MANUAL": "手动", From 8bf4f1314e69c864dbae5f57aad3127e727e512f Mon Sep 17 00:00:00 2001 From: Yan Date: Wed, 7 Mar 2018 15:19:09 +0800 Subject: [PATCH 02/21] fix nightly bug for notary case in replication env (#4355) --- tests/resources/Nightly-Util.robot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/resources/Nightly-Util.robot b/tests/resources/Nightly-Util.robot index f3c0827f2..f65c881b4 100644 --- a/tests/resources/Nightly-Util.robot +++ b/tests/resources/Nightly-Util.robot @@ -24,7 +24,7 @@ Nightly Test Setup [Arguments] ${ip} ${SSH_PWD} ${HARBOR_PASSWORD} ${ip1}==${EMPTY} Run Keyword CA setup ${ip} ${SSH_PWD} ${HARBOR_PASSWORD} Run Keyword Prepare Docker Cert ${ip} - Run Keyword And Ignore Error Run rm harbor_ca.crt + Run Keyword If '${ip1}' != '${EMPTY}' Run rm harbor_ca.crt Run Keyword If '${ip1}' != '${EMPTY}' CA setup ${ip1} ${SSH_PWD} ${HARBOR_PASSWORD} Run Keyword If '${ip1}' != '${EMPTY}' Prepare Docker Cert ${ip1} Run Keyword Start Docker Daemon Locally From 805e4aa4db1619c24963c19a7c6ede72293dce6e Mon Sep 17 00:00:00 2001 From: pfh Date: Wed, 7 Mar 2018 17:01:00 +0800 Subject: [PATCH 03/21] Modify fileter problem and input length #4185 #4264 --- .../src/app/shared/new-user-form/new-user-form.component.html | 2 +- src/ui_ng/src/app/user/user.component.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ui_ng/src/app/shared/new-user-form/new-user-form.component.html b/src/ui_ng/src/app/shared/new-user-form/new-user-form.component.html index b6a469e64..03f462614 100644 --- a/src/ui_ng/src/app/shared/new-user-form/new-user-form.component.html +++ b/src/ui_ng/src/app/shared/new-user-form/new-user-form.component.html @@ -32,7 +32,7 @@
- {{'SIDE_NAV.SYSTEM_MGMT.REGISTRY' | translate}} + {{'SIDE_NAV.SYSTEM_MGMT.REGISTRY' | translate}} diff --git a/src/ui_ng/lib/src/datetime-picker/datetime-picker.component.ts b/src/ui_ng/lib/src/datetime-picker/datetime-picker.component.ts index 72270a4e9..7eb061849 100644 --- a/src/ui_ng/lib/src/datetime-picker/datetime-picker.component.ts +++ b/src/ui_ng/lib/src/datetime-picker/datetime-picker.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core'; +import {Component, Input, Output, EventEmitter, ViewChild, OnChanges} from '@angular/core'; import { NgModel } from '@angular/forms'; import { DATETIME_PICKER_TEMPLATE } from './datetime-picker.component.html'; @@ -7,7 +7,7 @@ import { DATETIME_PICKER_TEMPLATE } from './datetime-picker.component.html'; selector: 'hbr-datetime', template: DATETIME_PICKER_TEMPLATE }) -export class DatePickerComponent { +export class DatePickerComponent implements OnChanges{ @Input() dateInput: string; @Input() oneDayOffset: boolean; @@ -17,6 +17,10 @@ export class DatePickerComponent { @Output() search = new EventEmitter(); + ngOnChanges(): void { + this.dateInput = this.dateInput.trim(); + } + get dateInvalid(): boolean { return (this.searchTime.errors && this.searchTime.errors.dateValidator && (this.searchTime.dirty || this.searchTime.touched)) || false; } diff --git a/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.html.ts b/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.html.ts index 47c006bad..327c8d334 100644 --- a/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.html.ts +++ b/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.html.ts @@ -1,11 +1,11 @@ export const LIST_REPLICATION_RULE_TEMPLATE: string = `
- - - - - + + + + + {{'REPLICATION.NAME' | translate}} {{'REPLICATION.PROJECT' | translate}} diff --git a/src/ui_ng/lib/src/replication/replication.component.ts b/src/ui_ng/lib/src/replication/replication.component.ts index 5479f9ad5..09b2ab0f7 100644 --- a/src/ui_ng/lib/src/replication/replication.component.ts +++ b/src/ui_ng/lib/src/replication/replication.component.ts @@ -388,6 +388,9 @@ export class ReplicationComponent implements OnInit, OnDestroy { refreshJobs() { + this.currentJobStatus = this.jobStatus[0]; + this.search.startTime = ' '; + this.search.endTime = ' '; this.search.repoName = ""; this.search.startTimestamp = ""; this.search.endTimestamp = ""; diff --git a/src/ui_ng/package.json b/src/ui_ng/package.json index 88b5993f2..7ddb87599 100644 --- a/src/ui_ng/package.json +++ b/src/ui_ng/package.json @@ -31,7 +31,7 @@ "clarity-icons": "^0.10.17", "clarity-ui": "^0.10.17", "core-js": "^2.4.1", - "harbor-ui": "0.6.46", + "harbor-ui": "0.6.47", "intl": "^1.2.5", "mutationobserver-shim": "^0.3.2", "ngx-cookie": "^1.0.0", diff --git a/src/ui_ng/src/app/project/create-project/create-project.component.html b/src/ui_ng/src/app/project/create-project/create-project.component.html index ff0303a21..af0f97cad 100644 --- a/src/ui_ng/src/app/project/create-project/create-project.component.html +++ b/src/ui_ng/src/app/project/create-project/create-project.component.html @@ -12,7 +12,8 @@ required pattern="^[a-z0-9]+(?:[._-][a-z0-9]+)*$" minlength="2" - #projectName="ngModel" + #projectName="ngModel" + autocomplete="off" (keyup)='handleValidation()'> {{ nameTooltipText | translate }} diff --git a/src/ui_ng/src/app/project/create-project/create-project.component.ts b/src/ui_ng/src/app/project/create-project/create-project.component.ts index 9fa6e3515..1f4c3e6ce 100644 --- a/src/ui_ng/src/app/project/create-project/create-project.component.ts +++ b/src/ui_ng/src/app/project/create-project/create-project.component.ts @@ -74,7 +74,7 @@ export class CreateProjectComponent implements AfterViewChecked, OnInit, OnDestr ngOnInit(): void { this.proNameChecker .debounceTime(500) - .distinctUntilChanged() + //.distinctUntilChanged() .subscribe((name: string) => { let cont = this.currentForm.controls["create_project_name"]; if (cont && this.hasChanged) { @@ -166,6 +166,8 @@ export class CreateProjectComponent implements AfterViewChecked, OnInit, OnDestr newProject() { this.project = new Project(); this.hasChanged = false; + this.isNameValid = true; + this.createProjectOpened = true; } diff --git a/src/ui_ng/src/app/user/user.component.ts b/src/ui_ng/src/app/user/user.component.ts index f44b888ae..e436696c8 100644 --- a/src/ui_ng/src/app/user/user.component.ts +++ b/src/ui_ng/src/app/user/user.component.ts @@ -49,10 +49,6 @@ import {BatchInfo, BathInfoChanges} from '../shared/confirmation-dialog/confirma export class UserComponent implements OnInit, OnDestroy { users: User[] = []; originalUsers: Promise; - private onGoing: boolean = true; - private adminMenuText: string = ""; - private adminColumn: string = ""; - private deletionSubscription: Subscription; selectedRow: User[] = []; ISADMNISTRATOR: string = "USER.ENABLE_ADMIN_ACTION"; batchDelectionInfos: BatchInfo[] = []; @@ -60,12 +56,15 @@ export class UserComponent implements OnInit, OnDestroy { currentTerm: string; totalCount: number = 0; currentPage: number = 1; + timerHandler: any; + private onGoing: boolean = true; + private adminMenuText: string = ""; + private adminColumn: string = ""; + private deletionSubscription: Subscription; @ViewChild(NewUserModalComponent) newUserDialog: NewUserModalComponent; - timerHandler: any; - constructor( private userService: UserService, private translate: TranslateService, @@ -98,10 +97,6 @@ export class UserComponent implements OnInit, OnDestroy { return this.selectedRow.length === 1 && this.isMySelf(this.selectedRow[0].user_id); } - private isMatchFilterTerm(terms: string, testedItem: string): boolean { - return testedItem.toLowerCase().indexOf(terms.toLowerCase()) !== -1; - } - public get canCreateUser(): boolean { let appConfig = this.appConfigService.getConfig(); if (appConfig) { @@ -173,9 +168,12 @@ export class UserComponent implements OnInit, OnDestroy { if (terms.trim() === "") { this.refreshUser((this.currentPage - 1) * 15, this.currentPage * 15); } else { - this.users = users.filter(user => { + let selectUsers = users.filter(user => { return this.isMatchFilterTerm(terms, user.username); }); + this.totalCount = selectUsers.length; + this.users = selectUsers.slice((this.currentPage - 1) * 15, this.currentPage * 15);//First page + this.forceRefreshView(5000); } }); @@ -367,4 +365,8 @@ export class UserComponent implements OnInit, OnDestroy { }, duration); } + private isMatchFilterTerm(terms: string, testedItem: string): boolean { + return testedItem.toLowerCase().indexOf(terms.toLowerCase()) !== -1; + } + } From 379f1134521654a2a10e2e7291501fa2ccdda671 Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Wed, 7 Mar 2018 13:20:28 +0800 Subject: [PATCH 06/21] Implement label management API --- docs/swagger.yaml | 185 ++++++++++++ make/photon/db/registry.sql | 18 ++ make/photon/db/registry_sqlite.sql | 23 +- src/common/const.go | 9 + src/common/dao/label.go | 99 +++++++ src/common/dao/label_test.go | 91 ++++++ src/common/models/base.go | 3 +- src/common/models/label.go | 94 +++++++ src/common/models/label_test.go | 86 ++++++ src/ui/api/harborapi_test.go | 2 + src/ui/api/label.go | 263 +++++++++++++++++ src/ui/api/label_test.go | 435 +++++++++++++++++++++++++++++ src/ui/router.go | 2 + tools/migration/changelog.md | 4 + 14 files changed, 1312 insertions(+), 2 deletions(-) create mode 100644 src/common/dao/label.go create mode 100644 src/common/dao/label_test.go create mode 100644 src/common/models/label.go create mode 100644 src/common/models/label_test.go create mode 100644 src/ui/api/label.go create mode 100644 src/ui/api/label_test.go diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 4a83a1636..c36b60308 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1639,6 +1639,164 @@ paths: project and target. '500': description: Unexpected internal errors. + /labels: + get: + summary: List labels according to the query strings. + description: > + This endpoint let user list labels by name, scope and project_id + parameters: + - name: name + in: query + type: string + required: false + description: The label name. + - name: scope + in: query + type: string + required: true + description: The label scope. Valid values are g and p. g for global labels and p for project labels. + - name: project_id + in: query + type: integer + format: int64 + required: false + description: Relevant project ID, required when scope is p. + - name: page + in: query + type: integer + format: int32 + required: false + description: The page nubmer. + - name: page_size + in: query + type: integer + format: int32 + required: false + description: The size of per page. + tags: + - Products + responses: + '200': + description: Get successfully. + schema: + type: array + items: + $ref: '#/definitions/Label' + '400': + description: Invalid parameters. + '401': + description: User need to log in first. + '500': + description: Unexpected internal errors. + post: + summary: Post creates a label + description: > + This endpoint let user creates a label. + parameters: + - name: label + in: body + description: The json object of label. + required: true + schema: + $ref: '#/definitions/Label' + tags: + - Products + responses: + '201': + description: Create successfully. + '400': + description: Invalid parameters. + '401': + description: User need to log in first. + '409': + description: >- + Label with the same name and same scope already exists. + '415': + $ref: '#/responses/UnsupportedMediaType' + '500': + description: Unexpected internal errors. + '/labels/{id}': + get: + summary: Get the label specified by ID. + description: | + This endpoint let user get the label by specific ID. + parameters: + - name: id + in: path + type: integer + format: int64 + required: true + description: Label ID + tags: + - Products + responses: + '200': + description: Get successfully. + schema: + $ref: '#/definitions/Label' + '401': + description: User need to log in first. + '404': + description: The resource does not exist. + '500': + description: Unexpected internal errors. + put: + summary: Update the label properties. + description: > + This endpoint let user update label properties. + parameters: + - name: id + in: path + type: integer + format: int64 + required: true + description: Label ID + - name: label + in: body + description: The updated label json object. + required: true + schema: + $ref: '#/definitions/Label' + tags: + - Products + responses: + '200': + description: Update successfully. + '400': + description: Invalid parameters. + '401': + description: User need to log in first. + '404': + description: The resource does not exist. + '409': + description: >- + The label with the same name already exists. + '500': + description: Unexpected internal errors. + delete: + summary: Delete the label specified by ID. + description: > + Delete the label specified by ID. + parameters: + - name: id + in: path + type: integer + format: int64 + required: true + description: Label ID + tags: + - Products + responses: + '200': + description: Delete successfully. + '400': + description: Invalid parameters. + '401': + description: User need to log in first. + '404': + description: The resource does not exist. + '500': + description: Unexpected internal errors. /replications: post: summary: Trigger the replication according to the specified policy. @@ -3056,3 +3214,30 @@ definitions: status: type: string description: The status of jobs. The only valid value is stop for now. + Label: + type: object + properties: + id: + type: integer + description: The ID of label. + name: + type: string + description: The name of label. + description: + type: string + description: The description of label. + color: + type: string + description: The color of label. + scope: + type: integer + description: The scope of label, g for global labels and p for project labels. + project_id: + type: integer + description: The project ID if the label is a project label. + creation_time: + type: string + description: The creation time of label. + update_time: + type: string + description: The update time of label. diff --git a/make/photon/db/registry.sql b/make/photon/db/registry.sql index 0ddc0087d..02c1fb6ca 100644 --- a/make/photon/db/registry.sql +++ b/make/photon/db/registry.sql @@ -254,6 +254,24 @@ create table properties ( UNIQUE (k) ); +create table harbor_label ( + id int NOT NULL AUTO_INCREMENT, + name varchar(128) NOT NULL, + description text, + color varchar(16), +# 's' for system level labels +# 'u' for user level labels + level char(1) NOT NULL, +# 'g' for global labels +# 'p' for project labels + scope char(1) NOT NULL, + project_id int, + creation_time timestamp default CURRENT_TIMESTAMP, + update_time timestamp default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, + PRIMARY KEY(id), + CONSTRAINT unique_name_and_scope UNIQUE (name,scope) + ); + CREATE TABLE IF NOT EXISTS `alembic_version` ( `version_num` varchar(32) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/make/photon/db/registry_sqlite.sql b/make/photon/db/registry_sqlite.sql index fdf6d9e9a..c5dc67567 100644 --- a/make/photon/db/registry_sqlite.sql +++ b/make/photon/db/registry_sqlite.sql @@ -111,7 +111,7 @@ create table project_metadata ( creation_time timestamp, update_time timestamp, deleted tinyint (1) DEFAULT 0 NOT NULL, - UNIQUE(project_id, name) ON CONFLICT REPLACE, + UNIQUE(project_id, name), FOREIGN KEY (project_id) REFERENCES project(project_id) ); @@ -240,6 +240,27 @@ create table properties ( UNIQUE(k) ); +create table harbor_label ( + id INTEGER PRIMARY KEY, + name varchar(128) NOT NULL, + description text, + color varchar(16), +/* + 's' for system level labels + 'u' for user level labels +*/ + level char(1) NOT NULL, +/* + 'g' for global labels + 'p' for project labels +*/ + scope char(1) NOT NULL, + project_id int, + creation_time timestamp default CURRENT_TIMESTAMP, + update_time timestamp default CURRENT_TIMESTAMP, + UNIQUE(name, scope) + ); + create table alembic_version ( version_num varchar(32) NOT NULL ); diff --git a/src/common/const.go b/src/common/const.go index a3c164307..44444bd06 100644 --- a/src/common/const.go +++ b/src/common/const.go @@ -29,6 +29,15 @@ const ( RoleDeveloper = 2 RoleGuest = 3 + LabelLevelSystem = "s" + LabelLevelUser = "u" + LabelScopeGlobal = "g" + LabelScopeProject = "p" + + ResourceTypeProject = "p" + ResourceTypeRepository = "r" + ResourceTypeImage = "i" + ExtEndpoint = "ext_endpoint" AUTHMode = "auth_mode" DatabaseType = "database_type" diff --git a/src/common/dao/label.go b/src/common/dao/label.go new file mode 100644 index 000000000..77b26a543 --- /dev/null +++ b/src/common/dao/label.go @@ -0,0 +1,99 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dao + +import ( + "time" + + "github.com/astaxie/beego/orm" + "github.com/vmware/harbor/src/common/models" +) + +// AddLabel creates a label +func AddLabel(label *models.Label) (int64, error) { + now := time.Now() + label.CreationTime = now + label.UpdateTime = now + return GetOrmer().Insert(label) +} + +// GetLabel specified by ID +func GetLabel(id int64) (*models.Label, error) { + label := &models.Label{ + ID: id, + } + if err := GetOrmer().Read(label); err != nil { + if err == orm.ErrNoRows { + return nil, nil + } + return nil, err + } + + return label, nil +} + +// GetTotalOfLabels returns the total count of labels +func GetTotalOfLabels(query *models.LabelQuery) (int64, error) { + qs := getLabelQuerySetter(query) + return qs.Count() +} + +// ListLabels list labels according to the query conditions +func ListLabels(query *models.LabelQuery) ([]*models.Label, error) { + qs := getLabelQuerySetter(query) + if query.Size > 0 { + qs = qs.Limit(query.Size) + if query.Page > 0 { + qs = qs.Offset((query.Page - 1) * query.Size) + } + } + qs = qs.OrderBy("Name") + + labels := []*models.Label{} + _, err := qs.All(&labels) + return labels, err +} + +func getLabelQuerySetter(query *models.LabelQuery) orm.QuerySeter { + qs := GetOrmer().QueryTable(&models.Label{}) + if len(query.Name) > 0 { + qs = qs.Filter("Name", query.Name) + } + if len(query.Level) > 0 { + qs = qs.Filter("Level", query.Level) + } + if len(query.Scope) > 0 { + qs = qs.Filter("Scope", query.Scope) + } + if query.ProjectID != 0 { + qs = qs.Filter("ProjectID", query.ProjectID) + } + return qs +} + +// UpdateLabel ... +func UpdateLabel(label *models.Label) error { + label.UpdateTime = time.Now() + _, err := GetOrmer().Update(label) + return err +} + +// DeleteLabel ... +func DeleteLabel(id int64) error { + _, err := GetOrmer().Delete(&models.Label{ + ID: id, + }) + return err +} diff --git a/src/common/dao/label_test.go b/src/common/dao/label_test.go new file mode 100644 index 000000000..60b389e1b --- /dev/null +++ b/src/common/dao/label_test.go @@ -0,0 +1,91 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dao + +import ( + "testing" + + "github.com/vmware/harbor/src/common" + "github.com/vmware/harbor/src/common/models" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMethodsOfLabel(t *testing.T) { + label := &models.Label{ + Name: "test", + Level: common.LabelLevelUser, + Scope: common.LabelScopeProject, + ProjectID: 1, + } + + // add + id, err := AddLabel(label) + require.Nil(t, err) + label.ID = id + + // get + l, err := GetLabel(id) + require.Nil(t, err) + assert.Equal(t, label.ID, l.ID) + assert.Equal(t, label.Name, l.Name) + assert.Equal(t, label.Scope, l.Scope) + assert.Equal(t, label.ProjectID, l.ProjectID) + + // get total count + total, err := GetTotalOfLabels(&models.LabelQuery{ + Scope: common.LabelScopeProject, + ProjectID: 1, + }) + require.Nil(t, err) + assert.Equal(t, int64(1), total) + + // list + labels, err := ListLabels(&models.LabelQuery{ + Scope: common.LabelScopeProject, + ProjectID: 1, + Name: label.Name, + }) + require.Nil(t, err) + assert.Equal(t, 1, len(labels)) + + // list + labels, err = ListLabels(&models.LabelQuery{ + Scope: common.LabelScopeProject, + ProjectID: 1, + Name: "not_exist_label", + }) + require.Nil(t, err) + assert.Equal(t, 0, len(labels)) + + // update + newName := "dev" + label.Name = newName + err = UpdateLabel(label) + require.Nil(t, err) + + l, err = GetLabel(id) + require.Nil(t, err) + assert.Equal(t, newName, l.Name) + + // delete + err = DeleteLabel(id) + require.Nil(t, err) + + l, err = GetLabel(id) + require.Nil(t, err) + assert.Nil(t, l) +} diff --git a/src/common/models/base.go b/src/common/models/base.go index 79ce7483e..89f201432 100644 --- a/src/common/models/base.go +++ b/src/common/models/base.go @@ -32,5 +32,6 @@ func init() { new(ClairVulnTimestamp), new(WatchItem), new(ProjectMetadata), - new(ConfigEntry)) + new(ConfigEntry), + new(Label)) } diff --git a/src/common/models/label.go b/src/common/models/label.go new file mode 100644 index 000000000..90d3f278f --- /dev/null +++ b/src/common/models/label.go @@ -0,0 +1,94 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package models + +import ( + "fmt" + "time" + + "github.com/astaxie/beego/validation" + "github.com/vmware/harbor/src/common" +) + +// Label holds information used for a label +type Label struct { + ID int64 `orm:"pk;auto;column(id)" json:"id"` + Name string `orm:"column(name)" json:"name"` + Description string `orm:"column(description)" json:"description"` + Color string `orm:"column(color)" json:"color"` + Level string `orm:"column(level)" json:"-"` + Scope string `orm:"column(scope)" json:"scope"` + ProjectID int64 `orm:"column(project_id)" json:"project_id"` + CreationTime time.Time `orm:"column(creation_time)" json:"creation_time"` + UpdateTime time.Time `orm:"column(update_time)" json:"update_time"` +} + +//TableName ... +func (l *Label) TableName() string { + return "harbor_label" +} + +// LabelQuery : query parameters for labels +type LabelQuery struct { + Name string + Level string + Scope string + ProjectID int64 + Pagination +} + +// Valid ... +func (l *Label) Valid(v *validation.Validation) { + if len(l.Name) == 0 { + v.SetError("name", "cannot be empty") + } + if len(l.Name) > 128 { + v.SetError("name", "max length is 128") + } + + if l.Scope != common.LabelScopeGlobal && l.Scope != common.LabelScopeProject { + v.SetError("scope", fmt.Sprintf("invalid: %s", l.Scope)) + } else if l.Scope == common.LabelScopeProject && l.ProjectID <= 0 { + v.SetError("project_id", fmt.Sprintf("invalid: %d", l.ProjectID)) + } +} + +/* +type ResourceLabel struct { + ID int64 `orm:"pk;auto;column(id)" json:"id"` + LabelID int64 `orm:"column(label_id)" json:"label_id"` + ResourceID string `orm:"column(resource_id)" json:"resource_id"` + ResourceType rune `orm:"column(resource_type)" json:"resource_type"` + CreationTime time.Time `orm:"column(creation_time)" json:"creation_time"` + UpdateTime time.Time `orm:"column(update_time)" json:"update_time"` +} + + +// Valid ... +func (r *ResourceLabel) Valid(v *validation.Validation) { + if r.LabelID <= 0 { + v.SetError("label_id", fmt.Sprintf("invalid: %d", r.LabelID)) + } + // TODO + //if r.ResourceID <= 0 { + // v.SetError("resource_id", fmt.Sprintf("invalid: %v", r.ResourceID)) + //} + if r.ResourceType != common.ResourceTypeProject && + r.ResourceType != common.ResourceTypeRepository && + r.ResourceType != common.ResourceTypeImage { + v.SetError("resource_type", fmt.Sprintf("invalid: %d", r.ResourceType)) + } +} +*/ diff --git a/src/common/models/label_test.go b/src/common/models/label_test.go new file mode 100644 index 000000000..575c8b792 --- /dev/null +++ b/src/common/models/label_test.go @@ -0,0 +1,86 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package models + +import ( + "testing" + + "github.com/astaxie/beego/validation" + "github.com/stretchr/testify/assert" +) + +func TestValidOfLabel(t *testing.T) { + cases := []struct { + label *Label + hasError bool + }{ + { + label: &Label{ + Name: "", + }, + hasError: true, + }, + { + label: &Label{ + Name: "test", + Scope: "", + }, + hasError: true, + }, + { + label: &Label{ + Name: "test", + Scope: "invalid_scope", + }, + hasError: true, + }, + { + label: &Label{ + Name: "test", + Scope: "g", + }, + hasError: false, + }, + { + label: &Label{ + Name: "test", + Scope: "p", + }, + hasError: true, + }, + { + label: &Label{ + Name: "test", + Scope: "p", + ProjectID: -1, + }, + hasError: true, + }, + { + label: &Label{ + Name: "test", + Scope: "p", + ProjectID: 1, + }, + hasError: false, + }, + } + + for _, c := range cases { + v := &validation.Validation{} + c.label.Valid(v) + assert.Equal(t, c.hasError, v.HasErrors()) + } +} diff --git a/src/ui/api/harborapi_test.go b/src/ui/api/harborapi_test.go index abca6f0e1..b63ce193f 100644 --- a/src/ui/api/harborapi_test.go +++ b/src/ui/api/harborapi_test.go @@ -132,6 +132,8 @@ func init() { beego.Router("/api/configurations/reset", &ConfigAPI{}, "post:Reset") beego.Router("/api/email/ping", &EmailAPI{}, "post:Ping") beego.Router("/api/replications", &ReplicationAPI{}) + beego.Router("/api/labels", &LabelAPI{}, "post:Post;get:List") + beego.Router("/api/labels/:id([0-9]+", &LabelAPI{}, "get:Get;put:Put;delete:Delete") _ = updateInitPassword(1, "Harbor12345") diff --git a/src/ui/api/label.go b/src/ui/api/label.go new file mode 100644 index 000000000..3d7dfe978 --- /dev/null +++ b/src/ui/api/label.go @@ -0,0 +1,263 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/vmware/harbor/src/common" + "github.com/vmware/harbor/src/common/dao" + "github.com/vmware/harbor/src/common/models" +) + +// LabelAPI handles requests for label management +type LabelAPI struct { + label *models.Label + BaseController +} + +// Prepare ... +func (l *LabelAPI) Prepare() { + l.BaseController.Prepare() + method := l.Ctx.Request.Method + if method == http.MethodGet { + return + } + + // POST, PUT, DELETE need login first + if !l.SecurityCtx.IsAuthenticated() { + l.HandleUnauthorized() + return + } + + if method == http.MethodPut || method == http.MethodDelete { + id, err := l.GetInt64FromPath(":id") + if err != nil || id <= 0 { + l.HandleBadRequest("invalid label ID") + return + } + + label, err := dao.GetLabel(id) + if err != nil { + l.HandleInternalServerError(fmt.Sprintf("failed to get label %d: %v", id, err)) + return + } + + if label == nil { + l.HandleNotFound(fmt.Sprintf("label %d not found", id)) + return + } + + if label.Scope == common.LabelScopeGlobal && !l.SecurityCtx.IsSysAdmin() || + label.Scope == common.LabelScopeProject && !l.SecurityCtx.HasAllPerm(label.ProjectID) { + l.HandleForbidden(l.SecurityCtx.GetUsername()) + return + } + l.label = label + } +} + +// Post creates a label +func (l *LabelAPI) Post() { + label := &models.Label{} + l.DecodeJSONReqAndValidate(label) + label.Level = common.LabelLevelUser + + switch label.Scope { + case common.LabelScopeGlobal: + if !l.SecurityCtx.IsSysAdmin() { + l.HandleForbidden(l.SecurityCtx.GetUsername()) + return + } + label.ProjectID = 0 + case common.LabelScopeProject: + exist, err := l.ProjectMgr.Exists(label.ProjectID) + if err != nil { + l.HandleInternalServerError(fmt.Sprintf("failed to check the existence of project %d: %v", + label.ProjectID, err)) + return + } + if !exist { + l.HandleNotFound(fmt.Sprintf("project %d not found", label.ProjectID)) + return + } + if !l.SecurityCtx.HasAllPerm(label.ProjectID) { + l.HandleForbidden(l.SecurityCtx.GetUsername()) + return + } + } + + labels, err := dao.ListLabels(&models.LabelQuery{ + Name: label.Name, + Level: label.Level, + Scope: label.Scope, + ProjectID: label.ProjectID, + }) + if err != nil { + l.HandleInternalServerError(fmt.Sprintf("failed to list labels: %v", err)) + return + } + if len(labels) > 0 { + l.HandleConflict() + return + } + + id, err := dao.AddLabel(label) + if err != nil { + l.HandleInternalServerError(fmt.Sprintf("failed to create label: %v", err)) + return + } + + l.Redirect(http.StatusCreated, strconv.FormatInt(id, 10)) +} + +// Get the label specified by ID +func (l *LabelAPI) Get() { + id, err := l.GetInt64FromPath(":id") + if err != nil || id <= 0 { + l.HandleBadRequest(fmt.Sprintf("invalid label ID: %s", l.GetStringFromPath(":id"))) + return + } + + label, err := dao.GetLabel(id) + if err != nil { + l.HandleInternalServerError(fmt.Sprintf("failed to get label %d: %v", id, err)) + return + } + + if label == nil { + l.HandleNotFound(fmt.Sprintf("label %d not found", id)) + return + } + + if label.Scope == common.LabelScopeProject { + if !l.SecurityCtx.HasReadPerm(label.ProjectID) { + if !l.SecurityCtx.IsAuthenticated() { + l.HandleUnauthorized() + return + } + l.HandleForbidden(l.SecurityCtx.GetUsername()) + return + } + } + + l.Data["json"] = label + l.ServeJSON() +} + +// List labels according to the query strings +func (l *LabelAPI) List() { + query := &models.LabelQuery{ + Name: l.GetString("name"), + Level: common.LabelLevelUser, + } + + scope := l.GetString("scope") + if scope != common.LabelScopeGlobal && scope != common.LabelScopeProject { + l.HandleBadRequest(fmt.Sprintf("invalid scope: %s", scope)) + return + } + query.Scope = scope + + if scope == common.LabelScopeProject { + projectIDStr := l.GetString("project_id") + if len(projectIDStr) == 0 { + l.HandleBadRequest("project_id is required") + return + } + projectID, err := strconv.ParseInt(projectIDStr, 10, 64) + if err != nil || projectID <= 0 { + l.HandleBadRequest(fmt.Sprintf("invalid project_id: %s", projectIDStr)) + return + } + + if !l.SecurityCtx.HasReadPerm(projectID) { + if !l.SecurityCtx.IsAuthenticated() { + l.HandleUnauthorized() + return + } + l.HandleForbidden(l.SecurityCtx.GetUsername()) + return + } + query.ProjectID = projectID + } + + total, err := dao.GetTotalOfLabels(query) + if err != nil { + l.HandleInternalServerError(fmt.Sprintf("failed to get total count of labels: %v", err)) + return + } + + query.Page, query.Size = l.GetPaginationParams() + + labels, err := dao.ListLabels(query) + if err != nil { + l.HandleInternalServerError(fmt.Sprintf("failed to list labels: %v", err)) + return + } + + l.SetPaginationHeader(total, query.Page, query.Size) + l.Data["json"] = labels + l.ServeJSON() +} + +// Put updates the label +func (l *LabelAPI) Put() { + label := &models.Label{} + l.DecodeJSONReq(label) + + oldName := l.label.Name + + // only name, description and color can be changed + l.label.Name = label.Name + l.label.Description = label.Description + l.label.Color = label.Color + + l.Validate(l.label) + + if l.label.Name != oldName { + labels, err := dao.ListLabels(&models.LabelQuery{ + Name: l.label.Name, + Level: l.label.Level, + Scope: l.label.Scope, + ProjectID: l.label.ProjectID, + }) + if err != nil { + l.HandleInternalServerError(fmt.Sprintf("failed to list labels: %v", err)) + return + } + if len(labels) > 0 { + l.HandleConflict() + return + } + } + + if err := dao.UpdateLabel(l.label); err != nil { + l.HandleInternalServerError(fmt.Sprintf("failed to update label %d: %v", l.label.ID, err)) + return + } + +} + +// Delete the label +func (l *LabelAPI) Delete() { + id := l.label.ID + if err := dao.DeleteLabel(id); err != nil { + l.HandleInternalServerError(fmt.Sprintf("failed to delete label %d: %v", id, err)) + return + } +} diff --git a/src/ui/api/label_test.go b/src/ui/api/label_test.go new file mode 100644 index 000000000..71f6022b0 --- /dev/null +++ b/src/ui/api/label_test.go @@ -0,0 +1,435 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/vmware/harbor/src/common" + "github.com/vmware/harbor/src/common/models" +) + +var ( + labelAPIBasePath = "/api/labels" + labelID int64 +) + +func TestLabelAPIPost(t *testing.T) { + postFunc := func(resp *httptest.ResponseRecorder) error { + id, err := parseResourceID(resp) + if err != nil { + return err + } + labelID = id + return nil + } + + cases := []*codeCheckingCase{ + // 401 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPost, + url: labelAPIBasePath, + }, + code: http.StatusUnauthorized, + }, + + // 400 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPost, + url: labelAPIBasePath, + bodyJSON: &models.Label{}, + credential: nonSysAdmin, + }, + code: http.StatusBadRequest, + }, + + // 403 non-sysadmin try to create global label + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPost, + url: labelAPIBasePath, + bodyJSON: &models.Label{ + Name: "test", + Scope: common.LabelScopeGlobal, + }, + credential: nonSysAdmin, + }, + code: http.StatusForbidden, + }, + + // 403 non-member user try to create project label + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPost, + url: labelAPIBasePath, + bodyJSON: &models.Label{ + Name: "test", + Scope: common.LabelScopeProject, + ProjectID: 1, + }, + credential: nonSysAdmin, + }, + code: http.StatusForbidden, + }, + + // 403 developer try to create project label + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPost, + url: labelAPIBasePath, + bodyJSON: &models.Label{ + Name: "test", + Scope: common.LabelScopeProject, + ProjectID: 1, + }, + credential: projDeveloper, + }, + code: http.StatusForbidden, + }, + + // 404 non-exist project + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPost, + url: labelAPIBasePath, + bodyJSON: &models.Label{ + Name: "test", + Scope: common.LabelScopeProject, + ProjectID: 10000, + }, + credential: projAdmin, + }, + code: http.StatusNotFound, + }, + + // 200 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPost, + url: labelAPIBasePath, + bodyJSON: &models.Label{ + Name: "test", + Scope: common.LabelScopeProject, + ProjectID: 1, + }, + credential: projAdmin, + }, + code: http.StatusCreated, + postFunc: postFunc, + }, + + // 409 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPost, + url: labelAPIBasePath, + bodyJSON: &models.Label{ + Name: "test", + Scope: common.LabelScopeProject, + ProjectID: 1, + }, + credential: projAdmin, + }, + code: http.StatusConflict, + }, + } + + runCodeCheckingCases(t, cases...) +} + +func TestLabelAPIGet(t *testing.T) { + cases := []*codeCheckingCase{ + // 400 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodGet, + url: fmt.Sprintf("%s/%d", labelAPIBasePath, 0), + }, + code: http.StatusBadRequest, + }, + + // 404 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodGet, + url: fmt.Sprintf("%s/%d", labelAPIBasePath, 1000), + }, + code: http.StatusNotFound, + }, + + // 200 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodGet, + url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID), + }, + code: http.StatusOK, + }, + } + runCodeCheckingCases(t, cases...) +} + +func TestLabelAPIList(t *testing.T) { + cases := []*codeCheckingCase{ + // 400 no scope query string + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodGet, + url: labelAPIBasePath, + }, + code: http.StatusBadRequest, + }, + + // 400 invalid scope + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodGet, + url: labelAPIBasePath, + queryStruct: struct { + Scope string `url:"scope"` + }{ + Scope: "invalid_scope", + }, + }, + code: http.StatusBadRequest, + }, + + // 400 invalid project_id + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodGet, + url: labelAPIBasePath, + queryStruct: struct { + Scope string `url:"scope"` + ProjectID int64 `url:"project_id"` + }{ + Scope: "p", + ProjectID: 0, + }, + }, + code: http.StatusBadRequest, + }, + } + runCodeCheckingCases(t, cases...) + + // 200 + labels := []*models.Label{} + err := handleAndParse(&testingRequest{ + method: http.MethodGet, + url: labelAPIBasePath, + queryStruct: struct { + Scope string `url:"scope"` + ProjectID int64 `url:"project_id"` + Name string `url:"name"` + }{ + Scope: "p", + ProjectID: 1, + Name: "test", + }, + }, &labels) + require.Nil(t, err) + assert.Equal(t, 1, len(labels)) + + err = handleAndParse(&testingRequest{ + method: http.MethodGet, + url: labelAPIBasePath, + queryStruct: struct { + Scope string `url:"scope"` + ProjectID int64 `url:"project_id"` + Name string `url:"name"` + }{ + Scope: "p", + ProjectID: 1, + Name: "dev", + }, + }, &labels) + require.Nil(t, err) + assert.Equal(t, 0, len(labels)) +} + +func TestLabelAPIPut(t *testing.T) { + cases := []*codeCheckingCase{ + // 401 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPut, + url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID), + }, + code: http.StatusUnauthorized, + }, + + // 400 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPut, + url: fmt.Sprintf("%s/%d", labelAPIBasePath, 0), + credential: nonSysAdmin, + }, + code: http.StatusBadRequest, + }, + + // 404 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPut, + url: fmt.Sprintf("%s/%d", labelAPIBasePath, 10000), + credential: nonSysAdmin, + }, + code: http.StatusNotFound, + }, + + // 403 non-member user + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPut, + url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID), + credential: nonSysAdmin, + }, + code: http.StatusForbidden, + }, + + // 403 developer + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPut, + url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID), + credential: projDeveloper, + }, + code: http.StatusForbidden, + }, + + // 400 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPut, + url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID), + bodyJSON: &models.Label{ + Name: "", + Scope: common.LabelScopeProject, + ProjectID: 1, + }, + credential: projAdmin, + }, + code: http.StatusBadRequest, + }, + + // 200 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPut, + url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID), + bodyJSON: &models.Label{ + Name: "product", + Scope: common.LabelScopeProject, + ProjectID: 1, + }, + credential: projAdmin, + }, + code: http.StatusOK, + }, + } + + runCodeCheckingCases(t, cases...) + + label := &models.Label{} + err := handleAndParse(&testingRequest{ + method: http.MethodGet, + url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID), + }, label) + require.Nil(t, err) + assert.Equal(t, "product", label.Name) +} + +func TestLabelAPIDelete(t *testing.T) { + cases := []*codeCheckingCase{ + // 401 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodDelete, + url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID), + }, + code: http.StatusUnauthorized, + }, + + // 400 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodDelete, + url: fmt.Sprintf("%s/%d", labelAPIBasePath, 0), + credential: nonSysAdmin, + }, + code: http.StatusBadRequest, + }, + + // 404 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodDelete, + url: fmt.Sprintf("%s/%d", labelAPIBasePath, 10000), + credential: nonSysAdmin, + }, + code: http.StatusNotFound, + }, + + // 403 non-member user + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodDelete, + url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID), + credential: nonSysAdmin, + }, + code: http.StatusForbidden, + }, + + // 403 developer + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodDelete, + url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID), + credential: projDeveloper, + }, + code: http.StatusForbidden, + }, + + // 200 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodDelete, + url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID), + credential: projAdmin, + }, + code: http.StatusOK, + }, + + // 404 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodDelete, + url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID), + credential: projAdmin, + }, + code: http.StatusNotFound, + }, + } + + runCodeCheckingCases(t, cases...) +} diff --git a/src/ui/router.go b/src/ui/router.go index eaa511d29..bcdc536ec 100644 --- a/src/ui/router.go +++ b/src/ui/router.go @@ -94,6 +94,8 @@ func initRouters() { beego.Router("/api/configurations/reset", &api.ConfigAPI{}, "post:Reset") beego.Router("/api/statistics", &api.StatisticAPI{}) beego.Router("/api/replications", &api.ReplicationAPI{}) + beego.Router("/api/labels", &api.LabelAPI{}, "post:Post;get:List") + beego.Router("/api/labels/:id([0-9]+", &api.LabelAPI{}, "get:Get;put:Put;delete:Delete") beego.Router("/api/systeminfo", &api.SystemInfoAPI{}, "get:GetGeneralInfo") beego.Router("/api/systeminfo/volumes", &api.SystemInfoAPI{}, "get:GetVolumeInfo") diff --git a/tools/migration/changelog.md b/tools/migration/changelog.md index 7098979c7..9dff157c3 100644 --- a/tools/migration/changelog.md +++ b/tools/migration/changelog.md @@ -65,3 +65,7 @@ Changelog for harbor database schema - add pk `id` to table `properties` - remove pk index from column 'k' of table `properties` - alter `name` length from 41 to 256 of table `project` + +## 1.5.0 + + - create table `harbor_label` \ No newline at end of file From dab5dd9879a59b49e959d8a2f7bffc325939fa08 Mon Sep 17 00:00:00 2001 From: Jesse Hu Date: Fri, 9 Mar 2018 18:47:13 +0800 Subject: [PATCH 07/21] Update Harbor helm chart to deploy Harbor 1.4.0 release (#4373) * Update Harbor helm chart to deploy Harbor 1.4.0 release * Change version to 0.1.0 and fix a doc error * Fix auth_mode default value * Refine doc and fix a typo --- contrib/helm/harbor/Chart.yaml | 10 +- contrib/helm/harbor/README.md | 108 +++++++++++----- contrib/helm/harbor/templates/NOTES.txt | 21 ++-- contrib/helm/harbor/templates/_helpers.tpl | 4 +- .../templates/adminserver/adminserver-cm.yaml | 23 +++- .../adminserver/adminserver-secrets.yaml | 5 +- .../templates/adminserver/adminserver-ss.yaml | 11 +- .../adminserver/adminserver-svc.yaml | 6 +- .../helm/harbor/templates/clair/clair-cm.yaml | 3 +- .../harbor/templates/clair/clair-dpl.yaml | 10 +- .../harbor/templates/clair/clair-svc.yaml | 24 +--- .../templates/clair/postgres-secret.yaml | 4 +- .../harbor/templates/clair/postgres-ss.yaml | 10 +- .../harbor/templates/clair/postgres-svc.yaml | 22 +--- .../harbor/templates/ingress/ingress.yaml | 12 +- .../helm/harbor/templates/ingress/secret.yaml | 12 +- .../templates/jobservice/jobservice-cm.yaml | 2 +- .../templates/jobservice/jobservice-dpl.yaml | 10 +- .../jobservice/jobservice-secrets.yaml | 6 +- .../templates/jobservice/jobservice-svc.yaml | 4 +- .../harbor/templates/mysql/mysql-secret.yaml | 2 +- .../helm/harbor/templates/mysql/mysql-ss.yaml | 8 +- .../harbor/templates/mysql/mysql-svc.yaml | 6 +- .../templates/registry/registry-cm.yaml | 2 +- .../templates/registry/registry-secret.yaml | 4 +- .../templates/registry/registry-ss.yaml | 12 +- .../templates/registry/registry-svc.yaml | 6 +- contrib/helm/harbor/templates/ui/ui-cm.yaml | 2 +- contrib/helm/harbor/templates/ui/ui-dpl.yaml | 6 +- .../helm/harbor/templates/ui/ui-secrets.yaml | 6 +- contrib/helm/harbor/templates/ui/ui-svc.yaml | 4 +- contrib/helm/harbor/values.yaml | 117 +++++------------- 32 files changed, 242 insertions(+), 240 deletions(-) diff --git a/contrib/helm/harbor/Chart.yaml b/contrib/helm/harbor/Chart.yaml index ebcb12043..55fa4b477 100644 --- a/contrib/helm/harbor/Chart.yaml +++ b/contrib/helm/harbor/Chart.yaml @@ -1,7 +1,7 @@ name: harbor -version: 0.0.1 -appVersion: 1.3.0 -description: An Enterprise-class Docker Registry Harbor by VMware +version: 0.1.0 +appVersion: 1.4.0 +description: An Enterprise-class Docker Registry by VMware keywords: - vmware - docker @@ -10,8 +10,10 @@ keywords: home: https://github.com/vmware/harbor icon: https://github.com/vmware/harbor/blob/master/docs/img/harbor_logo.png sources: -- https://github.com/vmware/harbor +- https://github.com/vmware/harbor/tree/master/contrib/helm/harbor maintainers: +- name: Jesse Hu + email: huh@vmware.com - name: paulczar email: username.taken@gmail.com engine: gotpl diff --git a/contrib/helm/harbor/README.md b/contrib/helm/harbor/README.md index 91bc3c6f4..17d6c52f4 100644 --- a/contrib/helm/harbor/README.md +++ b/contrib/helm/harbor/README.md @@ -4,24 +4,75 @@ ## Introduction -This is an experimental monolithic chart that installs and configures VMWare Harbor and its dependencies. The initial implementation of this includes all of the components required to run Harbor. As upstream harbor becomes more cloud native we will be able to break apart the monolith and utitlize helm dependencies. +This chart installs and configures Harbor. ## Prerequisites -- Kubernetes 1.7+ with Beta APIs enabled +- Kubernetes cluster 1.8+ with Beta APIs enabled +- Kubernetes Ingress Controller is enabled +- kubectl CLI 1.8+ - PV provisioner support in the underlying infrastructure +## Setup a Kubernetes cluster + +You can use any tools to setup a K8s cluster. +In this guide, we use [minikube](https://github.com/kubernetes/minikube) to setup a K8s cluster as the dev/test env. + +```bash +# Start minikube +minikube start --vm-driver=none +# Enable Ingress Controller +minikube addons enable ingress +``` + ## Installing the Chart -To install the chart with the release name `my-release`: - +First install [Helm CLI](https://github.com/kubernetes/helm#install), then initialize Helm. ```bash -$ git clone https://github.com/vmware/harbor.git -$ cd harbor/contrib/helm/harbor -$ helm install --name my-release incubator/harbor +helm init --canary-image ``` -The command deploys Harbor on the Kubernetes cluster in the default configuration. The [configuration](#configuration) section lists the parameters that can be configured during installation. +Download Harbor helm chart code. + +```bash +git clone https://github.com/vmware/harbor +cd harbor/contrib/helm/harbor +``` + +### Insecure Registry Mode + +If setting Harbor Registry as insecure-registries for docker, +you don't need to generate Root CA and SSL certificate for the Harbor ingress controller. + +Install the Harbor helm chart with a release name `my-release`: + +```bash +helm install . --debug --name my-release --set externalDomain=harbor.my.domain,insecureRegistry=true +``` + +**Make sure** `harbor.my.domain` resolves to the K8s Ingress Controller IP on the machines where you run docker or access Harbor UI. +You can add `harbor.my.domain` and IP mapping in the DNS server, or in /etc/hosts, or use the FQDN `harbor..xip.io`. + +Then add `"insecure-registries": ["harbor.my.domain"]` in the docker daemon config file and restart docker service. + +### Secure Registry Mode + +By default this chart will generate a root CA and SSL certificate for your Harbor. +You can also use your own CA signed certificate: + +open values.yaml, set the value of 'externalDomain' to your Harbor FQDN, and +set value of 'tlsCrt', 'tlsKey', 'caCrt'. The common name of the certificate must match your Harbor FQDN. + +Install the Harbor helm chart with a release name `my-release`: + +```bash +helm install . --debug --name my-release --set externalDomain=harbor.my.domain +``` + +Follow the `NOTES` section in the command output to get Harbor admin password and **add Harbor root CA into docker trusted certificates**. + +The command deploys Harbor on the Kubernetes cluster in the default configuration. +The [configuration](#configuration) section lists the parameters that can be configured during installation. > **Tip**: List all releases using `helm list` @@ -30,26 +81,29 @@ The command deploys Harbor on the Kubernetes cluster in the default configuratio To uninstall/delete the `my-release` deployment: ```bash -$ helm delete my-release +helm delete my-release ``` The command removes all the Kubernetes components associated with the chart and deletes the release. ## Configuration -The following tables lists the configurable parameters of the Percona chart and their default values. +The following tables lists the configurable parameters of the Harbor chart and the default values. | Parameter | Description | Default | | ----------------------- | ---------------------------------- | ----------------------- | | **Harbor** | -| `externalDomain` | domain harbor will run on (https://*harbor.url*/) |`harbor.192.168.99.100.xip.io` | -| `tls_crt` | TLS certificate to use for Harbor's https endpoint | see values.yaml | -| `tls_key` | TLS key to use for Harbor's https endpoint | see values.yaml | -| `ca_crt` | CA Cert for self signed TLS cert | see values.yaml | +| `harborImageTag` | The tag for Harbor docker images | `v1.4.0` | +| `externalDomain` | Harbor will run on (https://`externalDomain`/). Recommend using K8s Ingress Controller FQDN as `externalDomain`, or make sure this FQDN resolves to the K8s Ingress Controller IP. | `harbor.my.domain` | +| `insecureRegistry` | If set to true, you don't need to set tlsCrt/tlsKey/caCrt, but must add Harbor FQDN as insecure-registries for your docker client. | `false` | +| `tlsCrt` | TLS certificate to use for Harbor's https endpoint. Its CN must match `externalDomain`. | auto-generated | +| `tlsKey` | TLS key to use for Harbor's https endpoint | auto-generated | +| `caCrt` | CA Cert for self signed TLS cert | auto-generated | | `persistence.enabled` | enable persistent data storage | `false` | +| `secretKey` | The secret key used for encryption. Must be a string of 16 chars. | `not-a-secure-key` | | **Adminserver** | | `adminserver.image.repository` | Repository for adminserver image | `vmware/harbor-adminserver` | -| `adminserver.image.tag` | Tag for adminserver image | `v1.3.0` | +| `adminserver.image.tag` | Tag for adminserver image | `v1.4.0` | | `adminserver.image.pullPolicy` | Pull Policy for adminserver image | `IfNotPresent` | | `adminserver.emailHost` | email server | `smtp.mydomain.com` | | `adminserver.emailPort` | email port | `25` | @@ -64,14 +118,14 @@ The following tables lists the configurable parameters of the Percona chart and | `adminserver.volumes` | used to create PVCs if persistence is enabled (see instructions in values.yaml) | see values.yaml | | **Jobservice** | | `jobservice.image.repository` | Repository for jobservice image | `vmware/harbor-jobservice` | -| `jobservice.image.tag` | Tag for jobservice image | `v1.3.0` | +| `jobservice.image.tag` | Tag for jobservice image | `v1.4.0` | | `jobservice.image.pullPolicy` | Pull Policy for jobservice image | `IfNotPresent` | | `jobservice.key` | jobservice key | `not-a-secure-key` | | `jobservice.secret` | jobservice secret | `not-a-secure-secret` | | `jobservice.resources` | [resources](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to allocate for container | undefined | | **UI** | | `ui.image.repository` | Repository for ui image | `vmware/harbor-ui` | -| `ui.image.tag` | Tag for ui image | `v1.3.0` | +| `ui.image.tag` | Tag for ui image | `v1.4.0` | | `ui.image.pullPolicy` | Pull Policy for ui image | `IfNotPresent` | | `ui.key` | ui key | `not-a-secure-key` | | `ui.secret` | ui secret | `not-a-secure-secret` | @@ -79,7 +133,7 @@ The following tables lists the configurable parameters of the Percona chart and | `ui.resources` | [resources](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to allocate for container | undefined | | **MySQL** | | `mysql.image.repository` | Repository for mysql image | `vmware/harbor-mysql` | -| `mysql.image.tag` | Tag for mysql image | `v1.3.0` | +| `mysql.image.tag` | Tag for mysql image | `v1.4.0` | | `mysql.image.pullPolicy` | Pull Policy for mysql image | `IfNotPresent` | | `mysql.host` | MySQL Server | `~` | | `mysql.port` | MySQL Port | `3306` | @@ -89,22 +143,18 @@ The following tables lists the configurable parameters of the Percona chart and | `mysql.resources` | [resources](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to allocate for container | undefined | | `mysql.volumes` | used to create PVCs if persistence is enabled (see instructions in values.yaml) | see values.yaml | | **Registry** | -| `registry.image.repository` | Repository for registry image | `vmware/harbor-registry` | -| `registry.image.tag` | Tag for registry image | `v1.3.0` | +| `registry.image.repository` | Repository for registry image | `vmware/registry-photon` | +| `registry.image.tag` | Tag for registry image | `v2.6.2-v1.4.0` | | `registry.image.pullPolicy` | Pull Policy for registry image | `IfNotPresent` | | `registry.rootCrt` | registry root cert | see values.yaml | | `registry.httpSecret` | registry secret | `not-a-secure-secret` | | `registry.resources` | [resources](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to allocate for container | undefined | | `registry.volumes` | used to create PVCs if persistence is enabled (see instructions in values.yaml) | see values.yaml | | **Clair** | -| `clair.enabled` | Enable clair? | `false` | +| `clair.enabled` | Enable clair? | `true` | +| `clair.image.repository` | Repository for clair image | `vmware/clair-photon` | +| `clair.image.tag` | Tag for clair image | `v2.0.1-v1.4.0` | `clair.postgresPassword` | password for clair postgres | see values.yaml | -| `clair.image.repository` | Repository for clair image | `vmware/clair` | -| `clair.image.tag` | Tag for clair image | `v2.0.1-photon` | -| `clair.image.pullPolicy` | Pull Policy for clair image | `IfNotPresent` | -| `clair.pgImage.repository` | Repository for clair postgres image | `postgres` | -| `clair.pgImage.tag` | Tag for clair postgres image | `9.6.4` | -| `clair.pgImage.pullPolicy` | Pull Policy for clair postgres image | `IfNotPresent` | | `clair.resources` | [resources](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to allocate for container | undefined | `clair.pgResources` | [resources](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to allocate for container | undefined | | | | | @@ -112,13 +162,13 @@ The following tables lists the configurable parameters of the Percona chart and Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example: ```bash -$ helm install --name my-release --set mysql.pass=baconeggs . +helm install --name my-release --set mysql.pass=baconeggs . ``` Alternatively, a YAML file that specifies the values for the parameters can be provided while installing the chart. For example, ```bash -$ helm install --name my-release -f /path/to/values.yaml . +helm install --name my-release -f /path/to/values.yaml . ``` > **Tip**: You can use the default [values.yaml](values.yaml) diff --git a/contrib/helm/harbor/templates/NOTES.txt b/contrib/helm/harbor/templates/NOTES.txt index 446eeb018..7410d1757 100644 --- a/contrib/helm/harbor/templates/NOTES.txt +++ b/contrib/helm/harbor/templates/NOTES.txt @@ -1,15 +1,20 @@ -To add the CA certificate to docker copy the contents of the following command into /etc/docker/certs.d/{{ .Values.externalDomain }}: -$ kubectl get secret \ +Add the Harbor CA certificate to Docker by executing the following command: + + sudo mkdir -p /etc/docker/certs.d/{{ .Values.externalDomain }} + kubectl get secret \ --namespace {{ .Release.Namespace }} {{ template "harbor.fullname" . }}-ingress \ - -o jsonpath="{.data.ca\.crt}" | base64 --decode + -o jsonpath="{.data.ca\.crt}" | base64 --decode | \ + sudo tee /etc/docker/certs.d/{{ .Values.externalDomain }}/ca.crt -Access Harbor via: https://{{ .Values.externalDomain }} +Get Harbor admin password by executing the following command: -login to harbor with docker cli: + kubectl get secret --namespace {{ .Release.Namespace }} {{ template "harbor.fullname" . }}-adminserver -o jsonpath="{.data.HARBOR_ADMIN_PASSWORD}" | base64 --decode; echo -docker login {{ .Values.externalDomain }} +Add Harbor FQDN {{ .Values.externalDomain }} to K8s Ingress Controller IP resolution on DNS Server or in file /etc/hosts. -To get your admin password run the following (not yet ready): +Access Harbor UI via https://{{ .Values.externalDomain }} -$ kubectl get secret --namespace {{ .Release.Namespace }} {{ template "harbor.fullname" . }} -o jsonpath="{.data.}" | base64 --decode; echo +Login Harbor with Docker CLI: + + docker login {{ .Values.externalDomain }} diff --git a/contrib/helm/harbor/templates/_helpers.tpl b/contrib/helm/harbor/templates/_helpers.tpl index 10a7adcba..877856f96 100644 --- a/contrib/helm/harbor/templates/_helpers.tpl +++ b/contrib/helm/harbor/templates/_helpers.tpl @@ -17,7 +17,7 @@ We truncate at 63 chars because some Kubernetes name fields are limited to this {{- end -}} {{/* Helm required labels */}} -{{- define "helm.labels" -}} +{{- define "harbor.labels" -}} heritage: {{ .Release.Service }} release: {{ .Release.Name }} chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} @@ -25,7 +25,7 @@ app: "{{ template "harbor.name" . }}" {{- end -}} {{/* matchLabels */}} -{{- define "helm.matchLabels" -}} +{{- define "harbor.matchLabels" -}} release: {{ .Release.Name }} app: "{{ template "harbor.name" . }}" {{- end -}} diff --git a/contrib/helm/harbor/templates/adminserver/adminserver-cm.yaml b/contrib/helm/harbor/templates/adminserver/adminserver-cm.yaml index f1d926280..559f65e22 100644 --- a/contrib/helm/harbor/templates/adminserver/adminserver-cm.yaml +++ b/contrib/helm/harbor/templates/adminserver/adminserver-cm.yaml @@ -3,7 +3,8 @@ kind: ConfigMap metadata: name: "{{ template "harbor.fullname" . }}-adminserver" labels: -{{ include "helm.labels" . | indent 4 }} +{{ include "harbor.labels" . | indent 4 }} + component: adminserver data: {{ if .Values.mysql.host -}} MYSQL_HOST: "{{ .Values.mysql.host }}" @@ -19,21 +20,26 @@ data: EMAIL_SSL: "{{ .Values.adminserver.emailSsl }}" EMAIL_FROM: "{{ .Values.adminserver.emailFrom }}" EMAIL_IDENTITY: "{{ .Values.adminserver.emailIdentity }}" + EMAIL_INSECURE: "{{ .Values.adminserver.emailInsecure }}" EXT_ENDPOINT: "https://{{ .Values.externalDomain }}" + UI_URL: "http://{{ template "harbor.fullname" . }}-ui" + JOBSERVICE_URL: "http://{{ template "harbor.fullname" . }}-jobservice" REGISTRY_URL: "http://{{ template "harbor.fullname" . }}-registry:5000" TOKEN_SERVICE_URL: "http://{{ template "harbor.fullname" . }}-ui/service/token" WITH_NOTARY: "{{ .Values.notary.enabled }}" LOG_LEVEL: "info" - IMAGE_STORE_PATH: "/" - AUTH_MODE: "database" + IMAGE_STORE_PATH: "/" # This is a temporary hack. + AUTH_MODE: "db_auth" SELF_REGISTRATION: "on" LDAP_URL: "ldaps://ldapserver" LDAP_SEARCH_DN: "" LDAP_BASE_DN: "" LDAP_FILTER: "(objectClass=person)" LDAP_UID: "uid" - LDAP_SCOPE: "3" + LDAP_SCOPE: "2" LDAP_TIMEOUT: "5" + LDAP_TIMEOUT: "5" + LDAP_VERIFY_CERT: "True" DATABASE_TYPE: "mysql" PROJECT_CREATION_RESTRICTION: "everyone" VERIFY_REMOTE_CERT: "off" @@ -45,3 +51,12 @@ data: RESET: "false" WITH_CLAIR: "{{ .Values.clair.enabled }}" CLAIR_DB_HOST: "{{ template "harbor.fullname" . }}-clair-pg" + CLAIR_DB_PORT: "5432" + CLAIR_DB: "postgres" + CLAIR_DB_USERNAME: "postgres" + CLAIR_DB_PASSWORD: "{{ .Values.clair.postgresPassword }}" + UAA_ENDPOINT: "" + UAA_CLIENTID: "" + UAA_CLIENTSECRET: "" + UAA_VERIFY_CERT: "True" + REGISTRY_STORAGE_PROVIDER_NAME: "filesystem" diff --git a/contrib/helm/harbor/templates/adminserver/adminserver-secrets.yaml b/contrib/helm/harbor/templates/adminserver/adminserver-secrets.yaml index 28fe32bd0..6014ca16f 100644 --- a/contrib/helm/harbor/templates/adminserver/adminserver-secrets.yaml +++ b/contrib/helm/harbor/templates/adminserver/adminserver-secrets.yaml @@ -3,10 +3,11 @@ kind: Secret metadata: name: "{{ template "harbor.fullname" . }}-adminserver" labels: -{{ include "helm.labels" . | indent 4 }} +{{ include "harbor.labels" . | indent 4 }} + component: adminserver type: Opaque data: - key: {{ .Values.adminserver.key | b64enc | quote }} + secretKey: {{ .Values.secretKey | b64enc | quote }} EMAIL_PWD: {{ .Values.adminserver.emailPwd | b64enc | quote }} HARBOR_ADMIN_PASSWORD: {{ .Values.adminserver.harborAdminPassword | b64enc | quote }} MYSQL_PWD: {{ .Values.mysql.pass | b64enc | quote }} diff --git a/contrib/helm/harbor/templates/adminserver/adminserver-ss.yaml b/contrib/helm/harbor/templates/adminserver/adminserver-ss.yaml index 0b15523e5..89b5062ab 100644 --- a/contrib/helm/harbor/templates/adminserver/adminserver-ss.yaml +++ b/contrib/helm/harbor/templates/adminserver/adminserver-ss.yaml @@ -3,20 +3,21 @@ kind: StatefulSet metadata: name: "{{ template "harbor.fullname" . }}-adminserver" labels: -{{ include "helm.labels" . | indent 4 }} +{{ include "harbor.labels" . | indent 4 }} component: adminserver spec: replicas: 1 - serviceName: "{{ template "harbor.fullname" . }}" + serviceName: "{{ template "harbor.fullname" . }}-adminserver" selector: matchLabels: -{{ include "helm.matchLabels" . | indent 6 }} +{{ include "harbor.matchLabels" . | indent 6 }} component: adminserver template: metadata: labels: -{{ include "helm.labels" . | indent 8 }} +{{ include "harbor.labels" . | indent 8 }} component: adminserver + component: adminserver spec: containers: - name: adminserver @@ -53,7 +54,7 @@ spec: secret: secretName: "{{ template "harbor.fullname" . }}-adminserver" items: - - key: key + - key: secretKey path: key {{- if .Values.persistence.enabled }} volumeClaimTemplates: diff --git a/contrib/helm/harbor/templates/adminserver/adminserver-svc.yaml b/contrib/helm/harbor/templates/adminserver/adminserver-svc.yaml index d33d32d0e..d613b71ab 100644 --- a/contrib/helm/harbor/templates/adminserver/adminserver-svc.yaml +++ b/contrib/helm/harbor/templates/adminserver/adminserver-svc.yaml @@ -2,12 +2,10 @@ apiVersion: v1 kind: Service metadata: name: "{{ template "harbor.fullname" . }}-adminserver" - labels: -{{ include "helm.labels" . | indent 4 }} spec: ports: - port: 80 targetPort: 8080 selector: -{{ include "helm.matchLabels" . | indent 4 }} - component: adminserver +{{ include "harbor.matchLabels" . | indent 4 }} + component: adminserver \ No newline at end of file diff --git a/contrib/helm/harbor/templates/clair/clair-cm.yaml b/contrib/helm/harbor/templates/clair/clair-cm.yaml index 36125f505..96a617e95 100644 --- a/contrib/helm/harbor/templates/clair/clair-cm.yaml +++ b/contrib/helm/harbor/templates/clair/clair-cm.yaml @@ -4,7 +4,8 @@ kind: ConfigMap metadata: name: {{ template "harbor.fullname" . }} labels: -{{ include "helm.labels" . | indent 4 }} +{{ include "harbor.labels" . | indent 4 }} + component: clair data: config.yaml: | clair: diff --git a/contrib/helm/harbor/templates/clair/clair-dpl.yaml b/contrib/helm/harbor/templates/clair/clair-dpl.yaml index e3f463ad2..f10ec6a9c 100644 --- a/contrib/helm/harbor/templates/clair/clair-dpl.yaml +++ b/contrib/helm/harbor/templates/clair/clair-dpl.yaml @@ -4,14 +4,18 @@ kind: Deployment metadata: name: {{ template "harbor.fullname" . }}-clair labels: -{{ include "helm.labels" . | indent 4 }} +{{ include "harbor.labels" . | indent 4 }} component: clair spec: replicas: 1 + selector: + matchLabels: +{{ include "harbor.matchLabels" . | indent 6 }} + component: clair template: metadata: labels: -{{ include "helm.labels" . | indent 8 }} +{{ include "harbor.labels" . | indent 8 }} component: clair spec: containers: @@ -34,4 +38,4 @@ spec: items: - key: config.yaml path: config.yaml -{{ end }} +{{ end }} \ No newline at end of file diff --git a/contrib/helm/harbor/templates/clair/clair-svc.yaml b/contrib/helm/harbor/templates/clair/clair-svc.yaml index e6a043b86..439673fb9 100644 --- a/contrib/helm/harbor/templates/clair/clair-svc.yaml +++ b/contrib/helm/harbor/templates/clair/clair-svc.yaml @@ -1,19 +1,4 @@ {{ if .Values.clair.enabled }} -apiVersion: v1 -kind: Service -metadata: - name: "{{ template "harbor.fullname" . }}-clair" - labels: -{{ include "helm.labels" . | indent 4 }} -spec: - ports: - - port: 6060 - selector: - app: "{{ template "harbor.fullname" . }}" - component: adminserver - release: {{ .Release.Name }} ---- ---- # clair host isn't configurable yet. this creates a service # to get it working for now. # see https://github.com/vmware/harbor/issues/3250 @@ -21,11 +6,12 @@ apiVersion: v1 kind: Service metadata: name: clair + labels: +{{ include "harbor.labels" . | indent 4 }} spec: ports: - port: 6060 selector: - app: "{{ template "harbor.fullname" . }}" - component: adminserver - release: {{ .Release.Name }} -{{ end }} +{{ include "harbor.matchLabels" . | indent 4 }} + component: clair +{{ end }} \ No newline at end of file diff --git a/contrib/helm/harbor/templates/clair/postgres-secret.yaml b/contrib/helm/harbor/templates/clair/postgres-secret.yaml index 6d70c0e65..efa7f6996 100644 --- a/contrib/helm/harbor/templates/clair/postgres-secret.yaml +++ b/contrib/helm/harbor/templates/clair/postgres-secret.yaml @@ -4,8 +4,8 @@ kind: Secret metadata: name: {{ template "harbor.fullname" . }}-clair-pg-config labels: -{{ include "helm.labels" . | indent 4 }} +{{ include "harbor.labels" . | indent 4 }} type: Opaque data: POSTGRES_PASSWORD: {{ .Values.clair.postgresPassword | b64enc | quote }} -{{ end }} +{{ end }} \ No newline at end of file diff --git a/contrib/helm/harbor/templates/clair/postgres-ss.yaml b/contrib/helm/harbor/templates/clair/postgres-ss.yaml index ebddbfe03..d47721354 100644 --- a/contrib/helm/harbor/templates/clair/postgres-ss.yaml +++ b/contrib/helm/harbor/templates/clair/postgres-ss.yaml @@ -4,19 +4,19 @@ kind: StatefulSet metadata: name: {{ template "harbor.fullname" . }}-clair-pg labels: -{{ include "helm.labels" . | indent 4 }} +{{ include "harbor.labels" . | indent 4 }} component: clair-pg spec: serviceName: "{{ template "harbor.fullname" . }}-clair-pg" selector: matchLabels: -{{ include "helm.matchLabels" . | indent 6 }} +{{ include "harbor.matchLabels" . | indent 6 }} component: clair-pg template: metadata: name: {{ template "harbor.fullname" . }}-clair-pg labels: -{{ include "helm.labels" . | indent 8 }} +{{ include "harbor.labels" . | indent 8 }} component: clair-pg spec: containers: @@ -55,7 +55,7 @@ spec: - metadata: name: pgdata labels: -{{ include "helm.labels" . | indent 8 }} +{{ include "harbor.labels" . | indent 8 }} spec: accessModes: [{{ .Values.clair.volumes.pgData.accessMode | quote }}] {{- if .Values.clair.volumes.pgData.storageClass }} @@ -69,4 +69,4 @@ spec: requests: storage: {{ .Values.clair.volumes.pgData.size | quote }} {{- end -}} -{{- end -}} +{{- end -}} \ No newline at end of file diff --git a/contrib/helm/harbor/templates/clair/postgres-svc.yaml b/contrib/helm/harbor/templates/clair/postgres-svc.yaml index d01a0f13d..2944fd48a 100644 --- a/contrib/helm/harbor/templates/clair/postgres-svc.yaml +++ b/contrib/helm/harbor/templates/clair/postgres-svc.yaml @@ -4,27 +4,11 @@ kind: Service metadata: name: {{ template "harbor.fullname" . }}-clair-pg labels: -{{ include "helm.labels" . | indent 4 }} +{{ include "harbor.labels" . | indent 4 }} spec: ports: - port: 5432 selector: -{{ include "helm.matchLabels" . | indent 4 }} +{{ include "harbor.matchLabels" . | indent 4 }} component: clair-pg ---- -# clairdb host isn't configurable yet. this creates a service -# to get it working for now. -# see https://github.com/vmware/harbor/commit/f63588855f8d3b1b138d3be63ca165bb52ab930c -apiVersion: v1 -kind: Service -metadata: - name: postgres - labels: -{{ include "helm.labels" . | indent 4 }} -spec: - ports: - - port: 5432 - selector: -{{ include "helm.matchLabels" . | indent 4 }} - component: clair-pg -{{ end }} +{{ end }} \ No newline at end of file diff --git a/contrib/helm/harbor/templates/ingress/ingress.yaml b/contrib/helm/harbor/templates/ingress/ingress.yaml index 6651bb25e..18879b055 100644 --- a/contrib/helm/harbor/templates/ingress/ingress.yaml +++ b/contrib/helm/harbor/templates/ingress/ingress.yaml @@ -3,16 +3,16 @@ kind: Ingress metadata: name: "{{ template "harbor.fullname" . }}-ingress" labels: -{{ include "helm.labels" . | indent 4 }} +{{ include "harbor.labels" . | indent 4 }} annotations: - ingress.kubernetes.io/ssl-redirect: "true" - ingress.kubernetes.io/body-size: "0" - ingress.kubernetes.io/proxy-body-size: "0" +{{ toYaml .Values.ingress.annotations | indent 4 }} spec: +{{ if not .Values.insecureRegistry }} tls: - hosts: - "{{ .Values.externalDomain }}" secretName: "{{ template "harbor.fullname" . }}-ingress" +{{ end }} rules: - host: "{{ .Values.externalDomain }}" http: @@ -25,7 +25,3 @@ spec: backend: serviceName: {{ template "harbor.fullname" . }}-registry servicePort: 5000 - - path: /v1 - backend: - serviceName: {{ template "harbor.fullname" . }}-fake-service - servicePort: 5000 diff --git a/contrib/helm/harbor/templates/ingress/secret.yaml b/contrib/helm/harbor/templates/ingress/secret.yaml index 9bd70d1c3..1480d8aa6 100644 --- a/contrib/helm/harbor/templates/ingress/secret.yaml +++ b/contrib/helm/harbor/templates/ingress/secret.yaml @@ -1,11 +1,15 @@ +{{ if not .Values.insecureRegistry }} +{{ $ca := genCA "harbor-ca" 365 }} +{{ $cert := genSignedCert .Values.externalDomain nil nil 365 $ca }} apiVersion: v1 kind: Secret metadata: name: "{{ template "harbor.fullname" . }}-ingress" labels: -{{ include "helm.labels" . | indent 4 }} +{{ include "harbor.labels" . | indent 4 }} type: kubernetes.io/tls data: - tls.crt: {{ .Values.tlsCrt | b64enc | quote }} - tls.key: {{ .Values.tlsKey | b64enc | quote }} - ca.crt: {{ .Values.caCrt | b64enc | quote }} + tls.crt: {{ .Values.tlsCrt | default $cert.Cert | b64enc | quote }} + tls.key: {{ .Values.tlsKey | default $cert.Key | b64enc | quote }} + ca.crt: {{ .Values.caCrt | default $ca.Cert | b64enc | quote }} +{{ end }} diff --git a/contrib/helm/harbor/templates/jobservice/jobservice-cm.yaml b/contrib/helm/harbor/templates/jobservice/jobservice-cm.yaml index d496a9319..b605f4019 100644 --- a/contrib/helm/harbor/templates/jobservice/jobservice-cm.yaml +++ b/contrib/helm/harbor/templates/jobservice/jobservice-cm.yaml @@ -3,7 +3,7 @@ kind: ConfigMap metadata: name: "{{ template "harbor.fullname" . }}-jobservice" labels: -{{ include "helm.labels" . | indent 4 }} +{{ include "harbor.labels" . | indent 4 }} data: app.conf: |+ appname = jobservice diff --git a/contrib/helm/harbor/templates/jobservice/jobservice-dpl.yaml b/contrib/helm/harbor/templates/jobservice/jobservice-dpl.yaml index 181756f19..effa9bbda 100644 --- a/contrib/helm/harbor/templates/jobservice/jobservice-dpl.yaml +++ b/contrib/helm/harbor/templates/jobservice/jobservice-dpl.yaml @@ -3,14 +3,18 @@ kind: Deployment metadata: name: "{{ template "harbor.fullname" . }}-jobservice" labels: -{{ include "helm.labels" . | indent 4 }} +{{ include "harbor.labels" . | indent 4 }} component: jobservice spec: replicas: 1 + selector: + matchLabels: +{{ include "harbor.matchLabels" . | indent 6 }} + component: jobservice template: metadata: labels: -{{ include "helm.labels" . | indent 8 }} +{{ include "harbor.labels" . | indent 8 }} component: jobservice spec: containers: @@ -50,7 +54,7 @@ spec: secret: secretName: "{{ template "harbor.fullname" . }}-jobservice" items: - - key: key + - key: secretKey path: key - name: job-logs emptyDir: {} diff --git a/contrib/helm/harbor/templates/jobservice/jobservice-secrets.yaml b/contrib/helm/harbor/templates/jobservice/jobservice-secrets.yaml index 2ad2e4f1a..64264802c 100644 --- a/contrib/helm/harbor/templates/jobservice/jobservice-secrets.yaml +++ b/contrib/helm/harbor/templates/jobservice/jobservice-secrets.yaml @@ -3,9 +3,9 @@ kind: Secret metadata: name: "{{ template "harbor.fullname" . }}-jobservice" labels: -{{ include "helm.labels" . | indent 4 }} +{{ include "harbor.labels" . | indent 4 }} type: Opaque data: + secretKey: {{ .Values.secretKey | b64enc | quote }} JOBSERVICE_SECRET: {{ .Values.jobservice.secret | b64enc | quote }} - key: {{ .Values.jobservice.key | b64enc | quote }} - UI_SECRET: {{ .Values.ui.secret | b64enc | quote }} + UI_SECRET: {{ .Values.ui.secret | b64enc | quote }} \ No newline at end of file diff --git a/contrib/helm/harbor/templates/jobservice/jobservice-svc.yaml b/contrib/helm/harbor/templates/jobservice/jobservice-svc.yaml index e60f3d46e..0dd1462bc 100644 --- a/contrib/helm/harbor/templates/jobservice/jobservice-svc.yaml +++ b/contrib/helm/harbor/templates/jobservice/jobservice-svc.yaml @@ -3,11 +3,11 @@ kind: Service metadata: name: "{{ template "harbor.fullname" . }}-jobservice" labels: -{{ include "helm.labels" . | indent 4 }} +{{ include "harbor.labels" . | indent 4 }} spec: ports: - port: 80 targetPort: 8080 selector: -{{ include "helm.matchLabels" . | indent 4 }} +{{ include "harbor.matchLabels" . | indent 4 }} component: jobservice diff --git a/contrib/helm/harbor/templates/mysql/mysql-secret.yaml b/contrib/helm/harbor/templates/mysql/mysql-secret.yaml index 5a8845161..a7cebd12d 100644 --- a/contrib/helm/harbor/templates/mysql/mysql-secret.yaml +++ b/contrib/helm/harbor/templates/mysql/mysql-secret.yaml @@ -3,7 +3,7 @@ kind: Secret metadata: name: "{{ template "harbor.fullname" . }}-mysql" labels: -{{ include "helm.labels" . | indent 4 }} +{{ include "harbor.labels" . | indent 4 }} type: Opaque data: mysqlRootPassword: {{ .Values.mysql.pass | b64enc | quote }} diff --git a/contrib/helm/harbor/templates/mysql/mysql-ss.yaml b/contrib/helm/harbor/templates/mysql/mysql-ss.yaml index 2030f2d7b..138ead2ee 100644 --- a/contrib/helm/harbor/templates/mysql/mysql-ss.yaml +++ b/contrib/helm/harbor/templates/mysql/mysql-ss.yaml @@ -3,19 +3,19 @@ kind: StatefulSet metadata: name: "{{ template "harbor.fullname" . }}-mysql" labels: -{{ include "helm.labels" . | indent 4 }} +{{ include "harbor.labels" . | indent 4 }} component: mysql spec: replicas: 1 serviceName: "{{ template "harbor.fullname" . }}-mysql" selector: matchLabels: -{{ include "helm.matchLabels" . | indent 6 }} +{{ include "harbor.matchLabels" . | indent 6 }} component: mysql template: metadata: labels: -{{ include "helm.labels" . | indent 8 }} +{{ include "harbor.labels" . | indent 8 }} component: mysql spec: containers: @@ -43,7 +43,7 @@ spec: - metadata: name: "mysql-data" labels: -{{ include "helm.labels" . | indent 8 }} +{{ include "harbor.labels" . | indent 8 }} spec: accessModes: [{{ .Values.mysql.volumes.data.accessMode | quote }}] {{- if .Values.mysql.volumes.data.storageClass }} diff --git a/contrib/helm/harbor/templates/mysql/mysql-svc.yaml b/contrib/helm/harbor/templates/mysql/mysql-svc.yaml index 4f7d5fbe9..ca18dd836 100644 --- a/contrib/helm/harbor/templates/mysql/mysql-svc.yaml +++ b/contrib/helm/harbor/templates/mysql/mysql-svc.yaml @@ -3,10 +3,10 @@ kind: Service metadata: name: "{{ template "harbor.fullname" . }}-mysql" labels: -{{ include "helm.labels" . | indent 4 }} +{{ include "harbor.labels" . | indent 4 }} spec: ports: - port: 3306 selector: -{{ include "helm.matchLabels" . | indent 4 }} - component: mysql +{{ include "harbor.matchLabels" . | indent 4 }} + component: mysql \ No newline at end of file diff --git a/contrib/helm/harbor/templates/registry/registry-cm.yaml b/contrib/helm/harbor/templates/registry/registry-cm.yaml index 92f90aaa7..b1fd62997 100644 --- a/contrib/helm/harbor/templates/registry/registry-cm.yaml +++ b/contrib/helm/harbor/templates/registry/registry-cm.yaml @@ -3,7 +3,7 @@ kind: ConfigMap metadata: name: "{{ template "harbor.fullname" . }}-registry" labels: -{{ include "helm.labels" . | indent 4 }} +{{ include "harbor.labels" . | indent 4 }} data: config.yml: |+ version: 0.1 diff --git a/contrib/helm/harbor/templates/registry/registry-secret.yaml b/contrib/helm/harbor/templates/registry/registry-secret.yaml index 50b362f97..90b918769 100644 --- a/contrib/helm/harbor/templates/registry/registry-secret.yaml +++ b/contrib/helm/harbor/templates/registry/registry-secret.yaml @@ -3,8 +3,8 @@ kind: Secret metadata: name: "{{ template "harbor.fullname" . }}-registry" labels: -{{ include "helm.labels" . | indent 4 }} +{{ include "harbor.labels" . | indent 4 }} type: Opaque data: httpSecret: {{ .Values.registry.httpSecret | b64enc | quote }} - root.crt: {{ .Values.registry.rootCrt | b64enc | quote }} + root.crt: {{ .Values.registry.rootCrt | b64enc | quote }} \ No newline at end of file diff --git a/contrib/helm/harbor/templates/registry/registry-ss.yaml b/contrib/helm/harbor/templates/registry/registry-ss.yaml index 46b0ea051..0f961fece 100644 --- a/contrib/helm/harbor/templates/registry/registry-ss.yaml +++ b/contrib/helm/harbor/templates/registry/registry-ss.yaml @@ -3,19 +3,19 @@ kind: StatefulSet metadata: name: "{{ template "harbor.fullname" . }}-registry" labels: -{{ include "helm.labels" . | indent 4 }} +{{ include "harbor.labels" . | indent 4 }} component: registry spec: replicas: 1 serviceName: "{{ template "harbor.fullname" . }}-registry" selector: matchLabels: -{{ include "helm.matchLabels" . | indent 6 }} +{{ include "harbor.matchLabels" . | indent 6 }} component: registry template: metadata: labels: -{{ include "helm.labels" . | indent 8 }} +{{ include "harbor.labels" . | indent 8 }} component: registry spec: containers: @@ -61,8 +61,8 @@ spec: volumeClaimTemplates: - metadata: name: "registry-data" - labels: - {{ include "helm.labels" . | indent 8 }} + labels: +{{ include "harbor.labels" . | indent 8 }} spec: accessModes: [{{ .Values.registry.volumes.data.accessMode | quote }}] {{- if .Values.registry.volumes.data.storageClass }} @@ -76,4 +76,4 @@ spec: requests: storage: {{ .Values.registry.volumes.data.size | quote }} {{- end -}} - {{- end -}} + {{- end -}} \ No newline at end of file diff --git a/contrib/helm/harbor/templates/registry/registry-svc.yaml b/contrib/helm/harbor/templates/registry/registry-svc.yaml index 05d308c36..13f956091 100644 --- a/contrib/helm/harbor/templates/registry/registry-svc.yaml +++ b/contrib/helm/harbor/templates/registry/registry-svc.yaml @@ -3,10 +3,10 @@ kind: Service metadata: name: "{{ template "harbor.fullname" . }}-registry" labels: -{{ include "helm.labels" . | indent 4 }} +{{ include "harbor.labels" . | indent 4 }} spec: ports: - port: 5000 selector: -{{ include "helm.matchLabels" . | indent 4 }} - component: registry +{{ include "harbor.matchLabels" . | indent 4 }} + component: registry \ No newline at end of file diff --git a/contrib/helm/harbor/templates/ui/ui-cm.yaml b/contrib/helm/harbor/templates/ui/ui-cm.yaml index 65873782f..6cd0b05d6 100644 --- a/contrib/helm/harbor/templates/ui/ui-cm.yaml +++ b/contrib/helm/harbor/templates/ui/ui-cm.yaml @@ -3,7 +3,7 @@ kind: ConfigMap metadata: name: "{{ template "harbor.fullname" . }}-ui" labels: -{{ include "helm.labels" . | indent 4 }} +{{ include "harbor.labels" . | indent 4 }} data: app.conf: |+ appname = Harbor diff --git a/contrib/helm/harbor/templates/ui/ui-dpl.yaml b/contrib/helm/harbor/templates/ui/ui-dpl.yaml index a650fa59c..d6be078af 100644 --- a/contrib/helm/harbor/templates/ui/ui-dpl.yaml +++ b/contrib/helm/harbor/templates/ui/ui-dpl.yaml @@ -3,14 +3,14 @@ kind: Deployment metadata: name: "{{ template "harbor.fullname" . }}-ui" labels: -{{ include "helm.labels" . | indent 4 }} +{{ include "harbor.labels" . | indent 4 }} component: ui spec: replicas: 1 template: metadata: labels: -{{ include "helm.labels" . | indent 8 }} +{{ include "harbor.matchLabels" . | indent 8 }} component: ui spec: containers: @@ -64,7 +64,7 @@ spec: secret: secretName: "{{ template "harbor.fullname" . }}-ui" items: - - key: key + - key: secretKey path: key - name: ui-secrets-private-key secret: diff --git a/contrib/helm/harbor/templates/ui/ui-secrets.yaml b/contrib/helm/harbor/templates/ui/ui-secrets.yaml index 0a3759f5e..b0c070338 100644 --- a/contrib/helm/harbor/templates/ui/ui-secrets.yaml +++ b/contrib/helm/harbor/templates/ui/ui-secrets.yaml @@ -3,10 +3,10 @@ kind: Secret metadata: name: "{{ template "harbor.fullname" . }}-ui" labels: -{{ include "helm.labels" . | indent 4 }} +{{ include "harbor.labels" . | indent 4 }} type: Opaque data: + secretKey: {{ .Values.secretKey | b64enc | quote }} secret: {{ .Values.ui.secret | b64enc | quote }} - key: {{ .Values.ui.key | b64enc | quote }} private_key.pem: {{ .Values.ui.privateKeyPem | b64enc | quote }} - jobserviceSecret: {{ .Values.jobservice.secret | b64enc | quote }} + jobserviceSecret: {{ .Values.jobservice.secret | b64enc | quote }} \ No newline at end of file diff --git a/contrib/helm/harbor/templates/ui/ui-svc.yaml b/contrib/helm/harbor/templates/ui/ui-svc.yaml index 164f65d57..e74ce16c0 100644 --- a/contrib/helm/harbor/templates/ui/ui-svc.yaml +++ b/contrib/helm/harbor/templates/ui/ui-svc.yaml @@ -3,11 +3,11 @@ kind: Service metadata: name: "{{ template "harbor.fullname" . }}-ui" labels: -{{ include "helm.labels" . | indent 4 }} +{{ include "harbor.labels" . | indent 4 }} spec: ports: - port: 80 targetPort: 8080 selector: -{{ include "helm.matchLabels" . | indent 4 }} +{{ include "harbor.matchLabels" . | indent 4 }} component: ui diff --git a/contrib/helm/harbor/values.yaml b/contrib/helm/harbor/values.yaml index 433ef8645..d53d5f1ab 100644 --- a/contrib/helm/harbor/values.yaml +++ b/contrib/helm/harbor/values.yaml @@ -1,5 +1,5 @@ # Configure persisten Volumes per application -## Applications that require storage have a `volumes` defintion which will be used +## Applications that require storage have a `volumes` definition which will be used ## when `persistence.enabled` is set to true. ## example # mysql: @@ -28,81 +28,34 @@ persistence: enabled: false -externalDomain: harbor.192.168.99.100.xip.io -## tls_crt, tls_key, ca_crt should match the domain above +# The tag for Harbor docker images. +harborImageTag: &harbor_image_tag v1.4.0 -tlsCrt: | - -----BEGIN CERTIFICATE----- - MIIDJDCCAgygAwIBAgIJAKNSg1jp3l2oMA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV - BAMMB3Rlc3QtY2EwHhcNMTgwMTEzMTg1NTIwWhcNMTgwMzE0MTg1NTIwWjAnMSUw - IwYDVQQDDBxoYXJib3IuMTkyLjE2OC45OS4xMDAueGlwLmlvMIIBIjANBgkqhkiG - 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxlAD8tlryoGsweXCwDgfyPGkaM9hXsVLW2PH - /vGWBVMXOdxpFhuvH7tXmqN3Ek39YQjcsb+nHAGx7ynx6KFtvzcXCjGfeI1yuoN0 - 8H2sfV7yxtkVLu/uJGb8mSfsw9ubOR/zMbrsD1oH0tzi3cnW0kcbY0u0Xp/5g0PP - +tig0X+PDfumK/W6KnTOAmnfNTJwhhlljako+lveT5EjVtQMdJmV16PZJwCDA4b9 - 2U8EkLOjXcSg2ad03XxASGUuG8oMLHNXF0zcJ9421DviaRQGJUSjR571t/YCc2KK - AQVZ/zSI5duQVysfMZrjiuvSQfKSWRVY6z0JAWH7+Dx+1u8ilwIDAQABo2gwZjAJ - BgNVHRMEAjAAMAsGA1UdDwQEAwIF4DAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYB - BQUHAwEwLQYDVR0RBCYwJIIcaGFyYm9yLjE5Mi4xNjguOTkuMTAwLnhpcC5pb4cE - wKhjZDANBgkqhkiG9w0BAQsFAAOCAQEATgS0Y2wQiCQrVfiDFSIxtIBK2af0qtoA - J4DZ/1Jo01uGycFCyt9KOKbmFubrJu9NHuACL9od3RI37k6L73lV2zB3sS4NEcH2 - SvF+rOE7gmtgJULHCDFEWSMxHdUFwcdG1trRVe+9Gyp/LGdC4yyycmwquz7YXf+r - 7b5r26rFAYmO8rWYtDt4clC3JSR3O1BmF5ktRNzUtRvrzr3UuwYz0Wy72S/Sa+Iu - RnassP8mg6PCppeGccYFcFihL9kDl4g4Xu/PaMiKdxjdeAV6xAd7VbKBZSi/ljnF - OUUUi7MDJuUWbHEb0XrEXNzihBzf7bu4I2MftQidIg6LwWjiYZRHmw== - -----END CERTIFICATE----- -tlsKey: | - -----BEGIN RSA PRIVATE KEY----- - MIIEpAIBAAKCAQEAxlAD8tlryoGsweXCwDgfyPGkaM9hXsVLW2PH/vGWBVMXOdxp - FhuvH7tXmqN3Ek39YQjcsb+nHAGx7ynx6KFtvzcXCjGfeI1yuoN08H2sfV7yxtkV - Lu/uJGb8mSfsw9ubOR/zMbrsD1oH0tzi3cnW0kcbY0u0Xp/5g0PP+tig0X+PDfum - K/W6KnTOAmnfNTJwhhlljako+lveT5EjVtQMdJmV16PZJwCDA4b92U8EkLOjXcSg - 2ad03XxASGUuG8oMLHNXF0zcJ9421DviaRQGJUSjR571t/YCc2KKAQVZ/zSI5duQ - VysfMZrjiuvSQfKSWRVY6z0JAWH7+Dx+1u8ilwIDAQABAoIBAQC2QDDwzRm/2N6w - r3wEdU/YtyJWZEfi9cRkb9YMGW+64vrUZRh6heSyb9R5vEKgouX6eE+CV1S3a2Ng - HZdBKKIYegOFjcc13iCTAl7E6WpNKaZKUpSiN0QPVkpMYqG3+am0nQU+Lb/l9+J6 - yh8Anw763vhvj9Jqp/CBzx9jNBTPkh6u02Ayhegn7BBIpxk3LmdWSFn4IBXSxnMs - 6B9h8motQFXRJDFm37YFl3834jNWilJT2Z/MCumoAGwNhOFFd5wZM5St1jvfFQlw - A44+AbnOf9sArukXa2NA/HHs6hZHt9GN10kbMBj9wbQRN960OKK4P6+8vVrJ+gUu - iodHLiaxAoGBAP+hlsJvqatgLJmqrODpWrhRqXxWNYs3VJXR5XEEtygVMe2FT7a6 - pu5GWgjpQUHFqgqSNpRiJnxdI+AELH6AkeTMg4EyCoaJJKaitMslnCvQHL5oQjIb - IjJrxk/EObxh/7NuSf/nzUBfmhJhZ/pz6LbBLqiy35Cpq106XVC/XSMJAoGBAMaZ - Qd2nUQPhR7+wxDT38duKRIendYd4BiGqc7z9M91+HLjNg9W7cRPx6bUCaN5E4uwx - PzixsHmWc5H1bDgf9ymAMvexfB3BTXfO9tRn6nZu+XbcN6eJejKYj2iVMjDZrVHu - FZzrusRwPXI1I+b8rnKvNF+wf9DQcIVl7VW1G2CfAoGBAMLPSizzG8JWkKaqwwTD - 0TcWRKtUp4loqTVjuA7hIROS03HHXnBK3lxHkOWpnOma0XMs6hs6kUnFUUmu5Jmj - MYvDr5QNpqfQa/XxmQYXq2RYPQ9+NLQqqWzzZTX0vGsr48nCCvLSnECqmqfXQ35C - Rt6/aed2KZn9M3Lgv6yBqWDBAoGAIEXjeDuqZLEFUddN6zWnrf+IJ2tFJCCTDoF+ - kWWsOgA2dqmfFOqC87TKP8oGdKhJIAzYs0Pc48VZPozdazl2lt3oamwDOWqiRifx - 4I6KgXiDPZeHy8gBfZthIqOsJlgZXEkOZhPApA+BTL/p9610Q9rI7gvmmW5l+qeX - q+fkbQ0CgYBYwF4lcMrON5k8cwZMTyPMwOsY+TsxpsoqUqPPHm9JW2DZDQ1f7oEm - 1b6zTkwtqbnQX7vKosEivCbeQLN0XOZms+BM/KIwcuZjKxy/rkMNaFcqWlOvktug - hk8Jkkt1dANV5rNPYEkt7G+PiL7ApOV6fGvJRA4f8+RFrfPFtku4Sw== - -----END RSA PRIVATE KEY----- -caCrt: | - -----BEGIN CERTIFICATE----- - MIIC9zCCAd+gAwIBAgIJAPRSQQK2Q7dsMA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV - BAMMB3Rlc3QtY2EwHhcNMTgwMTEzMTg1NTIwWhcNMjgwMTExMTg1NTIwWjASMRAw - DgYDVQQDDAd0ZXN0LWNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA - znp0w37dGXlUgAXx5p4cwQ/XEiZGA1NorbcV3RTox3wX4b0bFJwKij8hLFMRZrEd - f40AxvQnyTcoX80c0w1M+3fG/pq5PhsfjrphX/EZ/rYJDZkO4vz60H8uc2g9AgVR - IvYbMobX7KcRruyi2dnt22q6O6Xy0pCkTE/+UAgcbwUCNDA9H/+8RhmXkAEaIyc8 - y5vIpwfjiSdX6Kqv5zg0ZRESE+s9g6+U4NfwHbeUqfl6/ZuP8xXy2az3tdTqN8l0 - dCMjv/dpLzPAOaZhzj+BYN1iVMTFhm6FzszkdTuvJliCIUJeyIqvzqz+k+ai8xR9 - s0hrZrTzmN2id5J5cWSWawIDAQABo1AwTjAdBgNVHQ4EFgQU1L/db3zQjJW8ycmd - 3D3jh4/HtJEwHwYDVR0jBBgwFoAU1L/db3zQjJW8ycmd3D3jh4/HtJEwDAYDVR0T - BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAJrNwGShGJhmVSoBsSi+4tEa98UC8 - 7ULKyvaK+RPYKb4K2igMFHZD3KhaayShEG3rw/Y53hdmU+3I82tQ+txGmQoqicxg - BwAODdvixH5gP4idh7G1Q6tDvgJGGl2HvcE8fzbVIO3qDKefPlif20eX2gUc/Ut5 - gyiyJutOQKVjEUb5bmUaeRyTXo8Vf2TIhIRfdXHg2ueWj2lDWbtVxQbn/m7aSqON - 9YN5xfXY36tpVp40RV1J36FUskkhgc/DZcgEMYdAr2XrjDS1A0TnEaDatQUgYgpd - J0oP9V+2FMfDFvIhX5tNrEuIIFMyO+HR0wxV7huTUeus4knyXBZur3If+g== - -----END CERTIFICATE----- +# The FQDN for Harbor service. +externalDomain: harbor.my.domain +# If set to true, you don't need to set tlsCrt/tlsKey/caCrt, but must add +# Harbor FQDN as insecure-registries for your docker client. +insecureRegistry: false +# The TLS certificate for Harbor. The common name of tlsCrt must match the externalDomain above. +tlsCrt: +tlsKey: +caCrt: + +# The secret key used for encryption. Must be a string of 16 chars. +secretKey: not-a-secure-key + +# These annotations allow the registry to work behind the nginx +# ingress controller. +ingress: + annotations: + ingress.kubernetes.io/ssl-redirect: "true" + ingress.kubernetes.io/body-size: "0" + ingress.kubernetes.io/proxy-body-size: "0" adminserver: image: repository: vmware/harbor-adminserver - tag: v1.3.0 + tag: *harbor_image_tag pullPolicy: IfNotPresent emailHost: "smtp.mydomain.com" emailPort: "25" @@ -110,7 +63,7 @@ adminserver: emailSsl: "false" emailFrom: "admin " emailIdentity: "" - key: not-a-secure-key + emailInsecure: "False" emailPwd: not-a-secure-password harborAdminPassword: Harbor12345 ## Persist data to a persistent volume @@ -129,9 +82,8 @@ adminserver: jobservice: image: repository: vmware/harbor-jobservice - tag: v1.3.0 + tag: *harbor_image_tag pullPolicy: IfNotPresent - key: not-a-secure-key secret: not-a-secure-secret # resources: # requests: @@ -143,10 +95,9 @@ jobservice: ui: image: repository: vmware/harbor-ui - tag: v1.3.0 + tag: *harbor_image_tag pullPolicy: IfNotPresent secret: not-a-secure-secret - key: not-a-secure-key privateKeyPem: | -----BEGIN RSA PRIVATE KEY----- MIIJKAIBAAKCAgEA4WYbxdrFGG6RnfyYKlHYML3lEqtA9cYWWOynE9BeaEr/cMnM @@ -211,7 +162,7 @@ ui: mysql: image: repository: vmware/harbor-db - tag: v1.3.0 + tag: *harbor_image_tag pullPolicy: IfNotPresent # If left blank will use the included mysql service name. host: ~ @@ -231,8 +182,8 @@ mysql: registry: image: - repository: registry - tag: "2.6.2" + repository: vmware/registry-photon + tag: v2.6.2-v1.4.0 pullPolicy: IfNotPresent httpSecret: not-a-secure-secret logLevel: @@ -296,12 +247,12 @@ registry: ## Enabling it will just break things. # clair: - enabled: false - postgresPassword: not-a-secure-password + enabled: true image: - repository: vmware/clair - tag: v2.0.1-photon + repository: vmware/clair-photon + tag: v2.0.1-v1.4.0 pullPolicy: IfNotPresent + postgresPassword: not-a-secure-password pgImage: repository: postgres tag: "9.6.4" From 4ccd7e7cdf27d0524993cc5eb28884ca59884484 Mon Sep 17 00:00:00 2001 From: sigsbee <23101283+sigsbee@users.noreply.github.com> Date: Mon, 12 Mar 2018 14:20:37 +0800 Subject: [PATCH 08/21] Add vulneraibility case and add a testcase doc. (#4378) Add disable schedule vulnerability data not ready can as an unprivileged user scan image with empty vul manual scan all project level image serverity policy scan on push --- tests/resources/Harbor-Pages/Project.robot | 11 +-- tests/resources/Harbor-Pages/ToolKit.robot | 4 +- .../Harbor-Pages/Vulnerability.robot | 64 +++++++++++++ tests/resources/Util.robot | 1 + tests/robot-cases/Group0-BAT/BAT.robot | 4 +- .../robot-cases/Group11-Nightly/Nightly.robot | 91 ++++++++++++++++++- .../Test-10-12-Scan-image-on-push.md | 25 +++++ 7 files changed, 184 insertions(+), 16 deletions(-) create mode 100644 tests/resources/Harbor-Pages/Vulnerability.robot create mode 100644 tests/testcases/Group10-Vulnerability/Test-10-12-Scan-image-on-push.md diff --git a/tests/resources/Harbor-Pages/Project.robot b/tests/resources/Harbor-Pages/Project.robot index 5c144b214..3cc3c6bd8 100644 --- a/tests/resources/Harbor-Pages/Project.robot +++ b/tests/resources/Harbor-Pages/Project.robot @@ -195,13 +195,6 @@ Expand Repo Click Element //repository//clr-dg-row[contains(.,'${projectname}')]//button/clr-icon Sleep 1 -Scan Repo - [Arguments] ${tagname} - #select one tag - Click Element //clr-dg-row[contains(.,"${tagname}")]//label - Click Element //button[contains(.,'Scan')] - Sleep 15 - Edit Repo Info Click Element //*[@id="repo-info"] Sleep 1 @@ -222,6 +215,4 @@ Edit Repo Info Page Should Contain test_description_info Capture Page Screenshot RepoInfo.png -Summary Chart Should Display - [Arguments] ${tagname} - Page Should Contain Element //clr-dg-row[contains(.,'${tagname}')]//hbr-vulnerability-bar//hbr-vulnerability-summary-chart + diff --git a/tests/resources/Harbor-Pages/ToolKit.robot b/tests/resources/Harbor-Pages/ToolKit.robot index b3acd2716..6a9088f1c 100644 --- a/tests/resources/Harbor-Pages/ToolKit.robot +++ b/tests/resources/Harbor-Pages/ToolKit.robot @@ -33,14 +33,16 @@ Partly Success Sleep 1 Filter Object +#Filter project repo user tag. [Arguments] ${kw} Click Element xpath=//hbr-filter//clr-icon Input Text xpath=//hbr-filter//input ${kw} Sleep 1 Select Object +#select single element such as user project repo tag [Arguments] ${obj} - Click Element //clr-dg-cell[contains(.,'${obj}')]//label + Click Element //clr-dg-row[contains(.,'${obj}')]//label Multi-delete Object [Arguments] @{obj} diff --git a/tests/resources/Harbor-Pages/Vulnerability.robot b/tests/resources/Harbor-Pages/Vulnerability.robot new file mode 100644 index 000000000..0e5b67f9c --- /dev/null +++ b/tests/resources/Harbor-Pages/Vulnerability.robot @@ -0,0 +1,64 @@ +*** Settings *** +Documentation This resource provides any keywords related to the Harbor private registry appliance +Resource ../../resources/Util.robot + +*** Variables *** + ${HARBOR_VERSION} v1.1.1 + +*** Keywords *** +Disable Scan Schedule + Click Element //vulnerability-config//select[@id="scanAllPolicy"] + Click Element //vulnerability-config//select[@id="scanAllPolicy"]//option[contains(.,'None')] + Click Element //button[contains(.,'SAVE')] + +Go To Vulnerability Config + Click Element //config//button[contains(.,'Vulnerability')] + +Trigger Scan Now + Click Element //config//button[contains(.,'NOW')] + Sleep 10 + +Set Vulnerabilty Serverity +#0 is high 1 is medium 2 is low 3 is negligible + [Arguments] ${level} + Goto Project Config + #enable first + Click Element //project-detail//clr-checkbox[@name="prevent-vulenrability-image"]//label + Checkbox Should Be Selected //project-detail//clr-checkbox//input[@name="prevent-vulenrability-image"] + Click Element //project-detail//select + #wait for dropdown popup + Sleep 1 + Select From List By Index //project-detail//select ${level} + Click Element //hbr-project-policy-config//button[contains(.,'SAVE')] + +Scan Is Disabled + Page Should Contain Element //button[contains(.,'Scan') and @disabled=''] + +Move To Summary Chart + Mouse Over //hbr-vulnerability-summary-chart + Sleep 1 + +Scan Repo +#use fail for image clair can not scan, otherwise use success + [Arguments] ${tagname} ${status} + #select one tag + Click Element //clr-dg-row[contains(.,"${tagname}")]//label + Click Element //button[contains(.,'Scan')] + Run Keyword If '${status}' == 'Succeed' Wait Until Page Contains Element //hbr-vulnerability-bar//hbr-vulnerability-summary-chart + Run Keyword If '${status}' == 'Fail' Wait Until Page Contains Element //hbr-vulnerability-bar//a + + +Summary Chart Should Display + [Arguments] ${tagname} + Page Should Contain Element //clr-dg-row[contains(.,'${tagname}')]//hbr-vulnerability-bar//hbr-vulnerability-summary-chart + +Enable Scan On Push + Checkbox Should Not Be Selected //clr-checkbox[@name="scan-image-on-push"]//input + Click Element //clr-checkbox[@name="scan-image-on-push"]//label + Checkbox Should Be Selected //clr-checkbox[@name="scan-image-on-push"]//input + Click Element //hbr-project-policy-config//button[contains(.,'SAVE')] + +Vulnerability Not Ready Project Hint + Page Should Contain Element //span[@class="db-status-warning"] +Vulnerability Not Ready Config Hint + Page Should Contain Element //vulnerability-config//clr-tooltip diff --git a/tests/resources/Util.robot b/tests/resources/Util.robot index ea13de942..c1e68a5dc 100644 --- a/tests/resources/Util.robot +++ b/tests/resources/Util.robot @@ -43,6 +43,7 @@ Resource Harbor-Pages/Administration-Users_Elements.robot Resource Harbor-Pages/Configuration.robot Resource Harbor-Pages/Configuration_Elements.robot Resource Harbor-Pages/ToolKit.robot +Resource Harbor-Pages/Vulnerability.robot Resource Docker-Util.robot Resource Admiral-Util.robot Resource OVA-Util.robot diff --git a/tests/robot-cases/Group0-BAT/BAT.robot b/tests/robot-cases/Group0-BAT/BAT.robot index edc274a3f..9e3d1c7b5 100644 --- a/tests/robot-cases/Group0-BAT/BAT.robot +++ b/tests/robot-cases/Group0-BAT/BAT.robot @@ -42,7 +42,7 @@ Test Case - Clair Basic Verfication Push Image ${ip} tester${d} Test1@34 project${d} hello-world Go Into Project project${d} Go Into Repo project${d}/hello-world - Scan Repo latest + Scan Repo latest Succeed Summary Chart Should Display latest #Edit Repo Info Close Browser @@ -71,4 +71,4 @@ Test Case - Ldap Basic Verfication Init LDAP Logout Harbor Sign In Harbor ${HARBOR_URL} mike zhu88jie - Close Browser \ No newline at end of file + Close Browser diff --git a/tests/robot-cases/Group11-Nightly/Nightly.robot b/tests/robot-cases/Group11-Nightly/Nightly.robot index 880d7120d..db0260e82 100644 --- a/tests/robot-cases/Group11-Nightly/Nightly.robot +++ b/tests/robot-cases/Group11-Nightly/Nightly.robot @@ -25,6 +25,16 @@ ${SSH_USER} root ${HARBOR_ADMIN} admin *** Test Cases *** +Test Case - Vulnerability Data Not Ready +#This case must run before vulnerability db ready + Init Chrome Driver + Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} + Go Into Project library + Vulnerability Not Ready Project Hint + Switch To Configure + Go To Vulnerability Config + Vulnerability Not Ready Config Hint + Test Case - Create An New User Init Chrome Driver ${d}= Get Current Date result_format=%m%s @@ -261,11 +271,86 @@ Test Case - Scan A Tag In The Repo Push Image ${ip} tester${d} Test1@34 project${d} hello-world Go Into Project project${d} Go Into Repo project${d}/hello-world - Scan Repo latest + Scan Repo latest Succeed Summary Chart Should Display latest + Pull Image ${ip} tester${d} Test1@34 project${d} hello-world # Edit Repo Info Close Browser +Test Case - Scan As An Unprivileged User + Init Chrome Driver + ${d}= get current date result_format=%m%s + Push Image ${ip} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} library hello-world + Create An New User ${HARBOR_URL} user${d} user${d}@vmware.com user${d} Test1@34 harbor + Go Into Project library + Go Into Repo hello-world + Select Object latest + Scan Is Disabled + Close Browser +## +Test Case - Scan Image With Empty Vul + Init Chrome Driver + Push Image ${ip} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} library hello-world + Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} + Go Into Project library + Go Into Repo hello-world + Scan Repo latest Succeed + Move To Summary Chart + Wait Until Page Contains Unknow + Close Browser +### +Test Case - Disable Scan Schedule + Init Chrome Driver + Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} + Switch To Configure + Go To Vulnerability Config + Disable Scan Schedule + Logout Harbor + Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} + Switch To Configure + Go To Vulnerability Config + Page Should Contain None + Close Browser +### +Test Case - Manual Scan All + Init Chrome Driver + Push Image ${ip} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} library redis + Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} + Switch To Configure + Go To Vulnerability Config + Trigger Scan Now + Back To Projects + Go Into Project library + Go Into Repo redis + Summary Chart Should Display latest + Close Browser +# +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} + Go Into Project library + Go Into Repo haproxy + Scan Repo latest Succeed + Back To Projects + Go Into Project library + Set Vulnerabilty Serverity 0 + Cannot pull image ${ip} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} library haproxy + Close Browser + +Test Case - Scan Image On Push + Init Chrome Driver + Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} + Go Into Project library + Goto Project Config + Enable Scan On Push + Push Image ${ip} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} library memcached + Back To Projects + Go Into Project library + Go Into Repo memcached + Summary Chart Should Display latest + Close Browser + Test Case - Manage Project Member Init Chrome Driver ${d}= Get current Date result_format=%m%s @@ -421,7 +506,7 @@ Test Case - View Scan Results Push Image ${ip} tester${d} Test1@34 project${d} tomcat Go Into Project project${d} Go Into Repo project${d}/tomcat - Scan Repo latest + Scan Repo latest Succeed Summary Chart Should Display latest View Repo Scan Details Close Browser @@ -433,7 +518,7 @@ Test Case - View Scan Error Push Image ${ip} tester${d} Test1@34 project${d} vmware/photon:1.0 Go Into Project project${d} Go Into Repo project${d}/vmware/photon - Scan Repo 1.0 + Scan Repo 1.0 Fail View Scan Error Log Close Browser diff --git a/tests/testcases/Group10-Vulnerability/Test-10-12-Scan-image-on-push.md b/tests/testcases/Group10-Vulnerability/Test-10-12-Scan-image-on-push.md new file mode 100644 index 000000000..eb9b22442 --- /dev/null +++ b/tests/testcases/Group10-Vulnerability/Test-10-12-Scan-image-on-push.md @@ -0,0 +1,25 @@ +Test 10-12 Scan Image on Push +======= + +# Purpose: +To verify that clair can automatic scan image when image is pushed. + +# References: +User guide + +# Environment: +* This test requires that a Harbor instance is running and available. +* Harbor is installed with clair enable. +* A linux host with Docker CLI installed. +* Clair has been updated to the latest. + +# Test Steps: +1. Login Harbor as admin. +2. Create a new project. +3. Goto project configuration page. +4. Enable automatically scan. +5. Push an image not scanned before to the project. + +# Expected Outcome: +* Step5 image should be scaned automatically. + From 7ec809b31ef7d3dabfeb49a76d8c898123d22422 Mon Sep 17 00:00:00 2001 From: Jesse Hu Date: Mon, 12 Mar 2018 14:29:50 +0800 Subject: [PATCH 09/21] Refine harbor helm chart README.md (#4390) --- contrib/helm/harbor/README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/contrib/helm/harbor/README.md b/contrib/helm/harbor/README.md index 17d6c52f4..24ef9ad35 100644 --- a/contrib/helm/harbor/README.md +++ b/contrib/helm/harbor/README.md @@ -1,10 +1,8 @@ -# Project Harbor by VMware - -[Harbor](http://vmware.github.io/harbor/) is an enterprise-class registry server that stores and distributes Docker images. Harbor extends the open source Docker Distribution by adding the functionalities usually required by an enterprise, such as security, identity and management. As an enterprise private registry, Harbor offers better performance and security. Having a registry closer to the build and run environment improves the image transfer efficiency. Harbor supports the setup of multiple registries and has images replicated between them. In addition, Harbor offers advanced security features, such as user management, access control and activity auditing. +# Helm Chart for Harbor ## Introduction -This chart installs and configures Harbor. +This [Helm](https://github.com/kubernetes/helm) chart installs [Harbor](http://vmware.github.io/harbor/) in a Kubernetes cluster. ## Prerequisites @@ -175,6 +173,6 @@ helm install --name my-release -f /path/to/values.yaml . ## Persistence -VMWare Harbor stores the data and configurations in emptyDir volumes. You can change the values.yaml to enable persistence and use a PersistentVolumeClaim instead. +Harbor stores the data and configurations in emptyDir volumes. You can change the values.yaml to enable persistence and use a PersistentVolumeClaim instead. > *"An emptyDir volume is first created when a Pod is assigned to a Node, and exists as long as that Pod is running on that node. When a Pod is removed from a node for any reason, the data in the emptyDir is deleted forever."* From 11254a024d781c8d0b28f10821a9d888cbaad360 Mon Sep 17 00:00:00 2001 From: Manjunath Dharamadas <30418983+mdharamadas1@users.noreply.github.com> Date: Mon, 12 Mar 2018 01:53:16 -0500 Subject: [PATCH 10/21] Add ability to skip check membership on CI (#4386) --- .drone.yml | 4 +++- CONTRIBUTING.md | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index e832a92d7..57baa3354 100644 --- a/.drone.yml +++ b/.drone.yml @@ -17,9 +17,11 @@ pipeline: SHELL: /bin/bash secrets: - github_automation_api_key + - skip_check_membership commands: - echo ${DRONE_COMMIT_AUTHOR} - - /bin/bash -c '[[ ! $(curl --silent "https://api.github.com/orgs/vmware/members/${DRONE_COMMIT_AUTHOR}?access_token=$GITHUB_AUTOMATION_API_KEY") ]]' + - echo $SKIP_CHECK_MEMBERSHIP + - if $SKIP_CHECK_MEMBERSHIP == true; then echo 'check-org-membership step skipped'; else /bin/bash -c '[[ ! $(curl --silent "https://api.github.com/orgs/vmware/members/${DRONE_COMMIT_AUTHOR}?access_token=$GITHUB_AUTOMATION_API_KEY") ]]'; fi when: status: success diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5bb8746a0..b7ad99831 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,6 +29,11 @@ Pull requests (PR) are always welcome, even they are small fixes like typos or a Please submit a PR to contain changes bit by bit. A PR consisting of a lot features and code changes may be hard to review. It is recommended to submit PRs in a incremental fasion. +If you are not a member of `vmware` org in github, then your PR Drone CI build may fail. In that case, request one of the existing members / reviewers to fork your failed build to skip membership checking. +```shell +drone build start --param SKIP_CHECK_MEMBERSHIP=true vmware/harbor +``` + ### Design new features You can propose new designs for existing Harbor features. You can also design From ac05806decc55f4628e7e8e51edab6405a0606e4 Mon Sep 17 00:00:00 2001 From: yixingjia Date: Mon, 12 Mar 2018 15:40:45 +0800 Subject: [PATCH 11/21] Reload the attributes of Harbor db and Clair db when restart Harbor. (#4349) --- src/adminserver/systemcfg/systemcfg.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/adminserver/systemcfg/systemcfg.go b/src/adminserver/systemcfg/systemcfg.go index b188fbfde..664d669d9 100644 --- a/src/adminserver/systemcfg/systemcfg.go +++ b/src/adminserver/systemcfg/systemcfg.go @@ -152,6 +152,13 @@ var ( repeatLoadEnvs = map[string]interface{}{ common.ExtEndpoint: "EXT_ENDPOINT", common.MySQLPassword: "MYSQL_PWD", + common.MySQLHost: "MYSQL_HOST", + common.MySQLUsername: "MYSQL_USR", + common.MySQLDatabase: "MYSQL_DATABASE", + common.MySQLPort: &parser{ + env: "MYSQL_PORT", + parse: parseStringToInt, + }, common.MaxJobWorkers: &parser{ env: "MAX_JOB_WORKERS", parse: parseStringToInt, @@ -170,6 +177,12 @@ var ( parse: parseStringToBool, }, common.ClairDBPassword: "CLAIR_DB_PASSWORD", + common.ClairDBHost: "CLAIR_DB_HOST", + common.ClairDBUsername: "CLAIR_DB_USERNAME", + common.ClairDBPort: &parser{ + env: "CLAIR_DB_PORT", + parse: parseStringToInt, + }, common.UAAEndpoint: "UAA_ENDPOINT", common.UAAClientID: "UAA_CLIENTID", common.UAAClientSecret: "UAA_CLIENTSECRET", From b81b55b0c1e25f6761a7cee01c89bba16e0c909b Mon Sep 17 00:00:00 2001 From: Tan Jiang Date: Mon, 12 Mar 2018 16:07:06 +0800 Subject: [PATCH 12/21] Extend the length of project name in request validation --- src/ui/api/project.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/api/project.go b/src/ui/api/project.go index c70693ec3..8544ca14c 100644 --- a/src/ui/api/project.go +++ b/src/ui/api/project.go @@ -42,7 +42,7 @@ type ProjectAPI struct { project *models.Project } -const projectNameMaxLen int = 30 +const projectNameMaxLen int = 255 const projectNameMinLen int = 2 const restrictedNameChars = `[a-z0-9]+(?:[._-][a-z0-9]+)*` @@ -491,7 +491,7 @@ func (p *ProjectAPI) Logs() { func validateProjectReq(req *models.ProjectRequest) error { pn := req.Name if isIllegalLength(req.Name, projectNameMinLen, projectNameMaxLen) { - return fmt.Errorf("Project name is illegal in length. (greater than 2 or less than 30)") + return fmt.Errorf("Project name is illegal in length. (greater than %d or less than %d)", projectNameMaxLen, projectNameMinLen) } validProjectName := regexp.MustCompile(`^` + restrictedNameChars + `$`) legal := validProjectName.MatchString(pn) From 36b9c4e45864675309d2e37564f308291b8c1a83 Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Mon, 12 Mar 2018 17:13:01 +0800 Subject: [PATCH 13/21] Implement adding/removing labels to/from repositories and images API --- docs/swagger.yaml | 177 ++++++++++++++++++ make/photon/db/registry.sql | 14 ++ make/photon/db/registry_sqlite.sql | 15 ++ src/common/dao/resource_label.go | 74 ++++++++ src/common/dao/resource_label_test.go | 74 ++++++++ src/common/models/base.go | 3 +- src/common/models/label.go | 35 ++-- src/ui/api/harborapi_test.go | 4 + src/ui/api/repository.go | 72 ++++++-- src/ui/api/repository_label.go | 248 +++++++++++++++++++++++++ src/ui/api/repository_label_test.go | 253 ++++++++++++++++++++++++++ src/ui/router.go | 4 + tools/migration/changelog.md | 3 +- 13 files changed, 935 insertions(+), 41 deletions(-) create mode 100644 src/common/dao/resource_label.go create mode 100644 src/common/dao/resource_label_test.go create mode 100644 src/ui/api/repository_label.go create mode 100644 src/ui/api/repository_label_test.go diff --git a/docs/swagger.yaml b/docs/swagger.yaml index c36b60308..898a83af1 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -995,6 +995,82 @@ paths: description: Forbidden. '404': description: Repository not found. + '/repositories/{repo_name}/labels': + get: + summary: Get labels of a repository. + description: | + Get labels of a repository specified by the repo_name. + parameters: + - name: repo_name + in: path + type: string + required: true + description: The name of repository. + tags: + - Products + responses: + '200': + description: Successfully. + '401': + description: Unauthorized. + '403': + description: Forbidden. + '404': + description: Repository not found. + post: + summary: Add a label to the repository. + description: | + Add a label to the repository. + parameters: + - name: repo_name + in: path + type: string + required: true + description: The name of repository. + - name: label + in: body + description: Only the ID property is required. + required: true + schema: + $ref: '#/definitions/Label' + tags: + - Products + responses: + '200': + description: Successfully. + '401': + description: Unauthorized. + '403': + description: Forbidden. + '404': + description: Resource not found. + '/repositories/{repo_name}/labels/{label_id}': + delete: + summary: Delete label from the repository. + description: | + Delete the label from the repository specified by the repo_name. + parameters: + - name: repo_name + in: path + type: string + required: true + description: The name of repository. + - name: label_id + in: path + type: integer + required: true + description: The ID of label. + tags: + - Products + responses: + '200': + description: Successfully. + '401': + description: Unauthorized. + '403': + description: Forbidden. + '404': + description: Resource not found. '/repositories/{repo_name}/tags/{tag}': get: summary: Get the tag of the repository. @@ -1075,6 +1151,97 @@ paths: $ref: '#/definitions/DetailedTag' '500': description: Unexpected internal errors. + '/repositories/{repo_name}/tags/{tag}/labels': + get: + summary: Get labels of an image. + description: | + Get labels of an image specified by the repo_name and tag. + parameters: + - name: repo_name + in: path + type: string + required: true + description: The name of repository. + - name: tag + in: path + type: string + required: true + description: The tag of the image. + tags: + - Products + responses: + '200': + description: Successfully. + '401': + description: Unauthorized. + '403': + description: Forbidden. + '404': + description: Resource not found. + post: + summary: Add a label to image. + description: | + Add a label to the image. + parameters: + - name: repo_name + in: path + type: string + required: true + description: The name of repository. + - name: tag + in: path + type: string + required: true + description: The tag of the image. + - name: label + in: body + description: Only the ID property is required. + required: true + schema: + $ref: '#/definitions/Label' + tags: + - Products + responses: + '200': + description: Successfully. + '401': + description: Unauthorized. + '403': + description: Forbidden. + '404': + description: Resource not found. + '/repositories/{repo_name}/tags/{tag}/labels/{label_id}': + delete: + summary: Delete label from the image. + description: | + Delete the label from the image specified by the repo_name and tag. + parameters: + - name: repo_name + in: path + type: string + required: true + description: The name of repository. + - name: tag + in: path + type: string + required: true + description: The tag of the image. + - name: label_id + in: path + type: integer + required: true + description: The ID of label. + tags: + - Products + responses: + '200': + description: Successfully. + '401': + description: Unauthorized. + '403': + description: Forbidden. + '404': + description: Resource not found. '/repositories/{repo_name}/tags/{tag}/manifest': get: summary: Get manifests of a relevant repository. @@ -3039,6 +3206,11 @@ definitions: type: array items: $ref: '#/definitions/ComponentOverviewEntry' + labels: + type: array + description: The label list. + items: + $ref: '#/definitions/Label' ComponentOverviewEntry: type: object properties: @@ -3072,6 +3244,11 @@ definitions: tags_count: type: integer description: The tags count of repository. + labels: + type: array + description: The label list. + items: + $ref: '#/definitions/Label' creation_time: type: string description: The creation time of repository. diff --git a/make/photon/db/registry.sql b/make/photon/db/registry.sql index 02c1fb6ca..c6d450452 100644 --- a/make/photon/db/registry.sql +++ b/make/photon/db/registry.sql @@ -272,6 +272,20 @@ create table harbor_label ( CONSTRAINT unique_name_and_scope UNIQUE (name,scope) ); +create table harbor_resource_label ( + id int NOT NULL AUTO_INCREMENT, + label_id int NOT NULL, + resource_id varchar(256) NOT NULL, +# 'p' for project +# 'r' for repository +# 'i' for image + resource_type char(1) NOT NULL, + creation_time timestamp default CURRENT_TIMESTAMP, + update_time timestamp default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, + PRIMARY KEY(id), + CONSTRAINT unique_label_resource UNIQUE (label_id,resource_id, resource_type) + ); + CREATE TABLE IF NOT EXISTS `alembic_version` ( `version_num` varchar(32) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/make/photon/db/registry_sqlite.sql b/make/photon/db/registry_sqlite.sql index c5dc67567..b870ff9f7 100644 --- a/make/photon/db/registry_sqlite.sql +++ b/make/photon/db/registry_sqlite.sql @@ -261,6 +261,21 @@ create table harbor_label ( UNIQUE(name, scope) ); +create table harbor_resource_label ( + id INTEGER PRIMARY KEY, + label_id int NOT NULL, + resource_id varchar(256) NOT NULL, +/* + 'p' for project + 'r' for repository + 'i' for image +*/ + resource_type char(1) NOT NULL, + creation_time timestamp default CURRENT_TIMESTAMP, + update_time timestamp default CURRENT_TIMESTAMP, + UNIQUE (label_id,resource_id, resource_type) + ); + create table alembic_version ( version_num varchar(32) NOT NULL ); diff --git a/src/common/dao/resource_label.go b/src/common/dao/resource_label.go new file mode 100644 index 000000000..f89bb27a5 --- /dev/null +++ b/src/common/dao/resource_label.go @@ -0,0 +1,74 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dao + +import ( + "time" + + "github.com/astaxie/beego/orm" + "github.com/vmware/harbor/src/common/models" +) + +// AddResourceLabel add a label to a resource +func AddResourceLabel(rl *models.ResourceLabel) (int64, error) { + now := time.Now() + rl.CreationTime = now + rl.UpdateTime = now + return GetOrmer().Insert(rl) +} + +// GetResourceLabel specified by ID +func GetResourceLabel(rType, rID string, labelID int64) (*models.ResourceLabel, error) { + rl := &models.ResourceLabel{ + ResourceType: rType, + ResourceID: rID, + LabelID: labelID, + } + if err := GetOrmer().Read(rl, "ResourceType", "ResourceID", "LabelID"); err != nil { + if err == orm.ErrNoRows { + return nil, nil + } + return nil, err + } + + return rl, nil +} + +// GetLabelsOfResource returns the label list of the resource +func GetLabelsOfResource(rType, rID string) ([]*models.Label, error) { + sql := `select l.id, l.name, l.description, l.color, l.scope, l.project_id, l.creation_time, l.update_time + from harbor_resource_label rl + join harbor_label l on rl.label_id=l.id + where rl.resource_type = ? and rl.resource_id = ?` + labels := []*models.Label{} + _, err := GetOrmer().Raw(sql, rType, rID).QueryRows(&labels) + return labels, err +} + +// DeleteResourceLabel ... +func DeleteResourceLabel(id int64) error { + _, err := GetOrmer().Delete(&models.ResourceLabel{ + ID: id, + }) + return err +} + +// DeleteLabelsOfResource removes all labels of resource specified by rType and rID +func DeleteLabelsOfResource(rType, rID string) error { + _, err := GetOrmer().QueryTable(&models.ResourceLabel{}). + Filter("ResourceType", rType). + Filter("ResourceID", rID).Delete() + return err +} diff --git a/src/common/dao/resource_label_test.go b/src/common/dao/resource_label_test.go new file mode 100644 index 000000000..33259e1fd --- /dev/null +++ b/src/common/dao/resource_label_test.go @@ -0,0 +1,74 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dao + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/vmware/harbor/src/common" + "github.com/vmware/harbor/src/common/models" +) + +func TestMethodsOfResourceLabel(t *testing.T) { + labelID, err := AddLabel(&models.Label{ + Name: "test_label", + Level: common.LabelLevelUser, + Scope: common.LabelScopeGlobal, + }) + require.Nil(t, err) + defer DeleteLabel(labelID) + + resourceID := "1" + resourceType := common.ResourceTypeRepository + + // add + rl := &models.ResourceLabel{ + LabelID: labelID, + ResourceType: resourceType, + ResourceID: resourceID, + } + + id, err := AddResourceLabel(rl) + require.Nil(t, err) + + // get + r, err := GetResourceLabel(resourceType, resourceID, labelID) + require.Nil(t, err) + assert.Equal(t, id, r.ID) + + // get by resource + labels, err := GetLabelsOfResource(resourceType, resourceID) + require.Nil(t, err) + require.Equal(t, 1, len(labels)) + assert.Equal(t, id, r.ID) + + // delete + err = DeleteResourceLabel(id) + require.Nil(t, err) + labels, err = GetLabelsOfResource(resourceType, resourceID) + require.Nil(t, err) + require.Equal(t, 0, len(labels)) + + // delete by resource + id, err = AddResourceLabel(rl) + require.Nil(t, err) + err = DeleteLabelsOfResource(resourceType, resourceID) + require.Nil(t, err) + labels, err = GetLabelsOfResource(resourceType, resourceID) + require.Nil(t, err) + require.Equal(t, 0, len(labels)) +} diff --git a/src/common/models/base.go b/src/common/models/base.go index 89f201432..a96e3a72e 100644 --- a/src/common/models/base.go +++ b/src/common/models/base.go @@ -33,5 +33,6 @@ func init() { new(WatchItem), new(ProjectMetadata), new(ConfigEntry), - new(Label)) + new(Label), + new(ResourceLabel)) } diff --git a/src/common/models/label.go b/src/common/models/label.go index 90d3f278f..6d949b81c 100644 --- a/src/common/models/label.go +++ b/src/common/models/label.go @@ -35,7 +35,7 @@ type Label struct { UpdateTime time.Time `orm:"column(update_time)" json:"update_time"` } -//TableName ... +// TableName ... func (l *Label) TableName() string { return "harbor_label" } @@ -65,30 +65,17 @@ func (l *Label) Valid(v *validation.Validation) { } } -/* +// ResourceLabel records the relationship between resource and label type ResourceLabel struct { - ID int64 `orm:"pk;auto;column(id)" json:"id"` - LabelID int64 `orm:"column(label_id)" json:"label_id"` - ResourceID string `orm:"column(resource_id)" json:"resource_id"` - ResourceType rune `orm:"column(resource_type)" json:"resource_type"` - CreationTime time.Time `orm:"column(creation_time)" json:"creation_time"` - UpdateTime time.Time `orm:"column(update_time)" json:"update_time"` + ID int64 `orm:"pk;auto;column(id)"` + LabelID int64 `orm:"column(label_id)"` + ResourceID string `orm:"column(resource_id)"` + ResourceType string `orm:"column(resource_type)"` + CreationTime time.Time `orm:"column(creation_time)"` + UpdateTime time.Time `orm:"column(update_time)"` } - -// Valid ... -func (r *ResourceLabel) Valid(v *validation.Validation) { - if r.LabelID <= 0 { - v.SetError("label_id", fmt.Sprintf("invalid: %d", r.LabelID)) - } - // TODO - //if r.ResourceID <= 0 { - // v.SetError("resource_id", fmt.Sprintf("invalid: %v", r.ResourceID)) - //} - if r.ResourceType != common.ResourceTypeProject && - r.ResourceType != common.ResourceTypeRepository && - r.ResourceType != common.ResourceTypeImage { - v.SetError("resource_type", fmt.Sprintf("invalid: %d", r.ResourceType)) - } +// TableName ... +func (r *ResourceLabel) TableName() string { + return "harbor_resource_label" } -*/ diff --git a/src/ui/api/harborapi_test.go b/src/ui/api/harborapi_test.go index b63ce193f..f108aed23 100644 --- a/src/ui/api/harborapi_test.go +++ b/src/ui/api/harborapi_test.go @@ -111,6 +111,10 @@ func init() { beego.Router("/api/users/?:id", &UserAPI{}) beego.Router("/api/logs", &LogAPI{}) beego.Router("/api/repositories/*", &RepositoryAPI{}, "put:Put") + beego.Router("/api/repositories/*/labels", &RepositoryLabelAPI{}, "get:GetOfRepository;post:AddToRepository") + beego.Router("/api/repositories/*/labels/:id([0-9]+", &RepositoryLabelAPI{}, "delete:RemoveFromRepository") + beego.Router("/api/repositories/*/tags/:tag/labels", &RepositoryLabelAPI{}, "get:GetOfImage;post:AddToImage") + beego.Router("/api/repositories/*/tags/:tag/labels/:id([0-9]+", &RepositoryLabelAPI{}, "delete:RemoveFromImage") beego.Router("/api/repositories/*/tags/:tag", &RepositoryAPI{}, "delete:Delete;get:GetTag") beego.Router("/api/repositories/*/tags", &RepositoryAPI{}, "get:GetTags") beego.Router("/api/repositories/*/tags/:tag/manifest", &RepositoryAPI{}, "get:GetManifests") diff --git a/src/ui/api/repository.go b/src/ui/api/repository.go index c61f2c20f..b57786ff6 100644 --- a/src/ui/api/repository.go +++ b/src/ui/api/repository.go @@ -25,6 +25,7 @@ import ( "github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema2" + "github.com/vmware/harbor/src/common" "github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/notifier" @@ -47,15 +48,16 @@ type RepositoryAPI struct { } type repoResp struct { - ID int64 `json:"id"` - Name string `json:"name"` - ProjectID int64 `json:"project_id"` - Description string `json:"description"` - PullCount int64 `json:"pull_count"` - StarCount int64 `json:"star_count"` - TagsCount int64 `json:"tags_count"` - CreationTime time.Time `json:"creation_time"` - UpdateTime time.Time `json:"update_time"` + ID int64 `json:"id"` + Name string `json:"name"` + ProjectID int64 `json:"project_id"` + Description string `json:"description"` + PullCount int64 `json:"pull_count"` + StarCount int64 `json:"star_count"` + TagsCount int64 `json:"tags_count"` + Labels []*models.Label `json:"labels"` + CreationTime time.Time `json:"creation_time"` + UpdateTime time.Time `json:"update_time"` } type tagDetail struct { @@ -78,6 +80,7 @@ type tagResp struct { tagDetail Signature *notary.Target `json:"signature"` ScanOverview *models.ImgScanOverview `json:"scan_overview,omitempty"` + Labels []*models.Label `json:"labels"` } type manifestResp struct { @@ -145,10 +148,10 @@ func getRepositories(projectID int64, keyword string, return nil, err } - return populateTagsCount(repositories) + return assembleRepos(repositories) } -func populateTagsCount(repositories []*models.RepoRecord) ([]*repoResp, error) { +func assembleRepos(repositories []*models.RepoRecord) ([]*repoResp, error) { result := []*repoResp{} for _, repository := range repositories { repo := &repoResp{ @@ -167,6 +170,15 @@ func populateTagsCount(repositories []*models.RepoRecord) ([]*repoResp, error) { return nil, err } repo.TagsCount = int64(len(tags)) + + labels, err := dao.GetLabelsOfResource(common.ResourceTypeRepository, + strconv.FormatInt(repository.RepositoryID, 10)) + if err != nil { + log.Errorf("failed to get labels of repository %s: %v", repository.Name, err) + } else { + repo.Labels = labels + } + result = append(result, repo) } return result, nil @@ -252,6 +264,11 @@ func (ra *RepositoryAPI) Delete() { } for _, t := range tags { + image := fmt.Sprintf("%s:%s", repoName, t) + if err = dao.DeleteLabelsOfResource(common.ResourceTypeImage, image); err != nil { + ra.HandleInternalServerError(fmt.Sprintf("failed to delete labels of image %s: %v", image, err)) + return + } if err = rc.DeleteTag(t); err != nil { if regErr, ok := err.(*registry_error.HTTPError); ok { if regErr.StatusCode == http.StatusNotFound { @@ -296,6 +313,22 @@ func (ra *RepositoryAPI) Delete() { ra.CustomAbort(http.StatusInternalServerError, "") } if !exist { + repository, err := dao.GetRepositoryByName(repoName) + if err != nil { + ra.HandleInternalServerError(fmt.Sprintf("failed to get repository %s: %v", repoName, err)) + return + } + if repository == nil { + ra.HandleNotFound(fmt.Sprintf("repository %s not found", repoName)) + return + } + + if err = dao.DeleteLabelsOfResource(common.ResourceTypeRepository, + strconv.FormatInt(repository.RepositoryID, 10)); err != nil { + ra.HandleInternalServerError(fmt.Sprintf("failed to delete labels of repository %s: %v", + repoName, err)) + return + } if err = dao.DeleteRepository(repoName); err != nil { log.Errorf("failed to delete repository %s: %v", repoName, err) ra.CustomAbort(http.StatusInternalServerError, "") @@ -343,7 +376,7 @@ func (ra *RepositoryAPI) GetTag() { return } - result := assemble(client, repository, []string{tag}, + result := assembleTags(client, repository, []string{tag}, ra.SecurityCtx.GetUsername()) ra.Data["json"] = result[0] ra.ServeJSON() @@ -387,13 +420,13 @@ func (ra *RepositoryAPI) GetTags() { return } - ra.Data["json"] = assemble(client, repoName, tags, ra.SecurityCtx.GetUsername()) + ra.Data["json"] = assembleTags(client, repoName, tags, ra.SecurityCtx.GetUsername()) ra.ServeJSON() } // get config, signature and scan overview and assemble them into one // struct for each tag in tags -func assemble(client *registry.Repository, repository string, +func assembleTags(client *registry.Repository, repository string, tags []string, username string) []*tagResp { var err error @@ -435,6 +468,15 @@ func assemble(client *registry.Repository, repository string, } } + // labels + image := fmt.Sprintf("%s:%s", repository, t) + labels, err := dao.GetLabelsOfResource(common.ResourceTypeImage, image) + if err != nil { + log.Errorf("failed to get labels of image %s: %v", image, err) + } else { + item.Labels = labels + } + result = append(result, item) } @@ -633,7 +675,7 @@ func (ra *RepositoryAPI) GetTopRepos() { ra.CustomAbort(http.StatusInternalServerError, "internal server error") } - result, err := populateTagsCount(repos) + result, err := assembleRepos(repos) if err != nil { log.Errorf("failed to popultate tags count to repositories: %v", err) ra.CustomAbort(http.StatusInternalServerError, "internal server error") diff --git a/src/ui/api/repository_label.go b/src/ui/api/repository_label.go new file mode 100644 index 000000000..95e92cdfc --- /dev/null +++ b/src/ui/api/repository_label.go @@ -0,0 +1,248 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/vmware/harbor/src/common" + "github.com/vmware/harbor/src/common/dao" + "github.com/vmware/harbor/src/common/models" + "github.com/vmware/harbor/src/common/utils" + uiutils "github.com/vmware/harbor/src/ui/utils" +) + +// RepositoryLabelAPI handles requests for adding/removing label to/from repositories and images +type RepositoryLabelAPI struct { + BaseController + repository *models.RepoRecord + tag string + label *models.Label +} + +// Prepare ... +func (r *RepositoryLabelAPI) Prepare() { + r.BaseController.Prepare() + if !r.SecurityCtx.IsAuthenticated() { + r.HandleUnauthorized() + return + } + + repository := r.GetString(":splat") + project, _ := utils.ParseRepository(repository) + if !r.SecurityCtx.HasWritePerm(project) { + r.HandleForbidden(r.SecurityCtx.GetUsername()) + return + } + + repo, err := dao.GetRepositoryByName(repository) + if err != nil { + r.HandleInternalServerError(fmt.Sprintf("failed to get repository %s: %v", + repository, err)) + return + } + if repo == nil { + r.HandleNotFound(fmt.Sprintf("repository %s not found", repository)) + return + } + r.repository = repo + + tag := r.GetString(":tag") + if len(tag) > 0 { + exist, err := imageExist(r.SecurityCtx.GetUsername(), repository, tag) + if err != nil { + r.HandleInternalServerError(fmt.Sprintf("failed to check the existence of image %s:%s: %v", + repository, tag, err)) + return + } + if !exist { + r.HandleNotFound(fmt.Sprintf("image %s:%s not found", repository, tag)) + return + } + r.tag = tag + } + + if r.Ctx.Request.Method == http.MethodPost { + l := &models.Label{} + r.DecodeJSONReq(l) + + label, err := dao.GetLabel(l.ID) + if err != nil { + r.HandleInternalServerError(fmt.Sprintf("failed to get label %d: %v", l.ID, err)) + return + } + + if label == nil { + r.HandleNotFound(fmt.Sprintf("label %d not found", l.ID)) + return + } + + if label.Level != common.LabelLevelUser { + r.HandleBadRequest("only user level labels can be used") + return + } + + if label.Scope == common.LabelScopeProject { + p, err := r.ProjectMgr.Get(project) + if err != nil { + r.HandleInternalServerError(fmt.Sprintf("failed to get project %s: %v", project, err)) + return + } + + if p.ProjectID != label.ProjectID { + r.HandleBadRequest("can not add labels which don't belong to the project to the resources under the project") + return + } + } + r.label = label + + return + } + + if r.Ctx.Request.Method == http.MethodDelete { + labelID, err := r.GetInt64FromPath(":id") + if err != nil { + r.HandleInternalServerError(fmt.Sprintf("failed to get ID parameter from path: %v", err)) + return + } + + label, err := dao.GetLabel(labelID) + if err != nil { + r.HandleInternalServerError(fmt.Sprintf("failed to get label %d: %v", labelID, err)) + return + } + + if label == nil { + r.HandleNotFound(fmt.Sprintf("label %d not found", labelID)) + return + } + r.label = label + } +} + +// GetOfImage returns labels of an image +func (r *RepositoryLabelAPI) GetOfImage() { + r.getLabels(common.ResourceTypeImage, fmt.Sprintf("%s:%s", r.repository.Name, r.tag)) +} + +// AddToImage adds the label to an image +func (r *RepositoryLabelAPI) AddToImage() { + rl := &models.ResourceLabel{ + LabelID: r.label.ID, + ResourceType: common.ResourceTypeImage, + ResourceID: fmt.Sprintf("%s:%s", r.repository.Name, r.tag), + } + r.addLabel(rl) +} + +// RemoveFromImage removes the label from an image +func (r *RepositoryLabelAPI) RemoveFromImage() { + rl := &models.ResourceLabel{ + LabelID: r.label.ID, + ResourceType: common.ResourceTypeImage, + ResourceID: fmt.Sprintf("%s:%s", r.repository.Name, r.tag), + } + r.removeLabel(rl) +} + +// GetOfRepository returns labels of a repository +func (r *RepositoryLabelAPI) GetOfRepository() { + r.getLabels(common.ResourceTypeRepository, strconv.FormatInt(r.repository.RepositoryID, 10)) +} + +// AddToRepository adds the label to a repository +func (r *RepositoryLabelAPI) AddToRepository() { + rl := &models.ResourceLabel{ + LabelID: r.label.ID, + ResourceType: common.ResourceTypeRepository, + ResourceID: strconv.FormatInt(r.repository.RepositoryID, 10), + } + r.addLabel(rl) +} + +// RemoveFromRepository removes the label from a repository +func (r *RepositoryLabelAPI) RemoveFromRepository() { + rl := &models.ResourceLabel{ + LabelID: r.label.ID, + ResourceType: common.ResourceTypeRepository, + ResourceID: strconv.FormatInt(r.repository.RepositoryID, 10), + } + r.removeLabel(rl) +} + +func (r *RepositoryLabelAPI) getLabels(rType, rID string) { + labels, err := dao.GetLabelsOfResource(rType, rID) + if err != nil { + r.HandleInternalServerError(fmt.Sprintf("failed to get labels of resource %s %s: %v", + rType, rID, err)) + return + } + r.Data["json"] = labels + r.ServeJSON() +} + +func (r *RepositoryLabelAPI) addLabel(rl *models.ResourceLabel) { + rlabel, err := dao.GetResourceLabel(rl.ResourceType, rl.ResourceID, rl.LabelID) + if err != nil { + r.HandleInternalServerError(fmt.Sprintf("failed to check the existence of label %d for resource %s %s: %v", + rl.LabelID, rl.ResourceType, rl.ResourceID, err)) + return + } + + if rlabel != nil { + r.HandleConflict() + return + } + if _, err := dao.AddResourceLabel(rl); err != nil { + r.HandleInternalServerError(fmt.Sprintf("failed to add label %d to resource %s %s: %v", + rl.LabelID, rl.ResourceType, rl.ResourceID, err)) + return + } + + // return the ID of label and return status code 200 rather than 201 as the label is not created + r.Redirect(http.StatusOK, strconv.FormatInt(rl.LabelID, 10)) +} + +func (r *RepositoryLabelAPI) removeLabel(rl *models.ResourceLabel) { + rlabel, err := dao.GetResourceLabel(rl.ResourceType, rl.ResourceID, rl.LabelID) + if err != nil { + r.HandleInternalServerError(fmt.Sprintf("failed to check the existence of label %d for resource %s %s: %v", + rl.LabelID, rl.ResourceType, rl.ResourceID, err)) + return + } + + if rlabel == nil { + r.HandleNotFound(fmt.Sprintf("label %d of resource %s %s not found", + rl.LabelID, rl.ResourceType, rl.ResourceID)) + return + } + if err = dao.DeleteResourceLabel(rlabel.ID); err != nil { + r.HandleInternalServerError(fmt.Sprintf("failed to delete resource label record %d: %v", + rlabel.ID, err)) + return + } +} + +func imageExist(username, repository, tag string) (bool, error) { + client, err := uiutils.NewRepositoryClientForUI(username, repository) + if err != nil { + return false, err + } + + _, exist, err := client.ManifestExist(tag) + return exist, err +} diff --git a/src/ui/api/repository_label_test.go b/src/ui/api/repository_label_test.go new file mode 100644 index 000000000..30604d6fc --- /dev/null +++ b/src/ui/api/repository_label_test.go @@ -0,0 +1,253 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/vmware/harbor/src/common" + "github.com/vmware/harbor/src/common/dao" + "github.com/vmware/harbor/src/common/models" +) + +var ( + resourceLabelAPIBasePath = "/api/repositories" + repository = "library/hello-world" + tag = "latest" + proLibraryLabelID int64 +) + +func TestAddToImage(t *testing.T) { + sysLevelLabelID, err := dao.AddLabel(&models.Label{ + Name: "sys_level_label", + Level: common.LabelLevelSystem, + }) + require.Nil(t, err) + defer dao.DeleteLabel(sysLevelLabelID) + + proTestLabelID, err := dao.AddLabel(&models.Label{ + Name: "pro_test_label", + Level: common.LabelLevelUser, + Scope: common.LabelScopeProject, + ProjectID: 100, + }) + require.Nil(t, err) + defer dao.DeleteLabel(proTestLabelID) + + proLibraryLabelID, err = dao.AddLabel(&models.Label{ + Name: "pro_library_label", + Level: common.LabelLevelUser, + Scope: common.LabelScopeProject, + ProjectID: 1, + }) + require.Nil(t, err) + + cases := []*codeCheckingCase{ + // 401 + &codeCheckingCase{ + request: &testingRequest{ + url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath, + repository, tag), + method: http.MethodPost, + }, + code: http.StatusUnauthorized, + }, + // 403 + &codeCheckingCase{ + request: &testingRequest{ + url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath, + repository, tag), + method: http.MethodPost, + credential: projGuest, + }, + code: http.StatusForbidden, + }, + // 404 repository doesn't exist + &codeCheckingCase{ + request: &testingRequest{ + url: fmt.Sprintf("%s/library/non-exist-repo/tags/%s/labels", resourceLabelAPIBasePath, tag), + method: http.MethodPost, + credential: projDeveloper, + }, + code: http.StatusNotFound, + }, + // 404 image doesn't exist + &codeCheckingCase{ + request: &testingRequest{ + url: fmt.Sprintf("%s/%s/tags/non-exist-tag/labels", resourceLabelAPIBasePath, repository), + method: http.MethodPost, + credential: projDeveloper, + }, + code: http.StatusNotFound, + }, + // 400 + &codeCheckingCase{ + request: &testingRequest{ + url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath, repository, tag), + method: http.MethodPost, + credential: projDeveloper, + }, + code: http.StatusBadRequest, + }, + // 404 label doesn't exist + &codeCheckingCase{ + request: &testingRequest{ + url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath, + repository, tag), + method: http.MethodPost, + credential: projDeveloper, + bodyJSON: struct { + ID int64 + }{ + ID: 1000, + }, + }, + code: http.StatusNotFound, + }, + // 400 system level label + &codeCheckingCase{ + request: &testingRequest{ + url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath, + repository, tag), + method: http.MethodPost, + credential: projDeveloper, + bodyJSON: struct { + ID int64 + }{ + ID: sysLevelLabelID, + }, + }, + code: http.StatusBadRequest, + }, + // 400 try to add the label of project1 to the image under project2 + &codeCheckingCase{ + request: &testingRequest{ + url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath, + repository, tag), + method: http.MethodPost, + credential: projDeveloper, + bodyJSON: struct { + ID int64 + }{ + ID: proTestLabelID, + }, + }, + code: http.StatusBadRequest, + }, + // 200 + &codeCheckingCase{ + request: &testingRequest{ + url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath, + repository, tag), + method: http.MethodPost, + credential: projDeveloper, + bodyJSON: struct { + ID int64 + }{ + ID: proLibraryLabelID, + }, + }, + code: http.StatusOK, + }, + } + runCodeCheckingCases(t, cases...) +} + +func TestGetOfImage(t *testing.T) { + labels := []*models.Label{} + err := handleAndParse(&testingRequest{ + url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath, repository, tag), + method: http.MethodGet, + credential: projDeveloper, + }, &labels) + require.Nil(t, err) + require.Equal(t, 1, len(labels)) + assert.Equal(t, proLibraryLabelID, labels[0].ID) +} + +func TestRemoveFromImage(t *testing.T) { + runCodeCheckingCases(t, &codeCheckingCase{ + request: &testingRequest{ + url: fmt.Sprintf("%s/%s/tags/%s/labels/%d", resourceLabelAPIBasePath, + repository, tag, proLibraryLabelID), + method: http.MethodDelete, + credential: projDeveloper, + }, + code: http.StatusOK, + }) + + labels := []*models.Label{} + err := handleAndParse(&testingRequest{ + url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath, + repository, tag), + method: http.MethodGet, + credential: projDeveloper, + }, &labels) + require.Nil(t, err) + require.Equal(t, 0, len(labels)) +} + +func TestAddToRepository(t *testing.T) { + runCodeCheckingCases(t, &codeCheckingCase{ + request: &testingRequest{ + url: fmt.Sprintf("%s/%s/labels", resourceLabelAPIBasePath, repository), + method: http.MethodPost, + bodyJSON: struct { + ID int64 + }{ + ID: proLibraryLabelID, + }, + credential: projDeveloper, + }, + code: http.StatusOK, + }) +} + +func TestGetOfRepository(t *testing.T) { + labels := []*models.Label{} + err := handleAndParse(&testingRequest{ + url: fmt.Sprintf("%s/%s/labels", resourceLabelAPIBasePath, repository), + method: http.MethodGet, + credential: projDeveloper, + }, &labels) + require.Nil(t, err) + require.Equal(t, 1, len(labels)) + assert.Equal(t, proLibraryLabelID, labels[0].ID) +} + +func TestRemoveFromRepository(t *testing.T) { + runCodeCheckingCases(t, &codeCheckingCase{ + request: &testingRequest{ + url: fmt.Sprintf("%s/%s/labels/%d", resourceLabelAPIBasePath, + repository, proLibraryLabelID), + method: http.MethodDelete, + credential: projDeveloper, + }, + code: http.StatusOK, + }) + + labels := []*models.Label{} + err := handleAndParse(&testingRequest{ + url: fmt.Sprintf("%s/%s/labels", resourceLabelAPIBasePath, repository), + method: http.MethodGet, + credential: projDeveloper, + }, &labels) + require.Nil(t, err) + require.Equal(t, 0, len(labels)) +} diff --git a/src/ui/router.go b/src/ui/router.go index bcdc536ec..84437ba77 100644 --- a/src/ui/router.go +++ b/src/ui/router.go @@ -70,7 +70,11 @@ func initRouters() { beego.Router("/api/repositories", &api.RepositoryAPI{}, "get:Get") beego.Router("/api/repositories/scanAll", &api.RepositoryAPI{}, "post:ScanAll") beego.Router("/api/repositories/*", &api.RepositoryAPI{}, "delete:Delete;put:Put") + beego.Router("/api/repositories/*/labels", &api.RepositoryLabelAPI{}, "get:GetOfRepository;post:AddToRepository") + beego.Router("/api/repositories/*/labels/:id([0-9]+", &api.RepositoryLabelAPI{}, "delete:RemoveFromRepository") beego.Router("/api/repositories/*/tags/:tag", &api.RepositoryAPI{}, "delete:Delete;get:GetTag") + beego.Router("/api/repositories/*/tags/:tag/labels", &api.RepositoryLabelAPI{}, "get:GetOfImage;post:AddToImage") + beego.Router("/api/repositories/*/tags/:tag/labels/:id([0-9]+", &api.RepositoryLabelAPI{}, "delete:RemoveFromImage") beego.Router("/api/repositories/*/tags", &api.RepositoryAPI{}, "get:GetTags") beego.Router("/api/repositories/*/tags/:tag/scan", &api.RepositoryAPI{}, "post:ScanImage") beego.Router("/api/repositories/*/tags/:tag/vulnerability/details", &api.RepositoryAPI{}, "Get:VulnerabilityDetails") diff --git a/tools/migration/changelog.md b/tools/migration/changelog.md index 9dff157c3..85406a446 100644 --- a/tools/migration/changelog.md +++ b/tools/migration/changelog.md @@ -68,4 +68,5 @@ Changelog for harbor database schema ## 1.5.0 - - create table `harbor_label` \ No newline at end of file + - create table `harbor_label` + - create table `harbor_resource_label` \ No newline at end of file From 3215d328aac7184d888dc415636c8b6f3c7baf27 Mon Sep 17 00:00:00 2001 From: Yan Date: Tue, 13 Mar 2018 19:21:35 +0800 Subject: [PATCH 14/21] fix run replication bug in nightly (#4402) --- tests/nightly-test/utils/test_executor.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/nightly-test/utils/test_executor.py b/tests/nightly-test/utils/test_executor.py index f352bdee6..50286277b 100644 --- a/tests/nightly-test/utils/test_executor.py +++ b/tests/nightly-test/utils/test_executor.py @@ -40,6 +40,20 @@ def execute(harbor_endpoints, vm_names, harbor_root_pwd, test_suite, auth_mode , sys.stdout.flush() exe_result = p.returncode + if test_suite == 'Replication': + cmd = cmd + "/drone/tests/robot-cases/Group11-Nightly/Replication.robot" + + logger.info(cmd) + p = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE) + while True: + out = p.stderr.read(1) + if out == '' and p.poll() != None: + break + if out != '': + sys.stdout.write(out) + sys.stdout.flush() + exe_result = p.returncode + if test_suite == 'Longevity': cmd = cmd + "/drone/tests/robot-cases/Group12-Longevity/Longevity.robot > /dev/null 2>&1" logger.info(cmd) From bcf81224ad99d49c27ff7fed9607c9697c93c51f Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Wed, 14 Mar 2018 13:42:19 +0800 Subject: [PATCH 15/21] Update according to the comments --- docs/swagger.yaml | 20 ++++++++++++++------ make/photon/db/registry.sql | 3 +++ make/photon/db/registry_sqlite.sql | 5 +++++ src/ui/router.go | 6 +++--- 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 898a83af1..682a59128 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1011,10 +1011,14 @@ paths: responses: '200': description: Successfully. + schema: + type: array + items: + $ref: '#/definitions/Label' '401': description: Unauthorized. '403': - description: Forbidden. + description: Forbidden. User should have read permisson for the repository to perform the action. '404': description: Repository not found. post: @@ -1041,7 +1045,7 @@ paths: '401': description: Unauthorized. '403': - description: Forbidden. + description: Forbidden. User should have write permisson for the repository to perform the action. '404': description: Resource not found. '/repositories/{repo_name}/labels/{label_id}': @@ -1068,7 +1072,7 @@ paths: '401': description: Unauthorized. '403': - description: Forbidden. + description: Forbidden. User should have write permisson for the repository to perform the action. '404': description: Resource not found. '/repositories/{repo_name}/tags/{tag}': @@ -1172,10 +1176,14 @@ paths: responses: '200': description: Successfully. + schema: + type: array + items: + $ref: '#/definitions/Label' '401': description: Unauthorized. '403': - description: Forbidden. + description: Forbidden. User should have read permisson for the image to perform the action. '404': description: Resource not found. post: @@ -1207,7 +1215,7 @@ paths: '401': description: Unauthorized. '403': - description: Forbidden. + description: Forbidden. User should have write permisson for the image to perform the action. '404': description: Resource not found. '/repositories/{repo_name}/tags/{tag}/labels/{label_id}': @@ -1239,7 +1247,7 @@ paths: '401': description: Unauthorized. '403': - description: Forbidden. + description: Forbidden. User should have write permisson for the image to perform the action. '404': description: Resource not found. '/repositories/{repo_name}/tags/{tag}/manifest': diff --git a/make/photon/db/registry.sql b/make/photon/db/registry.sql index c6d450452..02e96d663 100644 --- a/make/photon/db/registry.sql +++ b/make/photon/db/registry.sql @@ -275,6 +275,9 @@ create table harbor_label ( create table harbor_resource_label ( id int NOT NULL AUTO_INCREMENT, label_id int NOT NULL, +# the resource_id is the ID of project when the resource_type is p +# the resource_id is the ID of repository when the resource_type is r +# the resource_id is the name of image when the resource_type is i resource_id varchar(256) NOT NULL, # 'p' for project # 'r' for repository diff --git a/make/photon/db/registry_sqlite.sql b/make/photon/db/registry_sqlite.sql index b870ff9f7..5d045edb0 100644 --- a/make/photon/db/registry_sqlite.sql +++ b/make/photon/db/registry_sqlite.sql @@ -264,6 +264,11 @@ create table harbor_label ( create table harbor_resource_label ( id INTEGER PRIMARY KEY, label_id int NOT NULL, +/* + the resource_id is the ID of project when the resource_type is p + the resource_id is the ID of repository when the resource_type is r + the resource_id is the name of image when the resource_type is i +*/ resource_id varchar(256) NOT NULL, /* 'p' for project diff --git a/src/ui/router.go b/src/ui/router.go index 84437ba77..d6a722d6a 100644 --- a/src/ui/router.go +++ b/src/ui/router.go @@ -71,10 +71,10 @@ func initRouters() { beego.Router("/api/repositories/scanAll", &api.RepositoryAPI{}, "post:ScanAll") beego.Router("/api/repositories/*", &api.RepositoryAPI{}, "delete:Delete;put:Put") beego.Router("/api/repositories/*/labels", &api.RepositoryLabelAPI{}, "get:GetOfRepository;post:AddToRepository") - beego.Router("/api/repositories/*/labels/:id([0-9]+", &api.RepositoryLabelAPI{}, "delete:RemoveFromRepository") + beego.Router("/api/repositories/*/labels/:id([0-9]+)", &api.RepositoryLabelAPI{}, "delete:RemoveFromRepository") beego.Router("/api/repositories/*/tags/:tag", &api.RepositoryAPI{}, "delete:Delete;get:GetTag") beego.Router("/api/repositories/*/tags/:tag/labels", &api.RepositoryLabelAPI{}, "get:GetOfImage;post:AddToImage") - beego.Router("/api/repositories/*/tags/:tag/labels/:id([0-9]+", &api.RepositoryLabelAPI{}, "delete:RemoveFromImage") + beego.Router("/api/repositories/*/tags/:tag/labels/:id([0-9]+)", &api.RepositoryLabelAPI{}, "delete:RemoveFromImage") beego.Router("/api/repositories/*/tags", &api.RepositoryAPI{}, "get:GetTags") beego.Router("/api/repositories/*/tags/:tag/scan", &api.RepositoryAPI{}, "post:ScanImage") beego.Router("/api/repositories/*/tags/:tag/vulnerability/details", &api.RepositoryAPI{}, "Get:VulnerabilityDetails") @@ -99,7 +99,7 @@ func initRouters() { beego.Router("/api/statistics", &api.StatisticAPI{}) beego.Router("/api/replications", &api.ReplicationAPI{}) beego.Router("/api/labels", &api.LabelAPI{}, "post:Post;get:List") - beego.Router("/api/labels/:id([0-9]+", &api.LabelAPI{}, "get:Get;put:Put;delete:Delete") + beego.Router("/api/labels/:id([0-9]+)", &api.LabelAPI{}, "get:Get;put:Put;delete:Delete") beego.Router("/api/systeminfo", &api.SystemInfoAPI{}, "get:GetGeneralInfo") beego.Router("/api/systeminfo/volumes", &api.SystemInfoAPI{}, "get:GetVolumeInfo") From 44fc373c6deffbd281d181f8e82b1cd94e773356 Mon Sep 17 00:00:00 2001 From: stonezdj Date: Wed, 14 Mar 2018 19:07:06 +0800 Subject: [PATCH 16/21] Add LDAP Group Search Configure Param --- make/common/templates/adminserver/env | 4 ++ make/harbor.cfg | 12 ++++ make/prepare | 8 +++ src/adminserver/systemcfg/systemcfg.go | 14 +++- src/common/const.go | 4 ++ src/common/models/ldap.go | 23 +++++-- src/common/utils/ldap/ldap.go | 92 ++++++++++++++++++++++++-- src/common/utils/ldap/ldap_test.go | 90 ++++++++++++++++++++++++- src/common/utils/test/adminserver.go | 4 ++ src/ui/api/config.go | 8 +++ src/ui/config/config.go | 30 +++++++++ src/ui/config/config_test.go | 4 ++ 12 files changed, 277 insertions(+), 16 deletions(-) diff --git a/make/common/templates/adminserver/env b/make/common/templates/adminserver/env index dcb8f642a..9a5fabfac 100644 --- a/make/common/templates/adminserver/env +++ b/make/common/templates/adminserver/env @@ -12,6 +12,10 @@ LDAP_UID=$ldap_uid LDAP_SCOPE=$ldap_scope LDAP_TIMEOUT=$ldap_timeout LDAP_VERIFY_CERT=$ldap_verify_cert +LDAP_GROUP_BASEDN=$ldap_group_basedn +LDAP_GROUP_FILTER=$ldap_group_filter +LDAP_GROUP_GID=$ldap_group_gid +LDAP_GROUP_SCOPE=$ldap_group_scope DATABASE_TYPE=mysql MYSQL_HOST=$db_host MYSQL_PORT=$db_port diff --git a/make/harbor.cfg b/make/harbor.cfg index f784947f9..3a1ab6b67 100644 --- a/make/harbor.cfg +++ b/make/harbor.cfg @@ -91,6 +91,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 diff --git a/make/prepare b/make/prepare index e5df111ef..73addcc16 100755 --- a/make/prepare +++ b/make/prepare @@ -224,6 +224,10 @@ ldap_uid = rcp.get("configuration", "ldap_uid") ldap_scope = rcp.get("configuration", "ldap_scope") ldap_timeout = rcp.get("configuration", "ldap_timeout") ldap_verify_cert = rcp.get("configuration", "ldap_verify_cert") +ldap_group_basedn = rcp.get("configuration", "ldap_group_basedn") +ldap_group_filter = rcp.get("configuration", "ldap_group_filter") +ldap_group_gid = rcp.get("configuration", "ldap_group_gid") +ldap_group_scope = rcp.get("configuration", "ldap_group_scope") db_password = rcp.get("configuration", "db_password") db_host = rcp.get("configuration", "db_host") db_user = rcp.get("configuration", "db_user") @@ -325,6 +329,10 @@ render(os.path.join(templates_dir, "adminserver", "env"), ldap_scope=ldap_scope, ldap_verify_cert=ldap_verify_cert, ldap_timeout=ldap_timeout, + ldap_group_basedn=ldap_group_basedn, + ldap_group_filter=ldap_group_filter, + ldap_group_gid=ldap_group_gid, + ldap_group_scope=ldap_group_scope, db_password=db_password, db_host=db_host, db_user=db_user, diff --git a/src/adminserver/systemcfg/systemcfg.go b/src/adminserver/systemcfg/systemcfg.go index 664d669d9..d48bd66cb 100644 --- a/src/adminserver/systemcfg/systemcfg.go +++ b/src/adminserver/systemcfg/systemcfg.go @@ -89,6 +89,13 @@ var ( env: "LDAP_VERIFY_CERT", parse: parseStringToBool, }, + common.LDAPGroupBaseDN: "LDAP_GROUP_BASEDN", + common.LDAPGroupSearchFilter: "LDAP_GROUP_FILTER", + common.LDAPGroupAttributeName: "LDAP_GROUP_GID", + common.LDAPGroupSearchScope: &parser{ + env: "LDAP_GROUP_SCOPE", + parse: parseStringToInt, + }, common.EmailHost: "EMAIL_HOST", common.EmailPort: &parser{ env: "EMAIL_PORT", @@ -152,7 +159,7 @@ var ( repeatLoadEnvs = map[string]interface{}{ common.ExtEndpoint: "EXT_ENDPOINT", common.MySQLPassword: "MYSQL_PWD", - common.MySQLHost: "MYSQL_HOST", + common.MySQLHost: "MYSQL_HOST", common.MySQLUsername: "MYSQL_USR", common.MySQLDatabase: "MYSQL_DATABASE", common.MySQLPort: &parser{ @@ -179,8 +186,8 @@ var ( common.ClairDBPassword: "CLAIR_DB_PASSWORD", common.ClairDBHost: "CLAIR_DB_HOST", common.ClairDBUsername: "CLAIR_DB_USERNAME", - common.ClairDBPort: &parser{ - env: "CLAIR_DB_PORT", + common.ClairDBPort: &parser{ + env: "CLAIR_DB_PORT", parse: parseStringToInt, }, common.UAAEndpoint: "UAA_ENDPOINT", @@ -395,4 +402,5 @@ func validLdapScope(cfg map[string]interface{}, isMigrate bool) { ldapScope = 0 } cfg[ldapScopeKey] = ldapScope + } diff --git a/src/common/const.go b/src/common/const.go index 44444bd06..1004714c1 100644 --- a/src/common/const.go +++ b/src/common/const.go @@ -59,6 +59,10 @@ const ( LDAPScope = "ldap_scope" LDAPTimeout = "ldap_timeout" LDAPVerifyCert = "ldap_verify_cert" + LDAPGroupBaseDN = "ldap_group_base_dn" + LDAPGroupSearchFilter = "ldap_group_search_filter" + LDAPGroupAttributeName = "ldap_group_attribute_name" + LDAPGroupSearchScope = "ldap_group_search_scope" TokenServiceURL = "token_service_url" RegistryURL = "registry_url" EmailHost = "email_host" diff --git a/src/common/models/ldap.go b/src/common/models/ldap.go index 48e03cc77..28e09d026 100644 --- a/src/common/models/ldap.go +++ b/src/common/models/ldap.go @@ -27,12 +27,21 @@ type LdapConf struct { LdapVerifyCert bool `json:"ldap_verify_cert"` } +// LdapGroupConf holds information about ldap group +type LdapGroupConf struct { + LdapGroupBaseDN string `json:"ldap_group_base_dn,omitempty"` + LdapGroupFilter string `json:"ldap_group_filter,omitempty"` + LdapGroupNameAttribute string `json:"ldap_group_name_attribute,omitempty"` + LdapGroupSearchScope int `json:"ldap_group_search_scope"` +} + // LdapUser ... type LdapUser struct { - Username string `json:"ldap_username"` - Email string `json:"ldap_email"` - Realname string `json:"ldap_realname"` - DN string `json:"-"` + Username string `json:"ldap_username"` + Email string `json:"ldap_email"` + Realname string `json:"ldap_realname"` + DN string `json:"-"` + GroupDNList []string `json:"ldap_groupdn"` } //LdapImportUser ... @@ -45,3 +54,9 @@ type LdapFailedImportUser struct { UID string `json:"uid"` Error string `json:"err_msg"` } + +// LdapGroup ... +type LdapGroup struct { + GroupName string `json:"group_name,omitempty"` + GroupDN string `json:"group_dn,omitempty"` +} diff --git a/src/common/utils/ldap/ldap.go b/src/common/utils/ldap/ldap.go index 77cf1b462..505b3f277 100644 --- a/src/common/utils/ldap/ldap.go +++ b/src/common/utils/ldap/ldap.go @@ -185,6 +185,7 @@ func (session *Session) SearchUser(username string) ([]models.LdapUser, error) { for _, ldapEntry := range result.Entries { var u models.LdapUser + groupDNList := []string{} for _, attr := range ldapEntry.Attributes { //OpenLdap sometimes contain leading space in useranme val := strings.TrimSpace(attr.Values[0]) @@ -200,7 +201,10 @@ func (session *Session) SearchUser(username string) ([]models.LdapUser, error) { u.Email = val case "email": u.Email = val + case "memberof": + groupDNList = append(groupDNList, val) } + u.GroupDNList = groupDNList } u.DN = ldapEntry.DN ldapUsers = append(ldapUsers, u) @@ -248,20 +252,28 @@ func (session *Session) Open() error { // SearchLdap to search ldap with the provide filter func (session *Session) SearchLdap(filter string) (*goldap.SearchResult, error) { - - if err := session.Bind(session.ldapConfig.LdapSearchDn, session.ldapConfig.LdapSearchPassword); err != nil { - return nil, fmt.Errorf("Can not bind search dn, error: %v", err) - } - - attributes := []string{"uid", "cn", "mail", "email"} + attributes := []string{"uid", "cn", "mail", "email", "memberof"} lowerUID := strings.ToLower(session.ldapConfig.LdapUID) if lowerUID != "uid" && lowerUID != "cn" && lowerUID != "mail" && lowerUID != "email" { attributes = append(attributes, session.ldapConfig.LdapUID) } + return session.SearchLdapAttribute(session.ldapConfig.LdapBaseDn, filter, attributes) +} + +// SearchLdapAttribute - to search ldap with the provide filter, with specified attributes +func (session *Session) SearchLdapAttribute(baseDN, filter string, attributes []string) (*goldap.SearchResult, error) { + + if err := session.Bind(session.ldapConfig.LdapSearchDn, session.ldapConfig.LdapSearchPassword); err != nil { + return nil, fmt.Errorf("Can not bind search dn, error: %v", err) + } + filter = strings.TrimSpace(filter) + if !(strings.HasPrefix(filter, "(") || strings.HasSuffix(filter, ")")) { + filter = "(" + filter + ")" + } log.Debugf("Search ldap with filter:%v", filter) searchRequest := goldap.NewSearchRequest( - session.ldapConfig.LdapBaseDn, + baseDN, session.ldapConfig.LdapScope, goldap.NeverDerefAliases, 0, //Unlimited results @@ -318,3 +330,69 @@ func (session *Session) Close() { session.ldapConn.Close() } } + +//SearchGroupByName ... +func (session *Session) SearchGroupByName(groupName string) ([]models.LdapGroup, error) { + ldapGroupConfig, err := config.LDAPGroupConf() + log.Debugf("Ldap group config: %+v", ldapGroupConfig) + if err != nil { + return nil, err + } + return session.searchGroup(ldapGroupConfig.LdapGroupBaseDN, ldapGroupConfig.LdapGroupFilter, groupName, ldapGroupConfig.LdapGroupNameAttribute) +} + +//SearchGroupByDN ... +func (session *Session) SearchGroupByDN(groupDN string) ([]models.LdapGroup, error) { + ldapGroupConfig, err := config.LDAPGroupConf() + log.Debugf("Ldap group config: %+v", ldapGroupConfig) + if err != nil { + return nil, err + } + return session.searchGroup(groupDN, ldapGroupConfig.LdapGroupFilter, "", ldapGroupConfig.LdapGroupNameAttribute) +} + +func (session *Session) searchGroup(baseDN, filter, groupName, groupNameAttribute string) ([]models.LdapGroup, error) { + ldapGroups := make([]models.LdapGroup, 0) + log.Debugf("Groupname: %v, basedn: %v", groupName, baseDN) + ldapFilter := createGroupSearchFilter(filter, groupName, groupNameAttribute) + attributes := []string{groupNameAttribute} + result, err := session.SearchLdapAttribute(baseDN, ldapFilter, attributes) + if err != nil { + return nil, err + } + for _, ldapEntry := range result.Entries { + var group models.LdapGroup + group.GroupDN = ldapEntry.DN + for _, attr := range ldapEntry.Attributes { + //OpenLdap sometimes contain leading space in useranme + val := strings.TrimSpace(attr.Values[0]) + log.Debugf("Current ldap entry attr name: %s\n", attr.Name) + switch strings.ToLower(attr.Name) { + case strings.ToLower(groupNameAttribute): + group.GroupName = val + } + } + ldapGroups = append(ldapGroups, group) + } + return ldapGroups, nil +} + +func createGroupSearchFilter(oldFilter, groupName, groupNameAttribute string) string { + filter := "" + groupName = goldap.EscapeFilter(groupName) + groupNameAttribute = goldap.EscapeFilter(groupNameAttribute) + if len(oldFilter) == 0 { + if len(groupName) == 0 { + filter = groupNameAttribute + "=*" + } else { + filter = groupNameAttribute + "=*" + groupName + "*" + } + } else { + if len(groupName) == 0 { + filter = oldFilter + } else { + filter = "(&(" + oldFilter + ")(" + groupNameAttribute + "=*" + groupName + "*))" + } + } + return filter +} diff --git a/src/common/utils/ldap/ldap_test.go b/src/common/utils/ldap/ldap_test.go index 7f242d548..03e15955d 100644 --- a/src/common/utils/ldap/ldap_test.go +++ b/src/common/utils/ldap/ldap_test.go @@ -15,15 +15,16 @@ package ldap import ( "os" + "reflect" "testing" - "github.com/vmware/harbor/src/common/models" - "github.com/vmware/harbor/src/common" "github.com/vmware/harbor/src/common/dao" + "github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/test" uiConfig "github.com/vmware/harbor/src/ui/config" + goldap "gopkg.in/ldap.v2" ) var adminServerLdapTestConfig = map[string]interface{}{ @@ -217,6 +218,14 @@ func TestSearchUser(t *testing.T) { t.Fatalf("failed to search user test!") } + result2, err := session.SearchUser("mike") + if err != nil || len(result2) == 0 { + t.Fatalf("failed to search user mike!") + } + if len(result2[0].GroupDNList) < 1 && result2[0].GroupDNList[0] != "cn=harbor_users,ou=groups,dc=example,dc=com" { + t.Fatalf("failed to search user mike's memberof") + } + } func TestFormatURL(t *testing.T) { @@ -254,3 +263,80 @@ func TestFormatURL(t *testing.T) { } } + +func Test_createGroupSearchFilter(t *testing.T) { + type args struct { + oldFilter string + groupName string + groupNameAttribute string + } + tests := []struct { + name string + args args + want string + }{ + {"Normal Filter", args{oldFilter: "objectclass=groupOfNames", groupName: "harbor_users", groupNameAttribute: "cn"}, "(&(objectclass=groupOfNames)(cn=*harbor_users*))"}, + {"Empty Old", args{groupName: "harbor_users", groupNameAttribute: "cn"}, "cn=*harbor_users*"}, + {"Empty Both", args{groupNameAttribute: "cn"}, "cn=*"}, + {"Empty name", args{oldFilter: "objectclass=groupOfNames", groupNameAttribute: "cn"}, "objectclass=groupOfNames"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := createGroupSearchFilter(tt.args.oldFilter, tt.args.groupName, tt.args.groupNameAttribute); got != tt.want { + t.Errorf("createGroupSearchFilter() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSession_SearchGroup(t *testing.T) { + type fields struct { + ldapConfig models.LdapConf + ldapConn *goldap.Conn + } + type args struct { + baseDN string + filter string + groupName string + groupNameAttribute string + } + + ldapConfig := models.LdapConf{ + LdapURL: adminServerLdapTestConfig[common.LDAPURL].(string) + ":389", + LdapSearchDn: adminServerLdapTestConfig[common.LDAPSearchDN].(string), + LdapScope: 2, + LdapSearchPassword: adminServerLdapTestConfig[common.LDAPSearchPwd].(string), + LdapBaseDn: adminServerLdapTestConfig[common.LDAPBaseDN].(string), + } + + tests := []struct { + name string + fields fields + args args + want []models.LdapGroup + wantErr bool + }{ + {"normal search", + fields{ldapConfig: ldapConfig}, + args{baseDN: "dc=example,dc=com", filter: "objectClass=groupOfNames", groupName: "harbor_users", groupNameAttribute: "cn"}, + []models.LdapGroup{models.LdapGroup{GroupName: "harbor_users", GroupDN: "cn=harbor_users,ou=groups,dc=example,dc=com"}}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + session := &Session{ + ldapConfig: tt.fields.ldapConfig, + ldapConn: tt.fields.ldapConn, + } + session.Open() + defer session.Close() + got, err := session.searchGroup(tt.args.baseDN, tt.args.filter, tt.args.groupName, tt.args.groupNameAttribute) + if (err != nil) != tt.wantErr { + t.Errorf("Session.SearchGroup() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Session.SearchGroup() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/src/common/utils/test/adminserver.go b/src/common/utils/test/adminserver.go index 8c4397275..7a21276df 100644 --- a/src/common/utils/test/adminserver.go +++ b/src/common/utils/test/adminserver.go @@ -42,6 +42,10 @@ var adminServerDefaultConfig = map[string]interface{}{ common.LDAPFilter: "", common.LDAPScope: 3, common.LDAPTimeout: 30, + common.LDAPGroupBaseDN: "dc=example,dc=com", + common.LDAPGroupSearchFilter: "objectClass=groupOfNames", + common.LDAPGroupSearchScope: 2, + common.LDAPGroupAttributeName: "cn", common.TokenServiceURL: "http://token_service", common.RegistryURL: "http://registry", common.EmailHost: "127.0.0.1", diff --git a/src/ui/api/config.go b/src/ui/api/config.go index acd5cd09b..5fcbf6462 100644 --- a/src/ui/api/config.go +++ b/src/ui/api/config.go @@ -41,6 +41,10 @@ var ( common.LDAPScope, common.LDAPTimeout, common.LDAPVerifyCert, + common.LDAPGroupAttributeName, + common.LDAPGroupBaseDN, + common.LDAPGroupSearchFilter, + common.LDAPGroupSearchScope, common.EmailHost, common.EmailPort, common.EmailUsername, @@ -66,6 +70,9 @@ var ( common.LDAPBaseDN, common.LDAPUID, common.LDAPFilter, + common.LDAPGroupAttributeName, + common.LDAPGroupBaseDN, + common.LDAPGroupSearchFilter, common.EmailHost, common.EmailUsername, common.EmailPassword, @@ -80,6 +87,7 @@ var ( common.EmailPort, common.LDAPScope, common.LDAPTimeout, + common.LDAPGroupSearchScope, common.TokenExpiration, } diff --git a/src/ui/config/config.go b/src/ui/config/config.go index c3d36e331..2967e9222 100644 --- a/src/ui/config/config.go +++ b/src/ui/config/config.go @@ -20,6 +20,7 @@ import ( "fmt" "net/http" "os" + "strconv" "strings" "github.com/vmware/harbor/src/adminserver/client" @@ -205,6 +206,35 @@ func LDAPConf() (*models.LdapConf, error) { return ldapConf, nil } +// LDAPGroupConf returns the setting of ldap group search +func LDAPGroupConf() (*models.LdapGroupConf, error) { + + cfg, err := mg.Get() + if err != nil { + return nil, err + } + + ldapGroupConf := &models.LdapGroupConf{LdapGroupSearchScope: 2} + if _, ok := cfg[common.LDAPGroupBaseDN]; ok { + ldapGroupConf.LdapGroupBaseDN = cfg[common.LDAPGroupBaseDN].(string) + } + if _, ok := cfg[common.LDAPGroupSearchFilter]; ok { + ldapGroupConf.LdapGroupFilter = cfg[common.LDAPGroupSearchFilter].(string) + } + if _, ok := cfg[common.LDAPGroupAttributeName]; ok { + ldapGroupConf.LdapGroupNameAttribute = cfg[common.LDAPGroupAttributeName].(string) + } + if _, ok := cfg[common.LDAPGroupSearchScope]; ok { + if scopeStr, ok := cfg[common.LDAPGroupSearchScope].(string); ok { + ldapGroupConf.LdapGroupSearchScope, err = strconv.Atoi(scopeStr) + } + if scopeFloat, ok := cfg[common.LDAPGroupSearchScope].(float64); ok { + ldapGroupConf.LdapGroupSearchScope = int(scopeFloat) + } + } + return ldapGroupConf, nil +} + // TokenExpiration returns the token expiration time (in minute) func TokenExpiration() (int, error) { cfg, err := mg.Get() diff --git a/src/ui/config/config_test.go b/src/ui/config/config_test.go index aa1508adb..0b0698b65 100644 --- a/src/ui/config/config_test.go +++ b/src/ui/config/config_test.go @@ -75,6 +75,10 @@ func TestConfig(t *testing.T) { t.Fatalf("failed to get ldap settings: %v", err) } + if _, err := LDAPGroupConf(); err != nil { + t.Fatalf("failed to get ldap group settings: %v", err) + } + if _, err := TokenExpiration(); err != nil { t.Fatalf("failed to get token expiration: %v", err) } From 4255ed167a4a7bf7b48edc94c696f82ef81194e1 Mon Sep 17 00:00:00 2001 From: yixingj Date: Mon, 5 Mar 2018 14:13:22 +0800 Subject: [PATCH 17/21] Add monitor API for Harbor components. 1>AdminServer. 2>UI. --- .gitignore | 2 ++ src/adminserver/api/monitor.go | 14 +++++++++++++ src/adminserver/api/monitor_test.go | 16 +++++++++++++++ src/adminserver/handlers/handler.go | 15 ++++++++++++-- src/adminserver/handlers/handlers_test.go | 24 +++++++++++++++++------ src/adminserver/handlers/router.go | 1 + src/ui/api/harborapi_test.go | 7 ++++++- src/ui/api/systeminfo.go | 6 ++++++ src/ui/api/systeminfo_test.go | 7 +++++++ src/ui/router.go | 2 ++ 10 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 src/adminserver/api/monitor.go create mode 100644 src/adminserver/api/monitor_test.go diff --git a/.gitignore b/.gitignore index 20992c591..d79e11032 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,8 @@ src/ui_ng/typings/ **/*yarn-error.log.* .idea/ .DS_Store +.project +.vscode/ **/node_modules **/ssl/ **/proxy.config.json diff --git a/src/adminserver/api/monitor.go b/src/adminserver/api/monitor.go new file mode 100644 index 000000000..fa37f325d --- /dev/null +++ b/src/adminserver/api/monitor.go @@ -0,0 +1,14 @@ +package api + +import ( + "net/http" + "github.com/vmware/harbor/src/common/utils/log" +) + +// Ping monitor the server status +func Ping(w http.ResponseWriter, r *http.Request) { + if err := writeJSON(w, "Pong"); err != nil { + log.Errorf("Failed to write response: %v", err) + return + } +} diff --git a/src/adminserver/api/monitor_test.go b/src/adminserver/api/monitor_test.go new file mode 100644 index 000000000..d7536ce52 --- /dev/null +++ b/src/adminserver/api/monitor_test.go @@ -0,0 +1,16 @@ +package api +import( + "testing" + "net/http/httptest" + "net/http" + "github.com/stretchr/testify/assert" + "io/ioutil" +) + +func TestPing(t *testing.T) { + w := httptest.NewRecorder() + Ping(w, nil) + assert.Equal(t, http.StatusOK, w.Code) + result, _:= ioutil.ReadAll(w.Body) + assert.Equal(t, "\"Pong\"", string(result)) +} diff --git a/src/adminserver/handlers/handler.go b/src/adminserver/handlers/handler.go index 014893dff..b071f40f4 100644 --- a/src/adminserver/handlers/handler.go +++ b/src/adminserver/handlers/handler.go @@ -31,7 +31,10 @@ func NewHandler() http.Handler { "uiSecret": os.Getenv("UI_SECRET"), "jobserviceSecret": os.Getenv("JOBSERVICE_SECRET"), } - h = newAuthHandler(auth.NewSecretAuthenticator(secrets), h) + insecureAPIs := map[string]bool{ + "/api/ping":true, + } + h = newAuthHandler(auth.NewSecretAuthenticator(secrets), h, insecureAPIs) h = gorilla_handlers.LoggingHandler(os.Stdout, h) return h } @@ -39,12 +42,14 @@ func NewHandler() http.Handler { type authHandler struct { authenticator auth.Authenticator handler http.Handler + insecureAPIs map[string]bool } -func newAuthHandler(authenticator auth.Authenticator, handler http.Handler) http.Handler { +func newAuthHandler(authenticator auth.Authenticator, handler http.Handler, insecureAPIs map[string]bool) http.Handler { return &authHandler{ authenticator: authenticator, handler: handler, + insecureAPIs: insecureAPIs, } } @@ -56,6 +61,12 @@ func (a *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + if a.insecureAPIs !=nil && a.insecureAPIs[r.URL.Path] { + if a.handler != nil { + a.handler.ServeHTTP(w, r) + } + return + } valid, err := a.authenticator.Authenticate(r) if err != nil { log.Errorf("failed to authenticate request: %v", err) diff --git a/src/adminserver/handlers/handlers_test.go b/src/adminserver/handlers/handlers_test.go index 2ccd46d9e..af273bb88 100644 --- a/src/adminserver/handlers/handlers_test.go +++ b/src/adminserver/handlers/handlers_test.go @@ -45,28 +45,40 @@ func TestNewAuthHandler(t *testing.T) { cases := []struct { authenticator auth.Authenticator handler http.Handler + insecureAPIs map[string]bool responseCode int + requestURL string }{ - {nil, nil, http.StatusOK}, + {nil, nil, nil, http.StatusOK,"http://localhost/good"}, {&fakeAuthenticator{ authenticated: false, err: nil, - }, nil, http.StatusUnauthorized}, + }, nil, nil, http.StatusUnauthorized,"http://localhost/hello"}, {&fakeAuthenticator{ authenticated: false, err: errors.New("error"), - }, nil, http.StatusInternalServerError}, + }, nil, nil, http.StatusInternalServerError,"http://localhost/hello"}, {&fakeAuthenticator{ authenticated: true, err: nil, - }, &fakeHandler{http.StatusNotFound}, http.StatusNotFound}, + }, &fakeHandler{http.StatusNotFound}, nil, http.StatusNotFound,"http://localhost/notexsit"}, + {&fakeAuthenticator{ + authenticated: false, + err: nil, + }, &fakeHandler{http.StatusOK},map[string]bool{"/api/ping":true,},http.StatusOK,"http://localhost/api/ping"}, } for _, c := range cases { - handler := newAuthHandler(c.authenticator, c.handler) + handler := newAuthHandler(c.authenticator, c.handler, c.insecureAPIs) w := httptest.NewRecorder() - handler.ServeHTTP(w, nil) + r := httptest.NewRequest("GET",c.requestURL,nil) + handler.ServeHTTP(w, r) assert.Equal(t, c.responseCode, w.Code, "unexpected response code") } + handler := NewHandler() + w := httptest.NewRecorder() + r := httptest.NewRequest("GET","http://localhost/api/ping",nil) + handler.ServeHTTP(w,r) + } diff --git a/src/adminserver/handlers/router.go b/src/adminserver/handlers/router.go index 93c3bc5be..df11e8ac2 100644 --- a/src/adminserver/handlers/router.go +++ b/src/adminserver/handlers/router.go @@ -27,5 +27,6 @@ func newRouter() http.Handler { r.HandleFunc("/api/configurations", api.UpdateCfgs).Methods("PUT") r.HandleFunc("/api/configurations/reset", api.ResetCfgs).Methods("POST") r.HandleFunc("/api/systeminfo/capacity", api.Capacity).Methods("GET") + r.HandleFunc("/api/ping", api.Ping).Methods("GET") return r } diff --git a/src/ui/api/harborapi_test.go b/src/ui/api/harborapi_test.go index b63ce193f..b3beab4ce 100644 --- a/src/ui/api/harborapi_test.go +++ b/src/ui/api/harborapi_test.go @@ -134,7 +134,7 @@ func init() { beego.Router("/api/replications", &ReplicationAPI{}) beego.Router("/api/labels", &LabelAPI{}, "post:Post;get:List") beego.Router("/api/labels/:id([0-9]+", &LabelAPI{}, "get:Get;put:Put;delete:Delete") - + beego.Router("/api/ping", &SystemInfoAPI{}, "get:Ping") _ = updateInitPassword(1, "Harbor12345") if err := core.Init(); err != nil { @@ -987,6 +987,11 @@ func (a testapi) GetGeneralInfo() (int, []byte, error) { return request(_sling, jsonAcceptHeader) } +func (a testapi) Ping() (int, []byte, error) { + _sling := sling.New().Get(a.basePath).Path("/api/ping") + return request(_sling, jsonAcceptHeader) +} + //Get system cert func (a testapi) CertGet(authInfo usrInfo) (int, []byte, error) { _sling := sling.New().Get(a.basePath) diff --git a/src/ui/api/systeminfo.go b/src/ui/api/systeminfo.go index 18e37013b..4974ffdbd 100644 --- a/src/ui/api/systeminfo.go +++ b/src/ui/api/systeminfo.go @@ -247,3 +247,9 @@ func getClairVulnStatus() *models.ClairVulnerabilityStatus { res.Details = details return res } + +// Ping ping the harbor UI service. +func (sia *SystemInfoAPI) Ping() { + sia.Data["json"] = "Pong" + sia.ServeJSON() +} diff --git a/src/ui/api/systeminfo_test.go b/src/ui/api/systeminfo_test.go index 2f3bfce5a..edcc8c157 100644 --- a/src/ui/api/systeminfo_test.go +++ b/src/ui/api/systeminfo_test.go @@ -91,3 +91,10 @@ func TestGetCert(t *testing.T) { } CommonDelUser() } +func TestPing(t *testing.T) { + apiTest := newHarborAPI() + code, _, err := apiTest.Ping() + assert := assert.New(t) + assert.Nil(err, fmt.Sprintf("Unexpected Error: %v", err)) + assert.Equal(200, code, fmt.Sprintf("Unexpected status code: %d", code)) +} diff --git a/src/ui/router.go b/src/ui/router.go index bcdc536ec..6a5e2fb5d 100644 --- a/src/ui/router.go +++ b/src/ui/router.go @@ -60,6 +60,7 @@ func initRouters() { } // API + beego.Router("/api/ping", &api.SystemInfoAPI{}, "get:Ping") beego.Router("/api/search", &api.SearchAPI{}) beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List;post:Post") beego.Router("/api/projects/:id([0-9]+)/logs", &api.ProjectAPI{}, "get:Logs") @@ -110,6 +111,7 @@ func initRouters() { beego.Router("/service/token", &token.Handler{}) beego.Router("/registryproxy/*", &controllers.RegistryProxy{}, "*:Handle") + //Error pages beego.ErrorController(&controllers.ErrorController{}) From 38d6306f3b977080f2074210852f1aff6a1ed004 Mon Sep 17 00:00:00 2001 From: Manjunath Dharamadas <30418983+mdharamadas1@users.noreply.github.com> Date: Thu, 15 Mar 2018 21:48:38 -0500 Subject: [PATCH 18/21] Move drone clone step out of pipeline (#4420) --- .drone.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.drone.yml b/.drone.yml index 57baa3354..43a46721c 100644 --- a/.drone.yml +++ b/.drone.yml @@ -2,12 +2,13 @@ workspace: base: /drone path: src/github.com/vmware/harbor -pipeline: - clone: +clone: + git: image: plugins/git tags: true recursive: false +pipeline: check-org-membership: image: 'wdc-harbor-ci.eng.vmware.com/default-project/vic-integration-test:1.44' pull: true From 613464bc1620461bcef515f963d820051f087651 Mon Sep 17 00:00:00 2001 From: Tan Jiang Date: Wed, 21 Mar 2018 16:25:32 +0800 Subject: [PATCH 19/21] Migrate scan job to job service V1 phase1 --- src/common/utils/clair/utils.go | 5 + src/jobservice_v2/job/impl/scan/.job.go.swp | Bin 0 -> 16384 bytes src/jobservice_v2/job/impl/scan/job.go | 139 ++++++++++++++++++++ src/jobservice_v2/job/impl/utils/utils.go | 85 ++++++++++++ src/jobservice_v2/runtime/bootstrap.go | 7 + 5 files changed, 236 insertions(+) create mode 100644 src/jobservice_v2/job/impl/scan/.job.go.swp create mode 100644 src/jobservice_v2/job/impl/scan/job.go create mode 100644 src/jobservice_v2/job/impl/utils/utils.go diff --git a/src/common/utils/clair/utils.go b/src/common/utils/clair/utils.go index 4e1690e2f..d5ef006bb 100644 --- a/src/common/utils/clair/utils.go +++ b/src/common/utils/clair/utils.go @@ -95,3 +95,8 @@ func transformVuln(clairVuln *models.ClairLayerEnvelope) (*models.ComponentsOver Summary: compSummary, }, overallSev } + +//TransformVuln is for running scanning job in both job service V1 and V2. +func TransformVuln(clairVuln *models.ClairLayerEnvelope) (*models.ComponentsOverview, models.Severity) { + return transformVuln(clairVuln) +} diff --git a/src/jobservice_v2/job/impl/scan/.job.go.swp b/src/jobservice_v2/job/impl/scan/.job.go.swp new file mode 100644 index 0000000000000000000000000000000000000000..0551120f90b4dbc7c5493c9b9d6a348e557ccb7f GIT binary patch literal 16384 zcmeHNTZklA89qrimyX5+19@;YoOIlzd(%_BJ6;lInZ&v5rIWcV-7~vvmNA~LI^A76 zRdp(-s%EA$ZdMX881yj_BZ3INC@SnzUi3`};+s!`AgC`IQHcmjAo%^Ks=8}uT4r_; z5vhi6rY`6F=l{R|Kezu>_r!(PGJncC!SH*4u~#0t{`?L*!`}HKV_K=09v}3(5>GjD z>!4DMy-Wg|eYG7mBW*YPZahf3maY8emcK2uYz~C(Ds5g~QQb)DE!UPWY)vFSSGAK?#vbiMepye=6i87LVj87LVj87LVj z87LVj8TdbEAP#S5*I?uAnVn(w|F%8<@5t_tX8&K@^FF&R|CJ1s43rF%43rF%43rF% z43rF%43rF%43rF%4Ezr=ARNZdLa#?D0f6)WeE{fzw#cm;SGco291_{~Qc`!(!~2@G77I0Xz>}0-gkp0KfV$V?P4E1S|s|2mbgW#(oI=0Qf!- z02hIEU=4T}_{Y7B{TcWZ@Gfv2_$sgq41pbB9{8Zb*n7ZVfxiGh1vY^tUW!vFbn+tPR4!-d;@qLcnvrU zoB@sl?;c_7Z6E?9Ab`&U8^EW5yMVtV$#Na|9`Ict0_wmgfTO_2fS3*g7lYwh>qx*@Wt&YbtJTqRXBP(lSwf>g0w+*0a?6 z6aje%qz3$Z3+VfvR3h|ERZ==r-7`XS6LK4J>N17NB}+tq&8wf<6J&X_t)QpC8x;)@ z6Wo+f+Y3cJperzyH|JUB@eo!ctoRFfPDaB`eVLg~u~ISaOLZ7=XsyWY^id6QX5!Q` zE;$`Ip+Y1=m~kb97rMy-P7bw1Sm%u&%DbtHMh?y-7_%evk)fxA)8Hc=+p_PXyA0Rc zOE)|XRpiD>4;zTJO&Q#j))bni)}a(0mn1Ti*!3a`#Wia7jM8lxsxhT>mn$~1p(AaK zSq1e?-JJMCsBU@}4-?O$5b;Ie_RzOE1%c}eIR98Qa|~mV=~bgv)Ax z1F<#o1!BO28BEIp`#dyR&a@cYZbV<+NC&sw$R0>vOk2y+amCs&lzD}#6Aa#`3gRkj zY!oRe^9bXyU8y>7!n30<^v3Fpixrrk98L#0=vKM}`UOUNa*-KF;FUu~TkgG)*%N1x zI(m89UCA@JvKD1eV60s-eYggSn@zs9y0|)H2I8!&8Hnw8=b3bZs@YfgaWe>jzdCOx zK@Bgt47MyhSWuXrb`Z7)Ok4;L7`e;PAS56lLr?k`uOl80Bp=OjxT0R&Y~_<)aZrHAQ;Di{wOAIs=t>PFrFus?~W{DOxMQ=ECz^Yx3m8=9a|{ z&7~3AO2A>-R{rqj8j8{R(&-mo<>;P?oyp66&)w8n**nbMCT7o>PhcO9c*_hj+xSK{ zm4l@qt5k30n_1I|t2b8}@pxbjEMt5XG>!NsqW$J7m4jhBrpne&-L9{kPZ<~0!M1oznAb;bP!3)bOkAk&&=RQV?Y#2 z+wHkF)+j+gA)$SRm8x0qgi`x%6d9=^a7fI>*4nw%^)-HRuHBwnS!IwN@4}Od=|wi`>CVB8s?~5<@H)9i!5SIxI@}DNSKtET9686<>9( z!&{vypP%crIt@~2J@81hmM2q?3AxZcR;y9dXHn+F8Edh=P(o(wLgfOBpYA&@FPOo%MPfg=1__WR?m0-#ra@CCm zL<_9oL)`UlpjyNvewY;vRLqlzoB<1aSdoJsTZS=>!L;D_ToKrkn+{D5DmpQ{F48q; z^vp{&_ywuSVH`U08r836m8$hqnbHf;QM>&CLdmZ4=@TcPkGJW4 z#IdHwu7&(X`~R!hTi=O2IPL%Q^ZoB&uYV1A9C!%04fp_i{WpQXVGsX1;BDY_;AP+? zU>!IGyboEw1%3g%2D}P*z%#%p;9=k>a5q5h9s$Z<$w0|K$w0|K$w0|K$w0|K$w0}# ze~E$dNk^V3rsR;xNv98R(u3WX44f>*kHj^y5vD+e^5Yu0C(8JvTv}n`aX^J~_Jwl- z{xm;_{vE)1|Etf$SHqkuFwktX&{N}Vjq3-Tvs_i4(X;MFPQ0aE&Gr2O0RzWlRbjBqjC$_X4|I6vh^q(gJCE9J}By zTo`j~T4NQan`b;s-;xX?kp~nubn4TU*zHeY3yocA#T)>njv@mK2Y9L9cdK9`kT#`8 zsx!Q5dn%IfT6Hdo2THpynWPEc+~C-G30rh}yc2KM5%z^+m()O$1D 0 { + l.ParentName = layers[len(layers)-1].Name + } + layers = append(layers, l) + } + return layers, nil +} diff --git a/src/jobservice_v2/job/impl/utils/utils.go b/src/jobservice_v2/job/impl/utils/utils.go new file mode 100644 index 000000000..758b03399 --- /dev/null +++ b/src/jobservice_v2/job/impl/utils/utils.go @@ -0,0 +1,85 @@ +package utils + +import ( + "fmt" + "net/http" + + "github.com/docker/distribution/registry/auth/token" + "github.com/vmware/harbor/src/common/models" + "github.com/vmware/harbor/src/common/utils/registry" + "github.com/vmware/harbor/src/common/utils/registry/auth" +) + +// NewRepositoryClient creates a repository client with standard token authorizer +func NewRepositoryClient(endpoint string, insecure bool, credential auth.Credential, + tokenServiceEndpoint, repository string) (*registry.Repository, error) { + + transport := registry.GetHTTPTransport(insecure) + + authorizer := auth.NewStandardTokenAuthorizer(&http.Client{ + Transport: transport, + }, credential, tokenServiceEndpoint) + + uam := &userAgentModifier{ + userAgent: "harbor-registry-client", + } + + return registry.NewRepository(repository, endpoint, &http.Client{ + Transport: registry.NewTransport(transport, authorizer, uam), + }) +} + +// NewRepositoryClientForJobservice creates a repository client that can only be used to +// access the internal registry +func NewRepositoryClientForJobservice(repository, internalRegistryURL, secret, internalTokenServiceURL string) (*registry.Repository, error) { + transport := registry.GetHTTPTransport() + + credential := auth.NewCookieCredential(&http.Cookie{ + Name: models.UISecretCookie, + Value: secret, + }) + + authorizer := auth.NewStandardTokenAuthorizer(&http.Client{ + Transport: transport, + }, credential, internalTokenServiceURL) + + uam := &userAgentModifier{ + userAgent: "harbor-registry-client", + } + + return registry.NewRepository(repository, internalRegistryURL, &http.Client{ + Transport: registry.NewTransport(transport, authorizer, uam), + }) +} + +type userAgentModifier struct { + userAgent string +} + +// Modify adds user-agent header to the request +func (u *userAgentModifier) Modify(req *http.Request) error { + req.Header.Set(http.CanonicalHeaderKey("User-Agent"), u.userAgent) + return nil +} + +// BuildBlobURL ... +func BuildBlobURL(endpoint, repository, digest string) string { + return fmt.Sprintf("%s/v2/%s/blobs/%s", endpoint, repository, digest) +} + +//GetTokenForRepo is used for job handler to get a token for clair. +func GetTokenForRepo(repository, secret, internalTokenServiceURL string) (string, error) { + c := &http.Cookie{Name: models.UISecretCookie, Value: secret} + credentail := auth.NewCookieCredential(c) + t, err := auth.GetToken(internalTokenServiceURL, true, credentail, + []*token.ResourceActions{&token.ResourceActions{ + Type: "repository", + Name: repository, + Actions: []string{"pull"}, + }}) + if err != nil { + return "", err + } + + return t.Token, nil +} diff --git a/src/jobservice_v2/runtime/bootstrap.go b/src/jobservice_v2/runtime/bootstrap.go index 7fd8c04b3..edeb05985 100644 --- a/src/jobservice_v2/runtime/bootstrap.go +++ b/src/jobservice_v2/runtime/bootstrap.go @@ -10,12 +10,14 @@ import ( "syscall" "time" + "github.com/vmware/harbor/src/common/job" "github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/jobservice_v2/api" "github.com/vmware/harbor/src/jobservice_v2/config" "github.com/vmware/harbor/src/jobservice_v2/core" "github.com/vmware/harbor/src/jobservice_v2/env" "github.com/vmware/harbor/src/jobservice_v2/job/impl" + "github.com/vmware/harbor/src/jobservice_v2/job/impl/scan" "github.com/vmware/harbor/src/jobservice_v2/pool" ) @@ -141,6 +143,11 @@ func (bs *Bootstrap) loadAndRunRedisWorkerPool(ctx *env.Context, cfg config.Conf ctx.ErrorChan <- err return redisWorkerPool //avoid nil pointer issue } + if err := redisWorkerPool.RegisterJob(job.ImageScanJob, (*scan.ClairJob)(nil)); err != nil { + //exit + ctx.ErrorChan <- err + return redisWorkerPool //avoid nil pointer issue + } redisWorkerPool.Start() From 483496ed931f71e52052ac8cb8cba56d9dcefce4 Mon Sep 17 00:00:00 2001 From: Tan Jiang Date: Wed, 21 Mar 2018 16:32:21 +0800 Subject: [PATCH 20/21] remove swp file --- src/jobservice_v2/job/impl/scan/.job.go.swp | Bin 16384 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/jobservice_v2/job/impl/scan/.job.go.swp diff --git a/src/jobservice_v2/job/impl/scan/.job.go.swp b/src/jobservice_v2/job/impl/scan/.job.go.swp deleted file mode 100644 index 0551120f90b4dbc7c5493c9b9d6a348e557ccb7f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeHNTZklA89qrimyX5+19@;YoOIlzd(%_BJ6;lInZ&v5rIWcV-7~vvmNA~LI^A76 zRdp(-s%EA$ZdMX881yj_BZ3INC@SnzUi3`};+s!`AgC`IQHcmjAo%^Ks=8}uT4r_; z5vhi6rY`6F=l{R|Kezu>_r!(PGJncC!SH*4u~#0t{`?L*!`}HKV_K=09v}3(5>GjD z>!4DMy-Wg|eYG7mBW*YPZahf3maY8emcK2uYz~C(Ds5g~QQb)DE!UPWY)vFSSGAK?#vbiMepye=6i87LVj87LVj87LVj z87LVj8TdbEAP#S5*I?uAnVn(w|F%8<@5t_tX8&K@^FF&R|CJ1s43rF%43rF%43rF% z43rF%43rF%43rF%4Ezr=ARNZdLa#?D0f6)WeE{fzw#cm;SGco291_{~Qc`!(!~2@G77I0Xz>}0-gkp0KfV$V?P4E1S|s|2mbgW#(oI=0Qf!- z02hIEU=4T}_{Y7B{TcWZ@Gfv2_$sgq41pbB9{8Zb*n7ZVfxiGh1vY^tUW!vFbn+tPR4!-d;@qLcnvrU zoB@sl?;c_7Z6E?9Ab`&U8^EW5yMVtV$#Na|9`Ict0_wmgfTO_2fS3*g7lYwh>qx*@Wt&YbtJTqRXBP(lSwf>g0w+*0a?6 z6aje%qz3$Z3+VfvR3h|ERZ==r-7`XS6LK4J>N17NB}+tq&8wf<6J&X_t)QpC8x;)@ z6Wo+f+Y3cJperzyH|JUB@eo!ctoRFfPDaB`eVLg~u~ISaOLZ7=XsyWY^id6QX5!Q` zE;$`Ip+Y1=m~kb97rMy-P7bw1Sm%u&%DbtHMh?y-7_%evk)fxA)8Hc=+p_PXyA0Rc zOE)|XRpiD>4;zTJO&Q#j))bni)}a(0mn1Ti*!3a`#Wia7jM8lxsxhT>mn$~1p(AaK zSq1e?-JJMCsBU@}4-?O$5b;Ie_RzOE1%c}eIR98Qa|~mV=~bgv)Ax z1F<#o1!BO28BEIp`#dyR&a@cYZbV<+NC&sw$R0>vOk2y+amCs&lzD}#6Aa#`3gRkj zY!oRe^9bXyU8y>7!n30<^v3Fpixrrk98L#0=vKM}`UOUNa*-KF;FUu~TkgG)*%N1x zI(m89UCA@JvKD1eV60s-eYggSn@zs9y0|)H2I8!&8Hnw8=b3bZs@YfgaWe>jzdCOx zK@Bgt47MyhSWuXrb`Z7)Ok4;L7`e;PAS56lLr?k`uOl80Bp=OjxT0R&Y~_<)aZrHAQ;Di{wOAIs=t>PFrFus?~W{DOxMQ=ECz^Yx3m8=9a|{ z&7~3AO2A>-R{rqj8j8{R(&-mo<>;P?oyp66&)w8n**nbMCT7o>PhcO9c*_hj+xSK{ zm4l@qt5k30n_1I|t2b8}@pxbjEMt5XG>!NsqW$J7m4jhBrpne&-L9{kPZ<~0!M1oznAb;bP!3)bOkAk&&=RQV?Y#2 z+wHkF)+j+gA)$SRm8x0qgi`x%6d9=^a7fI>*4nw%^)-HRuHBwnS!IwN@4}Od=|wi`>CVB8s?~5<@H)9i!5SIxI@}DNSKtET9686<>9( z!&{vypP%crIt@~2J@81hmM2q?3AxZcR;y9dXHn+F8Edh=P(o(wLgfOBpYA&@FPOo%MPfg=1__WR?m0-#ra@CCm zL<_9oL)`UlpjyNvewY;vRLqlzoB<1aSdoJsTZS=>!L;D_ToKrkn+{D5DmpQ{F48q; z^vp{&_ywuSVH`U08r836m8$hqnbHf;QM>&CLdmZ4=@TcPkGJW4 z#IdHwu7&(X`~R!hTi=O2IPL%Q^ZoB&uYV1A9C!%04fp_i{WpQXVGsX1;BDY_;AP+? zU>!IGyboEw1%3g%2D}P*z%#%p;9=k>a5q5h9s$Z<$w0|K$w0|K$w0|K$w0|K$w0}# ze~E$dNk^V3rsR;xNv98R(u3WX44f>*kHj^y5vD+e^5Yu0C(8JvTv}n`aX^J~_Jwl- z{xm;_{vE)1|Etf$SHqkuFwktX&{N}Vjq3-Tvs_i4(X;MFPQ0aE&Gr2O0RzWlRbjBqjC$_X4|I6vh^q(gJCE9J}By zTo`j~T4NQan`b;s-;xX?kp~nubn4TU*zHeY3yocA#T)>njv@mK2Y9L9cdK9`kT#`8 zsx!Q5dn%IfT6Hdo2THpynWPEc+~C-G30rh}yc2KM5%z^+m()O$1D Date: Wed, 21 Mar 2018 16:59:32 +0800 Subject: [PATCH 21/21] Add missed package to fix compilation issue --- src/common/job/const.go | 6 ++++++ src/common/job/parms.go | 11 +++++++++++ 2 files changed, 17 insertions(+) create mode 100644 src/common/job/const.go create mode 100644 src/common/job/parms.go diff --git a/src/common/job/const.go b/src/common/job/const.go new file mode 100644 index 000000000..d04aea14c --- /dev/null +++ b/src/common/job/const.go @@ -0,0 +1,6 @@ +package job + +const ( + //ImageScanJob is name of scan job it will be used as key to register to job service. + ImageScanJob = "IMAGE_SCAN" +) diff --git a/src/common/job/parms.go b/src/common/job/parms.go new file mode 100644 index 000000000..22f75a81f --- /dev/null +++ b/src/common/job/parms.go @@ -0,0 +1,11 @@ +package job + +type ScanJobParms struct { + JobID int64 `json:"job_int_id"` + Repository string `json:"repository"` + Tag string `json:"tag"` + Secret string `json: "job_service_secret"` + RegistryURL string `json:"registry_url"` + ClairEndpoint string `json:"clair_endpoint"` + TokenEndpoint string `json:"token_endpoint"` +}