Merge branch 'job-service' into new-ui-with-sync-image

This commit is contained in:
kunw 2016-06-14 11:22:03 +08:00
commit 91eacb895c
52 changed files with 1834 additions and 1252 deletions

View File

@ -1,3 +1,5 @@
sudo: true
language: go
go:
@ -5,10 +7,32 @@ go:
go_import_path: github.com/vmware/harbor
#service:
# - mysql
services:
- docker
- mysql
env: DB_HOST=127.0.0.1 DB_PORT=3306 DB_USR=root DB_PWD=
dist: trusty
addons:
apt:
packages:
- mysql-server-5.6
- mysql-client-core-5.6
- mysql-client-5.6
env:
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_USR: root
DB_PWD:
DOCKER_COMPOSE_VERSION: 1.7.1
HARBOR_ADMIN: admin
HARBOR_ADMIN_PASSWD: Harbor12345
before_install:
- ./tests/hostcfg.sh
- cd Deploy
- ./prepare
- cd ..
install:
- sudo apt-get update && sudo apt-get install -y libldap2-dev
@ -29,12 +53,29 @@ install:
- go get -d github.com/go-sql-driver/mysql
- go get github.com/golang/lint/golint
- go get github.com/GeertJohan/fgt
- sudo apt-get install -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" docker-engine=1.11.1-0~trusty
- sudo rm /usr/local/bin/docker-compose
- curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose
- chmod +x docker-compose
- sudo mv docker-compose /usr/local/bin
- go get github.com/dghubble/sling
- go get github.com/stretchr/testify
before_script:
# create tables and load data
- mysql < ./Deploy/db/registry.sql -uroot --verbose
script:
- go list ./... | grep -v /vendor/ | xargs -L1 fgt golint
- go list ./... | grep -v 'vendor' | xargs -L1 go vet
- go list ./... | grep -v 'vendor' | xargs -L1 go test -v
- go list ./... | grep -v 'tests' | grep -v /vendor/ | xargs -L1 fgt golint
- go list ./... | grep -v 'tests' | grep -v 'vendor' | xargs -L1 go vet
- go list ./... | grep -v 'tests' | grep -v 'vendor' | xargs -L1 go test -v
- docker-compose -f Deploy/docker-compose.yml up -d
- docker ps
- go run tests/startuptest.go http://localhost/
- go run tests/userlogintest.go -name ${HARBOR_ADMIN} -passwd ${HARBOR_ADMIN_PASSWD}
# test for API
- go test -v ./tests/apitests

View File

@ -33,7 +33,7 @@ type ProjectMemberAPI struct {
}
type memberReq struct {
Username string `json:"user_name"`
Username string `json:"username"`
UserID int `json:"user_id"`
Roles []int `json:"roles"`
}
@ -104,7 +104,7 @@ func (pma *ProjectMemberAPI) Get() {
log.Errorf("Error occurred in GetUser, error: %v", err)
pma.CustomAbort(http.StatusInternalServerError, "Internal error.")
}
result["user_name"] = user.Username
result["username"] = user.Username
result["user_id"] = pma.memberID
result["roles"] = roleList
pma.Data["json"] = result

View File

@ -14,8 +14,6 @@ import (
// RepPolicyAPI handles /api/replicationPolicies /api/replicationPolicies/:id/enablement
type RepPolicyAPI struct {
BaseAPI
policyID int64
policy *models.RepPolicy
}
// Prepare validates whether the user has system admin role
@ -214,11 +212,11 @@ func (pa *RepPolicyAPI) UpdateEnablement() {
return
}
if pa.policy.Enabled == e.Enabled {
if policy.Enabled == e.Enabled {
return
}
if err := dao.UpdateRepPolicyEnablement(pa.policyID, e.Enabled); err != nil {
if err := dao.UpdateRepPolicyEnablement(id, e.Enabled); err != nil {
log.Errorf("Failed to update policy enablement in DB, error: %v", err)
pa.RenderError(http.StatusInternalServerError, "Internal Error")
return
@ -226,18 +224,18 @@ func (pa *RepPolicyAPI) UpdateEnablement() {
if e.Enabled == 1 {
go func() {
if err := TriggerReplication(pa.policyID, "", nil, models.RepOpTransfer); err != nil {
log.Errorf("failed to trigger replication of %d: %v", pa.policyID, err)
if err := TriggerReplication(id, "", nil, models.RepOpTransfer); err != nil {
log.Errorf("failed to trigger replication of %d: %v", id, err)
} else {
log.Infof("replication of %d triggered", pa.policyID)
log.Infof("replication of %d triggered", id)
}
}()
} else {
go func() {
if err := postReplicationAction(pa.policyID, "stop"); err != nil {
log.Errorf("failed to stop replication of %d: %v", pa.policyID, err)
if err := postReplicationAction(id, "stop"); err != nil {
log.Errorf("failed to stop replication of %d: %v", id, err)
} else {
log.Infof("try to stop replication of %d", pa.policyID)
log.Infof("try to stop replication of %d", id)
}
}()
}

View File

@ -4,7 +4,7 @@ swagger: '2.0'
info:
title: Harbor API
description: These APIs provide services for manipulating Harbor project.
version: "0.1.0"
version: "0.1.1"
# the domain of the service
host: localhost
# array of all schemes that your API supports
@ -167,7 +167,7 @@ paths:
- name: access_log
in: body
schema:
$ref: '#/definitions/AccessLog'
$ref: '#/definitions/AccessLogFilter'
description: Search results of access logs.
tags:
- Products
@ -204,7 +204,7 @@ paths:
schema:
type: array
items:
$ref: '#/definitions/Role'
$ref: '#/definitions/User'
400:
description: Illegal format of provided ID value.
401:
@ -567,7 +567,7 @@ paths:
schema:
type: array
items:
$ref: '#/definitions/Repository'
type: string
400:
description: Invalid project ID.
403:
@ -719,12 +719,41 @@ definitions:
description: Search results of the projects that matched the filter keywords.
type: array
items:
$ref: '#/definitions/Project'
$ref: '#/definitions/SearchProject'
repositories:
description: Search results of the repositories that matched the filter keywords.
type: array
items:
$ref: '#/definitions/Repository'
$ref: '#/definitions/SearchRepository'
SearchProject:
type: object
properties:
id:
type: integer
format: int64
description: The ID of project
name:
type: string
description: The name of the project
public:
type: integer
format: int
description: The flag to indicate the publicity of the project (1 is public, 0 is non-public)
SearchRepository:
type: object
properties:
repository_name:
type: string
description: The name of the repository
project_name:
type: string
description: The name of the project that the repository belongs to
project_id:
type: integer
description: The ID of the project that the repository belongs to
project_public:
type: integer
description: The flag to indicate the publicity of the project that the repository belongs to (1 is public, 0 is not)
Project:
type: object
properties:
@ -736,30 +765,39 @@ definitions:
type: integer
format: int32
description: The owner ID of the project always means the creator of the project.
project_name:
name:
type: string
description: The name of the project.
creation_time:
type: string
description: The creation time of the project.
update_time:
type: string
description: The update time of the project.
deleted:
type: integer
format: int32
description: A deletion mark of the project.
description: A deletion mark of the project (1 means it's deleted, 0 is not)
user_id:
type: integer
format: int32
description: A relation field to the user table.
owner_name:
type: string
description: The owner name of tthe project always means the creator of the project.
description: The owner name of the project.
public:
type: boolean
format: boolean
description: The public status of the project.
togglable:
type: boolean
description: Correspond to the UI about showing the public status of the project.
description: Correspond to the UI about whether the project's publicity is updatable (for UI)
current_user_role_id:
type: integer
description: The role ID of the current user who triggered the API (for UI)
repo_count:
type: integer
description: The number of the repositories under this project.
Repository:
type: object
properties:
@ -794,6 +832,7 @@ definitions:
user_id:
type: integer
format: int32
description: The ID of the user.
username:
type: string
email:
@ -816,7 +855,7 @@ definitions:
new_password:
type: string
description: New password for marking as to be updated.
AccessLog:
AccessLogFilter:
type: object
properties:
username:
@ -825,14 +864,32 @@ definitions:
keywords:
type: string
description: Operation name specified when project created.
beginTimestamp:
begin_timestamp:
type: integer
format: int32
format: int64
description: Begin timestamp for querying access logs.
endTimestamp:
end_timestamp:
type: integer
format: int32
format: int64
description: End timestamp for querying accessl logs.
AccessLog:
type: object
properties:
log_id:
type: integer
description: The ID of the log entry.
repo_name:
type: string
description: Name of the repository in this log entry.
repo_tag:
type: string
description: Tag of the repository in this log entry.
operation:
type: string
description: The operation against the repository in this log entry.
op_time:
type: time
description: The time when this operation is triggered.
Role:
type: object
properties:
@ -855,7 +912,7 @@ definitions:
type: integer
format: int32
description: Role ID for updating project role member.
user_name:
username:
type: string
description: Username relevant to a project role member.
TopRepo:

View File

@ -21,19 +21,18 @@ import (
// AccessLog holds information about logs which are used to record the actions that user take to the resourses.
type AccessLog struct {
LogID int `orm:"pk;column(log_id)" json:"LogId"`
UserID int `orm:"column(user_id)" json:"UserId"`
ProjectID int64 `orm:"column(project_id)" json:"ProjectId"`
RepoName string `orm:"column(repo_name)"`
RepoTag string `orm:"column(repo_tag)"`
GUID string `orm:"column(GUID)" json:"Guid"`
Operation string `orm:"column(operation)"`
OpTime time.Time `orm:"column(op_time)"`
Username string
Keywords string
LogID int `orm:"column(log_id)" json:"log_id"`
UserID int `orm:"column(user_id)" json:"user_id"`
ProjectID int64 `orm:"column(project_id)" json:"project_id"`
RepoName string `orm:"column(repo_name)" json:"repo_name"`
RepoTag string `orm:"column(repo_tag)" json:"repo_tag"`
GUID string `orm:"column(GUID)" json:"guid"`
Operation string `orm:"column(operation)" json:"operation"`
OpTime time.Time `orm:"column(op_time)" json:"op_time"`
Username string `json:"username"`
Keywords string `json:"keywords"`
BeginTime time.Time
BeginTimestamp int64
BeginTimestamp int64 `json:"begin_timestamp"`
EndTime time.Time
EndTimestamp int64
EndTimestamp int64 `json:"end_timestamp"`
}

View File

@ -21,19 +21,19 @@ import (
// Project holds the details of a project.
type Project struct {
ProjectID int64 `orm:"pk;column(project_id)" json:"ProjectId"`
OwnerID int `orm:"column(owner_id)" json:"OwnerId"`
Name string `orm:"column(name)"`
CreationTime time.Time `orm:"column(creation_time)"`
CreationTimeStr string
Deleted int `orm:"column(deleted)"`
UserID int `json:"UserId"`
OwnerName string
Public int `orm:"column(public)"`
ProjectID int64 `orm:"column(project_id)" json:"project_id"`
OwnerID int `orm:"column(owner_id)" json:"owner_id"`
Name string `orm:"column(name)" json:"name"`
CreationTime time.Time `orm:"column(creation_time)" json:"creation_time"`
CreationTimeStr string `json:"creation_time_str"`
Deleted int `orm:"column(deleted)" json:"deleted"`
//UserID int `json:"UserId"`
OwnerName string `json:"owner_name"`
Public int `orm:"column(public)" json:"public"`
//This field does not have correspondent column in DB, this is just for UI to disable button
Togglable bool
UpdateTime time.Time `orm:"update_time" json:"update_time"`
Role int `json:"role_id"`
Role int `json:"current_user_role_id"`
RepoCount int `json:"repo_count"`
}

View File

@ -21,20 +21,19 @@ import (
// User holds the details of a user.
type User struct {
UserID int `orm:"pk;column(user_id)" json:"UserId"`
UserID int `orm:"column(user_id)" json:"user_id"`
Username string `orm:"column(username)" json:"username"`
Email string `orm:"column(email)" json:"email"`
Password string `orm:"column(password)" json:"password"`
Realname string `orm:"column(realname)" json:"realname"`
Comment string `orm:"column(comment)" json:"comment"`
Deleted int `orm:"column(deleted)"`
Rolename string
RoleID int `json:"RoleId"`
RoleList []*Role `orm:"rel(m2m)"`
HasAdminRole int `orm:"column(sysadmin_flag)"`
ResetUUID string `orm:"column(reset_uuid)" json:"ResetUuid"`
Deleted int `orm:"column(deleted)" json:"deleted"`
Rolename string `json:"role_name"`
RoleID int `json:"role_id"`
// RoleList []Role `json:"role_list"`
HasAdminRole int `orm:"column(sysadmin_flag)" json:"has_admin_role"`
ResetUUID string `orm:"column(reset_uuid)" json:"reset_uuid"`
Salt string `orm:"column(salt)"`
CreationTime time.Time `orm:"creation_time" json:"creation_time"`
UpdateTime time.Time `orm:"update_time" json:"update_time"`
}

View File

@ -13,7 +13,6 @@
limitations under the License.
*/
.footer {
margin-top: 60px;
width: 100%;
/* Set the fixed height of the footer here */
height: 60px;

View File

@ -12,6 +12,7 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
var AjaxUtil = function(params){
this.url = params.url;
@ -154,8 +155,13 @@ jQuery(function(){
}
if(settings.callback != null){
$("#dlgConfirm").on("click", function(){
var hasEntered = false;
$("#dlgConfirm").on("click", function(e){
if(!hasEntered) {
hasEntered = true;
settings.callback();
}
});
}
$(self).modal('show');

View File

@ -105,7 +105,7 @@ jQuery(function(){
$('#accordionRepo').on('show.bs.collapse', function (e) {
$('#accordionRepo .in').collapse('hide');
var targetId = $(e.target).attr("targetId");
var repoName = targetId.replace(/[-]{6}/g, "/").replace(/[-]{3}/g, '.');
var repoName = targetId.replace(/[-]{6}/g, "/").replace(/[-]{3}/g, ".");
new AjaxUtil({
url: "/api/repositories/tags?repo_name=" + repoName,
type: "get",
@ -113,7 +113,7 @@ jQuery(function(){
$('#' + targetId +' table tbody tr').remove();
var row = [];
for(var i in data){
var tagName = data[i];
var tagName = data[i]
row.push('<tr><td><a href="#" imageId="' + tagName + '" repoName="' + repoName + '">' + tagName + '</a></td><td><input type="text" style="width:100%" readonly value=" docker pull '+ $("#harborRegUrl").val() +'/'+ repoName + ':' + tagName +'"></td></tr>');
}
$('#' + targetId +' table tbody').append(row.join(""));
@ -131,7 +131,6 @@ jQuery(function(){
}
}
data.Created = moment(new Date(data.Created)).format("YYYY-MM-DD HH:mm:ss");
$("#dlgModal").dialogModal({"title": i18n.getMessage("image_details"), "content": data});
}
}
@ -204,7 +203,7 @@ jQuery(function(){
if(operationType == "add"){
ajaxOpts.url = "/api/projects/" + projectId + "/members/";
ajaxOpts.type = "post";
ajaxOpts.data = {"roles" : checkedRoleItemList, "user_name": username};
ajaxOpts.data = {"roles" : checkedRoleItemList, "username": username};
}else if(operationType == "edit"){
ajaxOpts.url = "/api/projects/" + projectId + "/members/" + userId;
ajaxOpts.type = "put";
@ -241,24 +240,15 @@ jQuery(function(){
var ownerId = $("#ownerId").val();
$("#tblUser tbody tr").remove();
for(var i = 0; i < userList.length; ){
for(var i = 0; i < userList.length; i++){
var userId = userList[i].UserId;
var roleId = userList[i].RoleId;
var userId = userList[i].user_id;
var roleId = userList[i].role_id;
var username = userList[i].username;
var roleNameList = [];
for(var j = i; j < userList.length; i++, j++){
if(userList[j].UserId == userId){
roleNameList.push(name_mapping[userList[j].Rolename]);
}else{
break;
}
}
var row = '<tr>' +
'<td>' + username + '</td>' +
'<td>' + roleNameList.join(",") + '</td>' +
'<td>' + name_mapping[userList[i].role_name] + '</td>' +
'<td>';
var isShowOperations = true;
if(loginedUserRoleId >= 3 /*role: developer guest*/){
@ -284,11 +274,11 @@ jQuery(function(){
$.each(LogList || [], function(i, e){
$("#tabOperationLog tbody").append(
'<tr>' +
'<td>' + e.Username + '</td>' +
'<td>' + e.RepoName + '</td>' +
'<td>' + e.RepoTag + '</td>' +
'<td>' + e.Operation + '</td>' +
'<td>' + moment(new Date(e.OpTime)).format("YYYY-MM-DD HH:mm:ss") + '</td>' +
'<td>' + e.username + '</td>' +
'<td>' + e.repo_name + '</td>' +
'<td>' + e.repo_tag + '</td>' +
'<td>' + e.operation + '</td>' +
'<td>' + moment(new Date(e.op_time)).format("YYYY-MM-DD HH:mm:ss") + '</td>' +
'</tr>');
});
}
@ -302,7 +292,7 @@ jQuery(function(){
$("#operationType").val("edit");
$("#editUserId").val(user.user_id);
$("#spnSearch").hide();
$("#txtUserName").val(user.user_name);
$("#txtUserName").val(user.username);
$("#txtUserName").prop("disabled", true);
$("#btnSave").removeClass("disabled");
$("#dlgUserTitle").text(i18n.getMessage("edit_members"));
@ -404,7 +394,7 @@ jQuery(function(){
new AjaxUtil({
url: "/api/projects/" + projectId + "/logs/filter",
data:{"username":username, "project_id" : projectId, "keywords" : getKeyWords() , "beginTimestamp" : beginTimestamp, "endTimestamp" : endTimestamp},
data:{"username":username, "project_id" : Number(projectId), "keywords" : getKeyWords() , "begin_timestamp" : beginTimestamp, "end_timestamp" : endTimestamp},
type: "post",
success: function(data, status, xhr){
if(xhr && xhr.status == 200){
@ -464,9 +454,9 @@ jQuery(function(){
showClear: true
});
});
}
}
$(document).on("keydown", function(e){
$(document).on("keydown", function(e){
if(e.keyCode == 13){
e.preventDefault();
if($("#tabItemDetail li:eq(0)").is(":focus") || $("#txtRepoName").is(":focus")){
@ -479,5 +469,5 @@ jQuery(function(){
$("#btnSave").trigger("click");
}
}
});
});
})

View File

@ -19,7 +19,7 @@ jQuery(function(){
type: "get",
success: function(data, status, xhr){
if(xhr && xhr.status == 200){
if(data.HasAdminRole == 1) {
if(data.has_admin_role == 1) {
renderForAdminRole();
}
renderForAnyRole();
@ -55,16 +55,16 @@ jQuery(function(){
$("#tblProject tbody tr").remove();
$.each(data || [], function(i, e){
var row = '<tr>' +
'<td style="vertical-align: middle;"><a href="/registry/detail?project_id=' + e.ProjectId + '">' + e.Name + '</a></td>' +
'<td style="vertical-align: middle;">' + moment(new Date(e.CreationTime)).format("YYYY-MM-DD HH:mm:ss") + '</td>';
if(e.Public == 1 && e.Togglable){
row += '<td><button type="button" class="btn btn-success" projectid="' + e.ProjectId + '">' + i18n.getMessage("button_on")+ '</button></td>'
} else if (e.Public == 1) {
row += '<td><button type="button" class="btn btn-success" projectid="' + e.ProjectId + '" disabled>' + i18n.getMessage("button_on")+ '</button></td>';
} else if (e.Public == 0 && e.Togglable) {
row += '<td><button type="button" class="btn btn-danger" projectid="' + e.ProjectId + '">' + i18n.getMessage("button_off")+ '</button></td>';
} else if (e.Public == 0) {
row += '<td><button type="button" class="btn btn-danger" projectid="' + e.ProjectId + '" disabled>' + i18n.getMessage("button_off")+ '</button></td>';
'<td style="vertical-align: middle;"><a href="/registry/detail?project_id=' + e.project_id + '">' + e.name + '</a></td>' +
'<td style="vertical-align: middle;">' + moment(new Date(e.creation_time)).format("YYYY-MM-DD HH:mm:ss") + '</td>';
if(e.public == 1 && e.Togglable){
row += '<td><button type="button" class="btn btn-success" projectid="' + e.project_id + '">' + i18n.getMessage("button_on")+ '</button></td>'
} else if (e.public == 1) {
row += '<td><button type="button" class="btn btn-success" projectid="' + e.project_id + '" disabled>' + i18n.getMessage("button_on")+ '</button></td>';
} else if (e.public == 0 && e.Togglable) {
row += '<td><button type="button" class="btn btn-danger" projectid="' + e.project_id + '">' + i18n.getMessage("button_off")+ '</button></td>';
} else if (e.public == 0) {
row += '<td><button type="button" class="btn btn-danger" projectid="' + e.project_id + '" disabled>' + i18n.getMessage("button_off")+ '</button></td>';
row += '</tr>';
}
$("#tblProject tbody").append(row);
@ -163,12 +163,12 @@ jQuery(function(){
var row = '<tr>' +
'<td style="vertical-align: middle;">' + e.username + '</td>' +
'<td style="vertical-align: middle;">' + e.email + '</td>';
if(e.HasAdminRole == 1){
row += '<td style="padding-left: 30px;"><button type="button" class="btn btn-success" userid="' + e.UserId + '">' + i18n.getMessage("button_on") + '</button></td>';
if(e.has_admin_role == 1){
row += '<td style="padding-left: 30px;"><button type="button" class="btn btn-success" userid="' + e.user_id + '">' + i18n.getMessage("button_on") + '</button></td>';
} else {
row += '<td style="padding-left: 30px;"><button type="button" class="btn btn-danger" userid="' + e.UserId + '">' + i18n.getMessage("button_off") + '</button></td>';
row += '<td style="padding-left: 30px;"><button type="button" class="btn btn-danger" userid="' + e.user_id + '">' + i18n.getMessage("button_off") + '</button></td>';
}
row += '<td style="padding-left: 30px; vertical-align: middle;"><a href="#" style="visibility: hidden;" class="tdDeleteUser" userid="' + e.UserId + '" username="' + e.Username + '"><span class="glyphicon glyphicon-trash"></span></a></td>';
row += '<td style="padding-left: 30px; vertical-align: middle;"><a href="#" style="visibility: hidden;" class="tdDeleteUser" userid="' + e.user_id + '" username="' + e.username + '"><span class="glyphicon glyphicon-trash"></span></a></td>';
row += '</tr>';
$("#tblUser tbody").append(row);
});
@ -194,12 +194,12 @@ jQuery(function(){
}
}).exec();
});
$("#tblUser tbody tr").on("mouseover", function(){
$("#tblUser tbody tr").on("mouseover", function(e){
$(".tdDeleteUser", this).css({"visibility":"visible"});
}).on("mouseout", function(){
}).on("mouseout", function(e){
$(".tdDeleteUser", this).css({"visibility":"hidden"});
});
$("#tblUser tbody tr .tdDeleteUser").on("click", function(){
$("#tblUser tbody tr .tdDeleteUser").on("click", function(e){
var userId = $(this).attr("userid");
$("#dlgModal")
.dialogModal({
@ -219,6 +219,7 @@ jQuery(function(){
}).exec();
}
});
});
});
}
@ -231,4 +232,4 @@ jQuery(function(){
listUserAdminRole(username);
});
}
})
})

View File

@ -30,7 +30,6 @@ jQuery(function(){
}
bindEnterKey();
var spinner = new Spinner({scale:1}).spin();
$("#btnSubmit").on("click", function(){

View File

@ -0,0 +1,23 @@
/*
Copyright (c) 2016 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 HarborAPI
type AccessLog struct {
Username string `json:"username,omitempty"`
Keywords string `json:"keywords,omitempty"`
BeginTimestamp int32 `json:"beginTimestamp,omitempty"`
EndTimestamp int32 `json:"endTimestamp,omitempty"`
}

View File

@ -0,0 +1,113 @@
//Package HarborAPI
//These APIs provide services for manipulating Harbor project.
package HarborAPI
import (
"encoding/json"
//"fmt"
"io/ioutil"
"net/http"
"github.com/dghubble/sling"
)
type HarborAPI struct {
basePath string
}
func NewHarborAPI() *HarborAPI {
return &HarborAPI{
basePath: "http://localhost",
}
}
func NewHarborAPIWithBasePath(basePath string) *HarborAPI {
return &HarborAPI{
basePath: basePath,
}
}
type UsrInfo struct {
Name string
Passwd string
}
//Search for projects and repositories
//Implementation Notes
//The Search endpoint returns information about the projects and repositories
//offered at public status or related to the current logged in user.
//The response includes the project and repository list in a proper display order.
//@param q Search parameter for project and repository name.
//@return []Search
//func (a HarborAPI) SearchGet (q string) (Search, error) {
func (a HarborAPI) SearchGet(q string) (Search, error) {
_sling := sling.New().Get(a.basePath)
// create path and map variables
path := "/api/search"
_sling = _sling.Path(path)
type QueryParams struct {
Query string `url:"q"`
}
_sling = _sling.QueryStruct(&QueryParams{q})
// accept header
accepts := []string{"application/json", "text/plain"}
for key := range accepts {
_sling = _sling.Set("Accept", accepts[key])
break // only use the first Accept
}
req, err := _sling.Request()
client := &http.Client{}
httpResponse, err := client.Do(req)
defer httpResponse.Body.Close()
body, err := ioutil.ReadAll(httpResponse.Body)
if err != nil {
// handle error
}
var successPayload = new(Search)
err = json.Unmarshal(body, &successPayload)
return *successPayload, err
}
//Create a new project.
//Implementation Notes
//This endpoint is for user to create a new project.
//@param project New created project.
//@return void
//func (a HarborAPI) ProjectsPost (prjUsr UsrInfo, project Project) (int, error) {
func (a HarborAPI) ProjectsPost(prjUsr UsrInfo, project Project) (int, error) {
_sling := sling.New().Post(a.basePath)
// create path and map variables
path := "/api/projects"
_sling = _sling.Path(path)
// accept header
accepts := []string{"application/json", "text/plain"}
for key := range accepts {
_sling = _sling.Set("Accept", accepts[key])
break // only use the first Accept
}
// body params
_sling = _sling.BodyJSON(project)
req, err := _sling.Request()
req.SetBasicAuth(prjUsr.Name, prjUsr.Passwd)
client := &http.Client{}
httpResponse, err := client.Do(req)
defer httpResponse.Body.Close()
return httpResponse.StatusCode, err
}

View File

@ -0,0 +1,15 @@
// HarborLogout.go
package HarborAPI
import (
"net/http"
)
func (a HarborAPI) HarborLogout() (int, error) {
response, err := http.Get(a.basePath + "/logout")
defer response.Body.Close()
return response.StatusCode, err
}

View File

@ -0,0 +1,28 @@
// HarborLogon.go
package HarborAPI
import (
"io/ioutil"
"net/http"
"net/url"
"strings"
)
func (a HarborAPI) HarborLogin(user UsrInfo) (int, error) {
v := url.Values{}
v.Set("principal", user.Name)
v.Set("password", user.Passwd)
body := ioutil.NopCloser(strings.NewReader(v.Encode())) //endode v:[body struce]
client := &http.Client{}
reqest, err := http.NewRequest("POST", a.basePath+"/login", body)
reqest.Header.Set("Content-Type", "application/x-www-form-urlencoded;param=value") //setting post head
resp, err := client.Do(reqest)
defer resp.Body.Close() //close resp.Body
return resp.StatusCode, err
}

View File

@ -0,0 +1,15 @@
package HarborAPI
import ()
type Project struct {
ProjectId int32 `json:"id,omitempty"`
OwnerId int32 `json:"owner_id,omitempty"`
ProjectName string `json:"project_name,omitempty"`
CreationTime string `json:"creation_time,omitempty"`
Deleted int32 `json:"deleted,omitempty"`
UserId int32 `json:"user_id,omitempty"`
OwnerName string `json:"owner_name,omitempty"`
Public bool `json:"public,omitempty"`
Togglable bool `json:"togglable,omitempty"`
}

View File

@ -0,0 +1,9 @@
package HarborAPI
import ()
type Project4Search struct {
ProjectId int32 `json:"id,omitempty"`
ProjectName string `json:"name,omitempty"`
Public int32 `json:"public,omitempty"`
}

View File

@ -0,0 +1,16 @@
package HarborAPI
import (
"time"
)
type Repository struct {
Id string `json:"id,omitempty"`
Parent string `json:"parent,omitempty"`
Created time.Time `json:"created,omitempty"`
DurationDays string `json:"duration_days,omitempty"`
Author string `json:"author,omitempty"`
Architecture string `json:"architecture,omitempty"`
DockerVersion string `json:"docker_version,omitempty"`
Os string `json:"os,omitempty"`
}

View File

@ -0,0 +1,9 @@
package HarborAPI
type Repository4Search struct {
ProjectId int32 `json:"project_id,omitempty"`
ProjectName string `json:"project_name,omitempty"`
ProjectPublic int32 `json:"project_public,omitempty"`
RepoName string `json:"repository_name,omitempty"`
}

View File

@ -0,0 +1,7 @@
package HarborAPI
type Role struct {
RoleId int32 `json:"role_id,omitempty"`
RoleCode string `json:"role_code,omitempty"`
RoleName string `json:"role_name,omitempty"`
}

View File

@ -0,0 +1,6 @@
package HarborAPI
type RoleParam struct {
Roles []int32 `json:"roles,omitempty"`
UserName string `json:"user_name,omitempty"`
}

View File

@ -0,0 +1,8 @@
package HarborAPI
import ()
type Search struct {
Projects []Project4Search `json:"project,omitempty"`
Repositories []Repository4Search `json:"repository,omitempty"`
}

View File

@ -0,0 +1,11 @@
package HarborAPI
type User struct {
UserId int32 `json:"user_id,omitempty"`
Username string `json:"username,omitempty"`
Email string `json:"email,omitempty"`
Password string `json:"password,omitempty"`
Realname string `json:"realname,omitempty"`
Comment string `json:"comment,omitempty"`
Deleted int32 `json:"deleted,omitempty"`
}

View File

@ -0,0 +1,95 @@
package HarborAPItest
import (
"fmt"
"github.com/stretchr/testify/assert"
"testing"
"github.com/vmware/harbor/tests/apitests/apilib"
)
func TestAddProject(t *testing.T) {
fmt.Println("Test for Project Add (ProjectsPost) API\n")
assert := assert.New(t)
apiTest := HarborAPI.NewHarborAPI()
//prepare for test
adminEr := &HarborAPI.UsrInfo{"admin", "Harbor1234"}
admin := &HarborAPI.UsrInfo{"admin", "Harbor12345"}
prjUsr := &HarborAPI.UsrInfo{"unknown", "unknown"}
var project HarborAPI.Project
project.ProjectName = "testProject"
project.Public = true
//case 1: admin login fail, expect project creation fail.
fmt.Println("case 1: admin login fail, expect project creation fail.")
resault, err := apiTest.HarborLogin(*adminEr)
if err != nil {
t.Error("Error while admin login", err.Error())
t.Log(err)
} else {
assert.Equal(resault, int(401), "Admin login status should be 401")
//t.Log(resault)
}
resault, err = apiTest.ProjectsPost(*prjUsr, project)
if err != nil {
t.Error("Error while creat project", err.Error())
t.Log(err)
} else {
assert.Equal(resault, int(401), "Case 1: Project creation status should be 401")
//t.Log(resault)
}
//case 2: admin successful login, expect project creation success.
fmt.Println("case 2: admin successful login, expect project creation success.")
resault, err = apiTest.HarborLogin(*admin)
if err != nil {
t.Error("Error while admin login", err.Error())
t.Log(err)
} else {
assert.Equal(resault, int(200), "Admin login status should be 200")
//t.Log(resault)
}
if resault != 200 {
t.Log(resault)
} else {
prjUsr = admin
}
resault, err = apiTest.ProjectsPost(*prjUsr, project)
if err != nil {
t.Error("Error while creat project", err.Error())
t.Log(err)
} else {
assert.Equal(resault, int(201), "Case 2: Project creation status should be 201")
//t.Log(resault)
}
//case 3: duplicate project name, create project fail
fmt.Println("case 3: duplicate project name, create project fail")
resault, err = apiTest.ProjectsPost(*prjUsr, project)
if err != nil {
t.Error("Error while creat project", err.Error())
t.Log(err)
} else {
assert.Equal(resault, int(409), "Case 3: Project creation status should be 409")
//t.Log(resault)
}
//resault1, err := apiTest.HarborLogout()
//if err != nil {
// t.Error("Error while admin logout", err.Error())
// t.Log(err)
//} else {
// assert.Equal(resault1, int(200), "Admin logout status")
// //t.Log(resault)
//}
//if resault1 != 200 {
// t.Log(resault)
//}
}

View File

@ -0,0 +1,31 @@
package HarborAPItest
import (
"fmt"
"github.com/stretchr/testify/assert"
"testing"
"github.com/vmware/harbor/tests/apitests/apilib"
)
func TestSearch(t *testing.T) {
fmt.Println("Test for Search (SearchGet) API\n")
assert := assert.New(t)
apiTest := HarborAPI.NewHarborAPI()
var resault HarborAPI.Search
resault, err := apiTest.SearchGet("library")
//fmt.Printf("%+v\n", resault)
if err != nil {
t.Error("Error while search project or repository", err.Error())
t.Log(err)
} else {
assert.Equal(resault.Projects[0].ProjectId, int32(1), "Project id should be equal")
assert.Equal(resault.Projects[0].ProjectName, "library", "Project name should be library")
assert.Equal(resault.Projects[0].Public, int32(1), "Project public status should be 1 (true)")
//t.Log(resault)
}
//if resault.Response.StatusCode != 200 {
// t.Log(resault.Response)
//}
}

4
tests/hostcfg.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
IP=`ip addr s eth0 |grep "inet "|awk '{print $2}' |awk -F "/" '{print $1}'`
#echo $IP
sed "s/reg.mydomain.com/$IP/" -i Deploy/harbor.cfg

36
tests/startuptest.go Normal file
View File

@ -0,0 +1,36 @@
// Fetch prints the content found at a URL.
package main
import (
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
"time"
)
func main() {
time.Sleep(60*time.Second)
for _, url := range os.Args[1:] {
resp, err := http.Get(url)
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
os.Exit(1)
}
b, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)
os.Exit(1)
}
// fmt.Printf("%s", b)
if strings.Contains(string(b), "Harbor") {
fmt.Printf("sucess!\n")
} else {
os.Exit(1)
}
}
}

11
tests/testprepare.sh Executable file
View File

@ -0,0 +1,11 @@
IP=`ip addr s eth0 |grep "inet "|awk '{print $2}' |awk -F "/" '{print $1}'`
#echo $IP
docker pull hello-world
docker pull docker
#docker login -u admin -p Harbor12345 $IP
docker tag hello-world $IP/library/hello-world
docker push $IP/library/hello-world
docker tag docker $IP/library/docker
docker push $IP/library/docker

55
tests/userlogintest.go Normal file
View File

@ -0,0 +1,55 @@
package main
import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"flag"
)
func main() {
usrNamePtr := flag.String("name","anaymous","user name")
usrPasswdPtr := flag.String("passwd","anaymous","user password")
flag.Parse()
v := url.Values{}
v.Set("principal", *usrNamePtr)
v.Set("password", *usrPasswdPtr)
body := ioutil.NopCloser(strings.NewReader(v.Encode())) //endode v:[body struce]
fmt.Println(v)
client := &http.Client{}
reqest, err := http.NewRequest("POST", "http://localhost/login", body)
if err != nil {
fmt.Println("Fatal error ", err.Error())
}
reqest.Header.Set("Content-Type", "application/x-www-form-urlencoded;param=value") //setting post head
resp, err := client.Do(reqest)
defer resp.Body.Close() //close resp.Body
fmt.Println("login status: ", resp.StatusCode) //print status code
//content_post, err := ioutil.ReadAll(resp.Body)
//if err != nil {
// fmt.Println("Fatal error ", err.Error())
//}
//fmt.Println(string(content_post)) //print reply
response, err := http.Get("http://localhost/api/logout")
if err != nil {
fmt.Println("Fatal error ", err.Error())
}
defer response.Body.Close()
fmt.Println("logout status: ", resp.StatusCode) //print status code
//content_get, err := ioutil.ReadAll(response.Body)
//fmt.Println(string(content_get))
}

View File

@ -13,14 +13,14 @@
limitations under the License.
-->
<!-- Main jumbotron for a primary marketing message or call to action -->
<div class="jumbotron">
<div class="jumbotron">
<div class="container">
<img class="pull-left" src="static/resources/image/Harbor_Logo_rec.png" alt="Harbor's Logo" height="180" width="360"/>
<p class="pull-left" style="margin-top: 85px; color: #245580; font-size: 30pt; text-align: center; width: 60%;">{{i18n .Lang "index_title"}}</p>
</div>
</div>
</div>
<div class="container">
<div class="container">
<!-- Example row of columns -->
<div class="row">
<div class="col-md-12">
@ -33,5 +33,5 @@
<p>{{i18n .Lang "index_desc_5"}}</p>
</div>
</div>
</div> <!-- /container -->
</div> <!-- /container -->
<script src="static/resources/js/login.js"></script>

View File

@ -18,7 +18,7 @@
<li>{{.ProjectName}}</li>
</ol>
<div class="page-header" style="margin-top: -10px;">
<h2>{{.ProjectName}} </h2></h4>{{i18n .Lang "owner"}}: {{.OwnerName}}</h4>
<h2>{{.ProjectName}} </h2><h4>{{i18n .Lang "owner"}}: {{.OwnerName}}</h4>
</div>
<div row="tabpanel">
<div class="row">
@ -53,7 +53,7 @@
</div>
</div>
</form>
<p>
<p/>
<div class="table-responsive div-height">
<div class="alert alert-danger" role="alert" id="divErrMsg"><center></center></div>
<div class="panel-group" id="accordionRepo" role="tablist" aria-multiselectable="true">
@ -76,7 +76,7 @@
</div>
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#dlgUser" id="btnAddUser">{{i18n .Lang "add_members"}}</button>
</form>
<p>
<p/>
<div class="table-responsive div-height">
<table id="tblUser" class="table table-hover">
<thead>
@ -108,8 +108,7 @@
<button class="btn btn-link" type="button" data-toggle="collapse" data-target="#collapseAdvance" aria-expanded="false" aria-controls="collapseAdvance">{{i18n .Lang "advance"}}</button>
</div>
</div>
<form>
<p></p>
<p/>
<div class="collapse" id="collapseAdvance">
<form class="form">
<div class="form-group">
@ -139,7 +138,6 @@
</span>
</div>
</div>
</div>
<div class="form-group">
<div class="input-group">
@ -151,7 +149,6 @@
</span>
</div>
</div>
</div>
</form>
</div>
@ -170,11 +167,12 @@
</tbody>
</table>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="dlgUser" tabindex="-1" role="dialog" aria-labelledby="User" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">

View File

@ -78,8 +78,8 @@
</div>
</div>
</div>
</div>
<div class="modal fade" id="dlgAddProject" tabindex="-1" role="dialog" aria-labelledby="Add Project" aria-hidden="true">
</div>
<div class="modal fade" id="dlgAddProject" tabindex="-1" role="dialog" aria-labelledby="Add Project" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
@ -109,7 +109,7 @@
</div>
</div>
</div>
</div>
</div>
</div>
<script src="static/resources/js/validate-options.js"></script>
<script src="static/resources/js/project.js"></script>

View File

@ -14,8 +14,8 @@
-->
<!DOCTYPE html>
<html>
<body>
<body>
<p>{{.Hint}}:</p>
<a href="{{.URL}}/resetPassword?reset_uuid={{.UUID}}">{{.URL}}/resetPassword?reset_uuid={{.UUID}}</a>
</body>
</body>
</html>

View File

@ -14,15 +14,15 @@
-->
<!DOCTYPE html>
<html>
<head>
<head>
{{.HeaderInc}}
<title>{{.PageTitle}}</title>
</head>
<body>
</head>
<body>
{{.HeaderContent}}
{{.BodyContent}}
{{.FooterInc}}
{{.ModalDialog}}
{{.FootContent}}
</body>
</body>
</html>

View File

@ -11,7 +11,8 @@
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.
-->
-->
<input type="hidden" id="currentLanguage" value="{{.Lang}}">
<input type="hidden" id="isAdmin" value="{{.IsAdmin}}">
<nav class="navbar navbar-default" role="navigation" style="margin-bottom: 0;">
@ -27,6 +28,7 @@
<form class="navbar-form navbar-right">
<div class="form-group">
<div class="input-group">
<ul class="nav navbar-nav">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
@ -87,4 +89,5 @@
{{ end }}
</form>
</div>
</nav>
</nav>

View File

@ -13,11 +13,11 @@
limitations under the License.
-->
<style>
.center {
.center {
margin-left: auto;
margin-right: auto;
top: 10%;
}
}
</style>
<!-- Modal -->
<div class="center modal fade" id="dlgModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">