Commit 7ccdce33 authored by Wenkai Yin's avatar Wenkai Yin
Browse files

Refactor ping target API

Merge ping target API by ID into ping target API
parent 13ffebcf
Showing with 149 additions and 222 deletions
+149 -222
......@@ -1670,31 +1670,6 @@ paths:
description: Target not found.
'500':
description: Unexpected internal errors.
'/targets/{id}/ping':
post:
summary: Ping target.
description: |
This endpoint is for ping target.
parameters:
- name: id
in: path
type: integer
format: int64
required: true
description: The replication's target ID.
tags:
- Products
responses:
'200':
description: Ping replication's target successfully.
'400':
description: Can not ping target.
'401':
description: User need to log in first.
'404':
description: Target ID does not exist.
'500':
description: Unexpected internal errors.
'/targets/{id}':
put:
summary: Update replication's target.
......@@ -2512,6 +2487,10 @@ definitions:
PingTarget:
type: object
properties:
id:
type: integer
format: int
description: Target ID.
endpoint:
type: string
description: The target address URL string.
......
......@@ -118,7 +118,6 @@ func init() {
beego.Router("/api/targets/:id([0-9]+)", &TargetAPI{})
beego.Router("/api/targets/:id([0-9]+)/policies/", &TargetAPI{}, "get:ListPolicies")
beego.Router("/api/targets/ping", &TargetAPI{}, "post:Ping")
beego.Router("/api/targets/:id([0-9]+)/ping", &TargetAPI{}, "post:PingByID")
beego.Router("/api/policies/replication/:id([0-9]+)", &RepPolicyAPI{})
beego.Router("/api/policies/replication", &RepPolicyAPI{}, "get:List")
beego.Router("/api/policies/replication", &RepPolicyAPI{}, "post:Post;delete:Delete")
......@@ -636,18 +635,6 @@ func (a testapi) PingTarget(authInfo usrInfo, body interface{}) (int, error) {
return httpStatusCode, err
}
//PingTargetByID ...
func (a testapi) PingTargetByID(authInfo usrInfo, id int) (int, error) {
_sling := sling.New().Post(a.basePath)
path := fmt.Sprintf("/api/targets/%d/ping", id)
_sling = _sling.Path(path)
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, err
}
//Get target by targetID
func (a testapi) GetTargetByID(authInfo usrInfo, targetID string) (int, error) {
_sling := sling.New().Get(a.basePath)
......
......@@ -84,48 +84,57 @@ func (t *TargetAPI) ping(endpoint, username, password string, insecure bool) {
}
}
// PingByID ping target by ID
func (t *TargetAPI) PingByID() {
id := t.GetIDFromURL()
target, err := dao.GetRepTarget(id)
if err != nil {
log.Errorf("failed to get target %d: %v", id, err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if target == nil {
t.CustomAbort(http.StatusNotFound, fmt.Sprintf("target %d not found", id))
}
endpoint := target.URL
username := target.Username
password := target.Password
insecure := target.Insecure
if len(password) != 0 {
password, err = utils.ReversibleDecrypt(password, t.secretKey)
if err != nil {
log.Errorf("failed to decrypt password: %v", err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
}
t.ping(endpoint, username, password, insecure)
}
// Ping validates whether the target is reachable and whether the credential is valid
func (t *TargetAPI) Ping() {
req := struct {
Endpoint string `json:"endpoint"`
Username string `json:"username"`
Password string `json:"password"`
Insecure bool `json:"insecure"`
ID *int64 `json:"id"`
Endpoint *string `json:"endpoint"`
Username *string `json:"username"`
Password *string `json:"password"`
Insecure *bool `json:"insecure"`
}{}
t.DecodeJSONReq(&req)
if len(req.Endpoint) == 0 {
t.CustomAbort(http.StatusBadRequest, "endpoint is required")
target := &models.RepTarget{}
if req.ID != nil {
var err error
target, err = dao.GetRepTarget(*req.ID)
if err != nil {
t.HandleInternalServerError(fmt.Sprintf("failed to get target %d: %v", *req.ID, err))
return
}
if target == nil {
t.HandleNotFound(fmt.Sprintf("target %d not found", *req.ID))
return
}
if len(target.Password) != 0 {
target.Password, err = utils.ReversibleDecrypt(target.Password, t.secretKey)
if err != nil {
t.HandleInternalServerError(fmt.Sprintf("failed to decrypt password: %v", err))
return
}
}
}
if req.Endpoint != nil {
target.URL = *req.Endpoint
}
if req.Username != nil {
target.Username = *req.Username
}
if req.Password != nil {
target.Password = *req.Password
}
if req.Insecure != nil {
target.Insecure = *req.Insecure
}
if len(target.URL) == 0 {
t.HandleBadRequest("empty endpoint")
return
}
t.ping(req.Endpoint, req.Username, req.Password, req.Insecure)
t.ping(target.URL, target.Username, target.Password, target.Insecure)
}
// Get ...
......
......@@ -15,11 +15,13 @@ package api
import (
"fmt"
"net/http"
"os"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vmware/harbor/tests/apitests/apilib"
)
......@@ -110,72 +112,46 @@ func TestTargetsGet(t *testing.T) {
}
func TestTargetPing(t *testing.T) {
var httpStatusCode int
var err error
assert := assert.New(t)
apiTest := newHarborAPI()
fmt.Println("Testing Targets Ping Post API")
//case 1
body := struct {
// 404: not exist target
target01 := struct {
ID int64 `json:"id"`
}{
ID: 10000,
}
code, err := apiTest.PingTarget(*admin, target01)
require.Nil(t, err)
assert.Equal(t, http.StatusNotFound, code)
// 400: empty endpoint
target02 := struct {
Endpoint string `json:"endpoint"`
}{
Endpoint: "",
}
code, err = apiTest.PingTarget(*admin, target02)
require.Nil(t, err)
assert.Equal(t, http.StatusBadRequest, code)
// 200
target03 := struct {
ID int64 `json:"id"`
Endpoint string `json:"endpoint"`
Username string `json:"username"`
Password string `json:"password"`
Insecure bool `json:"insecure"`
}{
ID: int64(addTargetID),
Endpoint: os.Getenv("REGISTRY_URL"),
Username: adminName,
Password: adminPwd,
Insecure: true,
}
httpStatusCode, err = apiTest.PingTarget(*admin, body)
if err != nil {
t.Error("Error while ping target", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "")
}
//case 2
body.Endpoint = ""
httpStatusCode, err = apiTest.PingTarget(*admin, body)
if err != nil {
t.Error("Error while ping target", err.Error())
} else {
assert.Equal(int(400), httpStatusCode, "")
}
}
func TestTargetPingByID(t *testing.T) {
var httpStatusCode int
var err error
assert := assert.New(t)
apiTest := newHarborAPI()
fmt.Println("Testing Targets Ping Post API")
//-------------------case 1 : response code = 200------------------------//
fmt.Println("case 1 : response code = 200")
id := addTargetID
httpStatusCode, err = apiTest.PingTargetByID(*admin, id)
if err != nil {
t.Error("Error whihle ping target", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
}
//--------------case 2 : response code = 404,target not found------------//
fmt.Println("case 2 : response code = 404,target not found")
id = 1111
httpStatusCode, err = apiTest.PingTargetByID(*admin, id)
if err != nil {
t.Error("Error whihle ping target", err.Error())
t.Log(err)
} else {
assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404")
}
code, err = apiTest.PingTarget(*admin, target03)
require.Nil(t, err)
assert.Equal(t, http.StatusOK, code)
}
func TestTargetGetByID(t *testing.T) {
......
......@@ -114,7 +114,6 @@ func initRouters() {
beego.Router("/api/targets/:id([0-9]+)", &api.TargetAPI{})
beego.Router("/api/targets/:id([0-9]+)/policies/", &api.TargetAPI{}, "get:ListPolicies")
beego.Router("/api/targets/ping", &api.TargetAPI{}, "post:Ping")
beego.Router("/api/targets/:id([0-9]+)/ping", &api.TargetAPI{}, "post:PingByID")
beego.Router("/api/logs", &api.LogAPI{})
beego.Router("/api/configurations", &api.ConfigAPI{})
beego.Router("/api/configurations/reset", &api.ConfigAPI{}, "post:Reset")
......
......@@ -15,7 +15,7 @@ export const CREATE_EDIT_ENDPOINT_TEMPLATE: string = `
<div class="form-group">
<label for="destination_name" class="col-md-4 form-group-label-override required">{{ 'DESTINATION.NAME' | translate }}</label>
<label class="col-md-8" for="destination_name" aria-haspopup="true" role="tooltip" [class.invalid]="targetName.errors && (targetName.dirty || targetName.touched)" [class.valid]="targetName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-left">
<input type="text" id="destination_name" [disabled]="testOngoing" [readonly]="!editable" [(ngModel)]="target.name" name="targetName" size="20" #targetName="ngModel" required (keyup)="changedTargetName($event)">
<input type="text" id="destination_name" [disabled]="testOngoing" [readonly]="!editable" [(ngModel)]="target.name" name="targetName" size="20" #targetName="ngModel" required>
<span class="tooltip-content" *ngIf="targetName.errors && targetName.errors.required && (targetName.dirty || targetName.touched)">
{{ 'DESTINATION.NAME_IS_REQUIRED' | translate }}
</span>
......@@ -24,7 +24,7 @@ export const CREATE_EDIT_ENDPOINT_TEMPLATE: string = `
<div class="form-group">
<label for="destination_url" class="col-md-4 form-group-label-override required">{{ 'DESTINATION.URL' | translate }}</label>
<label class="col-md-8" for="destination_url" aria-haspopup="true" role="tooltip" [class.invalid]="targetEndpoint.errors && (targetEndpoint.dirty || targetEndpoint.touched)" [class.valid]="targetEndpoint.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-left">
<input type="text" id="destination_url" [disabled]="testOngoing" [readonly]="!editable" [(ngModel)]="target.endpoint" size="20" name="endpointUrl" #targetEndpoint="ngModel" required (keyup)="clearPassword($event)" placeholder="http(s)://192.168.1.1">
<input type="text" id="destination_url" [disabled]="testOngoing" [readonly]="!editable" [(ngModel)]="target.endpoint" size="20" name="endpointUrl" #targetEndpoint="ngModel" required placeholder="http(s)://192.168.1.1">
<span class="tooltip-content" *ngIf="targetEndpoint.errors && targetEndpoint.errors.required && (targetEndpoint.dirty || targetEndpoint.touched)">
{{ 'DESTINATION.URL_IS_REQUIRED' | translate }}
</span>
......@@ -32,15 +32,15 @@ export const CREATE_EDIT_ENDPOINT_TEMPLATE: string = `
</div>
<div class="form-group">
<label for="destination_username" class="col-md-4 form-group-label-override">{{ 'DESTINATION.USERNAME' | translate }}</label>
<input type="text" class="col-md-8" id="destination_username" [disabled]="testOngoing" [readonly]="!editable" [(ngModel)]="target.username" size="20" name="username" #username="ngModel" (keyup)="clearPassword($event)">
<input type="text" class="col-md-8" id="destination_username" [disabled]="testOngoing" [readonly]="!editable" [(ngModel)]="target.username" size="20" name="username" #username="ngModel">
</div>
<div class="form-group">
<label for="destination_password" class="col-md-4 form-group-label-override">{{ 'DESTINATION.PASSWORD' | translate }}</label>
<input type="password" class="col-md-8" id="destination_password" [disabled]="testOngoing" [readonly]="!editable" [(ngModel)]="target.password" size="20" name="password" #password="ngModel" (focus)="clearPassword($event)">
<input type="password" class="col-md-8" id="destination_password" [disabled]="testOngoing" [readonly]="!editable" [(ngModel)]="target.password" size="20" name="password" #password="ngModel">
</div>
<div class="form-group">
<label for="destination_insecure" class="col-md-4 form-group-label-override">{{'CONFIG.VERIFY_REMOTE_CERT' | translate }}</label>
<clr-checkbox #insecure class="col-md-8" name="insecure" id="destination_insecure" [clrDisabled]="testOngoing" [clrChecked]="!target.insecure" (clrCheckedChange)="setInsecureValue($event)">
<clr-checkbox #insecure class="col-md-8" name="insecure" id="destination_insecure" [clrDisabled]="testOngoing || !editable" [clrChecked]="!target.insecure" (clrCheckedChange)="setInsecureValue($event)">
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right" style="top:-7px;">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.VERIFY_REMOTE_CERT' | translate}}</span>
......@@ -55,8 +55,8 @@ export const CREATE_EDIT_ENDPOINT_TEMPLATE: string = `
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="testConnection()" [disabled]="testOngoing || onGoing || targetEndpoint.errors">{{ 'DESTINATION.TEST_CONNECTION' | translate }}</button>
<button type="button" class="btn btn-outline" (click)="onCancel()" [disabled]="testOngoing || onGoing">{{ 'BUTTON.CANCEL' | translate }}</button>
<button type="button" class="btn btn-outline" (click)="testConnection()" [disabled]="inProgress || targetEndpoint.errors">{{ 'DESTINATION.TEST_CONNECTION' | translate }}</button>
<button type="button" class="btn btn-outline" (click)="onCancel()" [disabled]="inProgress">{{ 'BUTTON.CANCEL' | translate }}</button>
<button type="submit" class="btn btn-primary" (click)="onSubmit()" [disabled]="!isValid">{{ 'BUTTON.OK' | translate }}</button>
</div>
</clr-modal>`;
\ No newline at end of file
......@@ -35,7 +35,7 @@ import { TranslateService } from '@ngx-translate/core';
import { CREATE_EDIT_ENDPOINT_STYLE } from './create-edit-endpoint.component.css';
import { CREATE_EDIT_ENDPOINT_TEMPLATE } from './create-edit-endpoint.component.html';
import { toPromise, clone, compareValue } from '../utils';
import { toPromise, clone, compareValue, isEmptyObject } from '../utils';
import { Subscription } from 'rxjs/Subscription';
......@@ -51,8 +51,6 @@ export class CreateEditEndpointComponent implements AfterViewChecked, OnDestroy
createEditDestinationOpened: boolean;
staticBackdrop: boolean = true;
closable: boolean = false;
actionType: ActionType;
editable: boolean;
target: Endpoint = this.initEndpoint();
......@@ -62,11 +60,9 @@ export class CreateEditEndpointComponent implements AfterViewChecked, OnDestroy
@ViewChild('targetForm')
currentForm: NgForm;
endpointHasChanged: boolean;
targetNameHasChanged: boolean;
testOngoing: boolean;
onGoing: boolean;
endpointId: number | string;
@ViewChild(InlineAlertComponent)
inlineAlert: InlineAlertComponent;
......@@ -84,41 +80,21 @@ export class CreateEditEndpointComponent implements AfterViewChecked, OnDestroy
private ref: ChangeDetectorRef
) { }
public get hasChanged(): boolean {
if (this.actionType === ActionType.ADD_NEW) {
//Create new
return this.target && (
(this.target.endpoint && this.target.endpoint.trim() !== "") ||
(this.target.name && this.target.name.trim() !== "") ||
(this.target.username && this.target.username.trim() !== "") ||
(this.target.password && this.target.password.trim() !== "")) ||
this.target.insecure;
} else {
//Edit
return !compareValue(this.target, this.initVal);
}
}
public get isValid(): boolean {
return !this.testOngoing &&
!this.onGoing &&
this.targetForm &&
this.targetForm.valid &&
this.editable &&
(this.targetNameHasChanged || this.endpointHasChanged || this.checkboxHasChanged);
!compareValue(this.target, this.initVal);
}
public get inProgress(): boolean {
return this.onGoing || this.testOngoing;
}
public get checkboxHasChanged(): boolean {
return (this.target.insecure !== this.initVal.insecure) ? true : false;
}
setInsecureValue($event: any) {
this.target.insecure = !$event;
this.endpointHasChanged = true;
}
ngOnDestroy(): void {
......@@ -148,8 +124,6 @@ export class CreateEditEndpointComponent implements AfterViewChecked, OnDestroy
reset(): void {
//Reset status variables
this.endpointHasChanged = false;
this.targetNameHasChanged = false;
this.testOngoing = false;
this.onGoing = false;
......@@ -157,6 +131,9 @@ export class CreateEditEndpointComponent implements AfterViewChecked, OnDestroy
this.target = this.initEndpoint();
this.initVal = this.initEndpoint();
this.formValues = null;
this.endpointId = '';
this.inlineAlert.close();
}
//Forcely refresh the view
......@@ -179,7 +156,7 @@ export class CreateEditEndpointComponent implements AfterViewChecked, OnDestroy
//reset
this.reset();
if (targetId) {
this.actionType = ActionType.EDIT;
this.endpointId = targetId;
this.translateService.get('DESTINATION.TITLE_EDIT').subscribe(res => this.modalTitle = res);
toPromise<Endpoint>(this.endpointService
.getEndpoint(targetId))
......@@ -197,7 +174,7 @@ export class CreateEditEndpointComponent implements AfterViewChecked, OnDestroy
})
.catch(error => this.errorHandler.error(error));
} else {
this.actionType = ActionType.ADD_NEW;
this.endpointId = '';
this.translateService.get('DESTINATION.TITLE_ADD').subscribe(res => this.modalTitle = res);
//Directly open the modal
this.open();
......@@ -206,14 +183,23 @@ export class CreateEditEndpointComponent implements AfterViewChecked, OnDestroy
testConnection() {
let payload: Endpoint = this.initEndpoint();
if (this.endpointHasChanged) {
if (!this.endpointId) {
payload.endpoint = this.target.endpoint;
payload.username = this.target.username;
payload.password = this.target.password;
payload.insecure = this.target.insecure;
} else {
}else {
let changes: {[key: string]: any} = this.getChanges();
for (let prop in payload) {
delete payload[prop];
}
payload.id = this.target.id;
if (!isEmptyObject(changes)) {
let changekeys: {[key: string]: any} = Object.keys(this.getChanges());
changekeys.forEach((key: string) => {
payload[key] = changes[key];
});
}
}
this.testOngoing = true;
......@@ -232,27 +218,11 @@ export class CreateEditEndpointComponent implements AfterViewChecked, OnDestroy
});
}
changedTargetName($event: any) {
if (this.editable) {
this.targetNameHasChanged = true;
}
}
clearPassword($event: any) {
if (this.editable) {
this.target.password = '';
this.endpointHasChanged = true;
}
}
onSubmit() {
switch (this.actionType) {
case ActionType.ADD_NEW:
this.addEndpoint();
break;
case ActionType.EDIT:
this.updateEndpoint();
break;
if (this.endpointId) {
this.updateEndpoint();
} else {
this.addEndpoint();
}
}
......@@ -286,27 +256,19 @@ export class CreateEditEndpointComponent implements AfterViewChecked, OnDestroy
if (this.onGoing) {
return;//Avoid duplicated submitting
}
if (!(this.targetNameHasChanged || this.endpointHasChanged || this.checkboxHasChanged)) {
return;//Avoid invalid submitting
}
let payload: Endpoint = this.initEndpoint();
if (this.targetNameHasChanged) {
payload.name = this.target.name;
}else {
delete payload.name;
for (let prop in payload) {
delete payload[prop];
}
if (this.endpointHasChanged) {
payload.endpoint = this.target.endpoint;
payload.username = this.target.username;
payload.password = this.target.password;
}else {
delete payload.endpoint;
}
if (this.checkboxHasChanged) {
payload.insecure = this.target.insecure;
}else {
delete payload.insecure;
let changes: {[key: string]: any} = this.getChanges();
let changekeys: {[key: string]: any} = Object.keys(this.getChanges());
if (isEmptyObject(changes)) {
return;
}
changekeys.forEach((key: string) => {
payload[key] = changes[key];
});
if (!this.target.id) { return; }
......@@ -346,9 +308,10 @@ export class CreateEditEndpointComponent implements AfterViewChecked, OnDestroy
}
onCancel() {
if (this.hasChanged) {
let changes: {[key: string]: any} = this.getChanges();
if (!isEmptyObject(changes)) {
this.inlineAlert.showInlineConfirmation({ message: 'ALERT.FORM_CHANGE_CONFIRMATION' });
} else {
}else {
this.close();
if (this.targetForm) {
this.targetForm.reset();
......@@ -388,5 +351,28 @@ export class CreateEditEndpointComponent implements AfterViewChecked, OnDestroy
}
}
}
getChanges(): { [key: string]: any | any[] } {
let changes: { [key: string]: any | any[] } = {};
if (!this.target || !this.initVal) {
return changes;
}
for (let prop in this.target) {
let field: any = this.initVal[prop];
if (!compareValue(field, this.target[prop])) {
changes[prop] = this.target[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
}
......@@ -185,22 +185,12 @@ export class EndpointDefaultService extends EndpointService {
if(!endpoint) {
return Promise.reject('Invalid endpoint.');
}
let requestUrl: string ;
if(endpoint.id) {
requestUrl = `${this._endpointUrl}/${endpoint.id}/ping`;
return this.http
.post(requestUrl, HTTP_JSON_OPTIONS)
.toPromise()
.then(response=>response.status)
.catch(error=>Promise.reject(error));
} else {
requestUrl = `${this._endpointUrl}/ping`;
let requestUrl: string = `${this._endpointUrl}/ping`;
return this.http
.post(requestUrl, endpoint, HTTP_JSON_OPTIONS)
.toPromise()
.then(response=>response.status)
.catch(error=>Promise.reject(error));
}
}
public getEndpointWithReplicationRules(endpointId: number | string): Observable<any> | Promise<any> | any {
......
......@@ -75,6 +75,7 @@ export interface Endpoint extends Base {
password?: string;
insecure: boolean;
type: number;
[key: string]: any;
}
/**
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment