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 language: go
go: go:
@ -5,10 +7,32 @@ go:
go_import_path: github.com/vmware/harbor go_import_path: github.com/vmware/harbor
#service: services:
# - mysql - 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: install:
- sudo apt-get update && sudo apt-get install -y libldap2-dev - 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 -d github.com/go-sql-driver/mysql
- go get github.com/golang/lint/golint - go get github.com/golang/lint/golint
- go get github.com/GeertJohan/fgt - 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: before_script:
# create tables and load data # create tables and load data
- mysql < ./Deploy/db/registry.sql -uroot --verbose - mysql < ./Deploy/db/registry.sql -uroot --verbose
script: script:
- go list ./... | grep -v /vendor/ | xargs -L1 fgt golint - go list ./... | grep -v 'tests' | grep -v /vendor/ | xargs -L1 fgt golint
- go list ./... | grep -v 'vendor' | xargs -L1 go vet - go list ./... | grep -v 'tests' | 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 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 { type memberReq struct {
Username string `json:"user_name"` Username string `json:"username"`
UserID int `json:"user_id"` UserID int `json:"user_id"`
Roles []int `json:"roles"` Roles []int `json:"roles"`
} }
@ -104,7 +104,7 @@ func (pma *ProjectMemberAPI) Get() {
log.Errorf("Error occurred in GetUser, error: %v", err) log.Errorf("Error occurred in GetUser, error: %v", err)
pma.CustomAbort(http.StatusInternalServerError, "Internal error.") pma.CustomAbort(http.StatusInternalServerError, "Internal error.")
} }
result["user_name"] = user.Username result["username"] = user.Username
result["user_id"] = pma.memberID result["user_id"] = pma.memberID
result["roles"] = roleList result["roles"] = roleList
pma.Data["json"] = result pma.Data["json"] = result

View File

@ -14,8 +14,6 @@ import (
// RepPolicyAPI handles /api/replicationPolicies /api/replicationPolicies/:id/enablement // RepPolicyAPI handles /api/replicationPolicies /api/replicationPolicies/:id/enablement
type RepPolicyAPI struct { type RepPolicyAPI struct {
BaseAPI BaseAPI
policyID int64
policy *models.RepPolicy
} }
// Prepare validates whether the user has system admin role // Prepare validates whether the user has system admin role
@ -214,11 +212,11 @@ func (pa *RepPolicyAPI) UpdateEnablement() {
return return
} }
if pa.policy.Enabled == e.Enabled { if policy.Enabled == e.Enabled {
return 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) log.Errorf("Failed to update policy enablement in DB, error: %v", err)
pa.RenderError(http.StatusInternalServerError, "Internal Error") pa.RenderError(http.StatusInternalServerError, "Internal Error")
return return
@ -226,18 +224,18 @@ func (pa *RepPolicyAPI) UpdateEnablement() {
if e.Enabled == 1 { if e.Enabled == 1 {
go func() { go func() {
if err := TriggerReplication(pa.policyID, "", nil, models.RepOpTransfer); err != nil { if err := TriggerReplication(id, "", nil, models.RepOpTransfer); err != nil {
log.Errorf("failed to trigger replication of %d: %v", pa.policyID, err) log.Errorf("failed to trigger replication of %d: %v", id, err)
} else { } else {
log.Infof("replication of %d triggered", pa.policyID) log.Infof("replication of %d triggered", id)
} }
}() }()
} else { } else {
go func() { go func() {
if err := postReplicationAction(pa.policyID, "stop"); err != nil { if err := postReplicationAction(id, "stop"); err != nil {
log.Errorf("failed to stop replication of %d: %v", pa.policyID, err) log.Errorf("failed to stop replication of %d: %v", id, err)
} else { } 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: info:
title: Harbor API title: Harbor API
description: These APIs provide services for manipulating Harbor project. description: These APIs provide services for manipulating Harbor project.
version: "0.1.0" version: "0.1.1"
# the domain of the service # the domain of the service
host: localhost host: localhost
# array of all schemes that your API supports # array of all schemes that your API supports
@ -167,7 +167,7 @@ paths:
- name: access_log - name: access_log
in: body in: body
schema: schema:
$ref: '#/definitions/AccessLog' $ref: '#/definitions/AccessLogFilter'
description: Search results of access logs. description: Search results of access logs.
tags: tags:
- Products - Products
@ -204,7 +204,7 @@ paths:
schema: schema:
type: array type: array
items: items:
$ref: '#/definitions/Role' $ref: '#/definitions/User'
400: 400:
description: Illegal format of provided ID value. description: Illegal format of provided ID value.
401: 401:
@ -567,7 +567,7 @@ paths:
schema: schema:
type: array type: array
items: items:
$ref: '#/definitions/Repository' type: string
400: 400:
description: Invalid project ID. description: Invalid project ID.
403: 403:
@ -719,12 +719,41 @@ definitions:
description: Search results of the projects that matched the filter keywords. description: Search results of the projects that matched the filter keywords.
type: array type: array
items: items:
$ref: '#/definitions/Project' $ref: '#/definitions/SearchProject'
repositories: repositories:
description: Search results of the repositories that matched the filter keywords. description: Search results of the repositories that matched the filter keywords.
type: array type: array
items: 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: Project:
type: object type: object
properties: properties:
@ -736,30 +765,39 @@ definitions:
type: integer type: integer
format: int32 format: int32
description: The owner ID of the project always means the creator of the project. description: The owner ID of the project always means the creator of the project.
project_name: name:
type: string type: string
description: The name of the project. description: The name of the project.
creation_time: creation_time:
type: string type: string
description: The creation time of the project. description: The creation time of the project.
update_time:
type: string
description: The update time of the project.
deleted: deleted:
type: integer type: integer
format: int32 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: user_id:
type: integer type: integer
format: int32 format: int32
description: A relation field to the user table. description: A relation field to the user table.
owner_name: owner_name:
type: string type: string
description: The owner name of tthe project always means the creator of the project. description: The owner name of the project.
public: public:
type: boolean type: boolean
format: boolean format: boolean
description: The public status of the project. description: The public status of the project.
togglable: togglable:
type: boolean 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: Repository:
type: object type: object
properties: properties:
@ -794,6 +832,7 @@ definitions:
user_id: user_id:
type: integer type: integer
format: int32 format: int32
description: The ID of the user.
username: username:
type: string type: string
email: email:
@ -816,7 +855,7 @@ definitions:
new_password: new_password:
type: string type: string
description: New password for marking as to be updated. description: New password for marking as to be updated.
AccessLog: AccessLogFilter:
type: object type: object
properties: properties:
username: username:
@ -825,14 +864,32 @@ definitions:
keywords: keywords:
type: string type: string
description: Operation name specified when project created. description: Operation name specified when project created.
beginTimestamp: begin_timestamp:
type: integer type: integer
format: int32 format: int64
description: Begin timestamp for querying access logs. description: Begin timestamp for querying access logs.
endTimestamp: end_timestamp:
type: integer type: integer
format: int32 format: int64
description: End timestamp for querying accessl logs. 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: Role:
type: object type: object
properties: properties:
@ -855,7 +912,7 @@ definitions:
type: integer type: integer
format: int32 format: int32
description: Role ID for updating project role member. description: Role ID for updating project role member.
user_name: username:
type: string type: string
description: Username relevant to a project role member. description: Username relevant to a project role member.
TopRepo: 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. // AccessLog holds information about logs which are used to record the actions that user take to the resourses.
type AccessLog struct { type AccessLog struct {
LogID int `orm:"pk;column(log_id)" json:"LogId"` LogID int `orm:"column(log_id)" json:"log_id"`
UserID int `orm:"column(user_id)" json:"UserId"` UserID int `orm:"column(user_id)" json:"user_id"`
ProjectID int64 `orm:"column(project_id)" json:"ProjectId"` ProjectID int64 `orm:"column(project_id)" json:"project_id"`
RepoName string `orm:"column(repo_name)"` RepoName string `orm:"column(repo_name)" json:"repo_name"`
RepoTag string `orm:"column(repo_tag)"` RepoTag string `orm:"column(repo_tag)" json:"repo_tag"`
GUID string `orm:"column(GUID)" json:"Guid"` GUID string `orm:"column(GUID)" json:"guid"`
Operation string `orm:"column(operation)"` Operation string `orm:"column(operation)" json:"operation"`
OpTime time.Time `orm:"column(op_time)"` OpTime time.Time `orm:"column(op_time)" json:"op_time"`
Username string Username string `json:"username"`
Keywords string Keywords string `json:"keywords"`
BeginTime time.Time BeginTime time.Time
BeginTimestamp int64 BeginTimestamp int64 `json:"begin_timestamp"`
EndTime time.Time EndTime time.Time
EndTimestamp int64 EndTimestamp int64 `json:"end_timestamp"`
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -105,7 +105,7 @@ jQuery(function(){
$('#accordionRepo').on('show.bs.collapse', function (e) { $('#accordionRepo').on('show.bs.collapse', function (e) {
$('#accordionRepo .in').collapse('hide'); $('#accordionRepo .in').collapse('hide');
var targetId = $(e.target).attr("targetId"); 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({ new AjaxUtil({
url: "/api/repositories/tags?repo_name=" + repoName, url: "/api/repositories/tags?repo_name=" + repoName,
type: "get", type: "get",
@ -113,7 +113,7 @@ jQuery(function(){
$('#' + targetId +' table tbody tr').remove(); $('#' + targetId +' table tbody tr').remove();
var row = []; var row = [];
for(var i in data){ 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>'); 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("")); $('#' + 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"); data.Created = moment(new Date(data.Created)).format("YYYY-MM-DD HH:mm:ss");
$("#dlgModal").dialogModal({"title": i18n.getMessage("image_details"), "content": data}); $("#dlgModal").dialogModal({"title": i18n.getMessage("image_details"), "content": data});
} }
} }
@ -204,7 +203,7 @@ jQuery(function(){
if(operationType == "add"){ if(operationType == "add"){
ajaxOpts.url = "/api/projects/" + projectId + "/members/"; ajaxOpts.url = "/api/projects/" + projectId + "/members/";
ajaxOpts.type = "post"; ajaxOpts.type = "post";
ajaxOpts.data = {"roles" : checkedRoleItemList, "user_name": username}; ajaxOpts.data = {"roles" : checkedRoleItemList, "username": username};
}else if(operationType == "edit"){ }else if(operationType == "edit"){
ajaxOpts.url = "/api/projects/" + projectId + "/members/" + userId; ajaxOpts.url = "/api/projects/" + projectId + "/members/" + userId;
ajaxOpts.type = "put"; ajaxOpts.type = "put";
@ -241,24 +240,15 @@ jQuery(function(){
var ownerId = $("#ownerId").val(); var ownerId = $("#ownerId").val();
$("#tblUser tbody tr").remove(); $("#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 userId = userList[i].user_id;
var roleId = userList[i].RoleId; var roleId = userList[i].role_id;
var username = userList[i].username; 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>' + var row = '<tr>' +
'<td>' + username + '</td>' + '<td>' + username + '</td>' +
'<td>' + roleNameList.join(",") + '</td>' + '<td>' + name_mapping[userList[i].role_name] + '</td>' +
'<td>'; '<td>';
var isShowOperations = true; var isShowOperations = true;
if(loginedUserRoleId >= 3 /*role: developer guest*/){ if(loginedUserRoleId >= 3 /*role: developer guest*/){
@ -284,11 +274,11 @@ jQuery(function(){
$.each(LogList || [], function(i, e){ $.each(LogList || [], function(i, e){
$("#tabOperationLog tbody").append( $("#tabOperationLog tbody").append(
'<tr>' + '<tr>' +
'<td>' + e.Username + '</td>' + '<td>' + e.username + '</td>' +
'<td>' + e.RepoName + '</td>' + '<td>' + e.repo_name + '</td>' +
'<td>' + e.RepoTag + '</td>' + '<td>' + e.repo_tag + '</td>' +
'<td>' + e.Operation + '</td>' + '<td>' + e.operation + '</td>' +
'<td>' + moment(new Date(e.OpTime)).format("YYYY-MM-DD HH:mm:ss") + '</td>' + '<td>' + moment(new Date(e.op_time)).format("YYYY-MM-DD HH:mm:ss") + '</td>' +
'</tr>'); '</tr>');
}); });
} }
@ -302,7 +292,7 @@ jQuery(function(){
$("#operationType").val("edit"); $("#operationType").val("edit");
$("#editUserId").val(user.user_id); $("#editUserId").val(user.user_id);
$("#spnSearch").hide(); $("#spnSearch").hide();
$("#txtUserName").val(user.user_name); $("#txtUserName").val(user.username);
$("#txtUserName").prop("disabled", true); $("#txtUserName").prop("disabled", true);
$("#btnSave").removeClass("disabled"); $("#btnSave").removeClass("disabled");
$("#dlgUserTitle").text(i18n.getMessage("edit_members")); $("#dlgUserTitle").text(i18n.getMessage("edit_members"));
@ -404,7 +394,7 @@ jQuery(function(){
new AjaxUtil({ new AjaxUtil({
url: "/api/projects/" + projectId + "/logs/filter", 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", type: "post",
success: function(data, status, xhr){ success: function(data, status, xhr){
if(xhr && xhr.status == 200){ if(xhr && xhr.status == 200){

View File

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

View File

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

@ -18,7 +18,7 @@
<li>{{.ProjectName}}</li> <li>{{.ProjectName}}</li>
</ol> </ol>
<div class="page-header" style="margin-top: -10px;"> <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>
<div row="tabpanel"> <div row="tabpanel">
<div class="row"> <div class="row">
@ -53,7 +53,7 @@
</div> </div>
</div> </div>
</form> </form>
<p> <p/>
<div class="table-responsive div-height"> <div class="table-responsive div-height">
<div class="alert alert-danger" role="alert" id="divErrMsg"><center></center></div> <div class="alert alert-danger" role="alert" id="divErrMsg"><center></center></div>
<div class="panel-group" id="accordionRepo" role="tablist" aria-multiselectable="true"> <div class="panel-group" id="accordionRepo" role="tablist" aria-multiselectable="true">
@ -76,7 +76,7 @@
</div> </div>
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#dlgUser" id="btnAddUser">{{i18n .Lang "add_members"}}</button> <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#dlgUser" id="btnAddUser">{{i18n .Lang "add_members"}}</button>
</form> </form>
<p> <p/>
<div class="table-responsive div-height"> <div class="table-responsive div-height">
<table id="tblUser" class="table table-hover"> <table id="tblUser" class="table table-hover">
<thead> <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> <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>
</div> </div>
<form> <p/>
<p></p>
<div class="collapse" id="collapseAdvance"> <div class="collapse" id="collapseAdvance">
<form class="form"> <form class="form">
<div class="form-group"> <div class="form-group">
@ -139,7 +138,6 @@
</span> </span>
</div> </div>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-group"> <div class="input-group">
@ -151,7 +149,6 @@
</span> </span>
</div> </div>
</div> </div>
</div> </div>
</form> </form>
</div> </div>
@ -170,6 +167,7 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</form>
</div> </div>
</div> </div>
</div> </div>

View File

@ -12,6 +12,7 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<input type="hidden" id="currentLanguage" value="{{.Lang}}"> <input type="hidden" id="currentLanguage" value="{{.Lang}}">
<input type="hidden" id="isAdmin" value="{{.IsAdmin}}"> <input type="hidden" id="isAdmin" value="{{.IsAdmin}}">
<nav class="navbar navbar-default" role="navigation" style="margin-bottom: 0;"> <nav class="navbar navbar-default" role="navigation" style="margin-bottom: 0;">
@ -27,6 +28,7 @@
<form class="navbar-form navbar-right"> <form class="navbar-form navbar-right">
<div class="form-group"> <div class="form-group">
<div class="input-group"> <div class="input-group">
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
<li class="dropdown"> <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
@ -88,3 +90,4 @@
</form> </form>
</div> </div>
</nav> </nav>