Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Tan Jiang 2016-09-27 19:40:56 +08:00
commit 7219544bba
206 changed files with 13549 additions and 3256 deletions

View File

@ -9,24 +9,26 @@ go_import_path: github.com/vmware/harbor
services: services:
- docker - docker
- mysql
dist: trusty dist: trusty
addons:
apt:
packages:
- mysql-server-5.6
- mysql-client-core-5.6
- mysql-client-5.6
env: env:
DB_HOST: 127.0.0.1 DB_HOST: 127.0.0.1
DB_PORT: 3306 DB_PORT: 3306
DB_USR: root DB_USR: root
DB_PWD: DB_PWD: root123
MYSQL_HOST: localhost
MYSQL_PORT: 3306
MYSQL_USR: root
MYSQL_PWD: root123
DOCKER_COMPOSE_VERSION: 1.7.1 DOCKER_COMPOSE_VERSION: 1.7.1
HARBOR_ADMIN: admin HARBOR_ADMIN: admin
HARBOR_ADMIN_PASSWD: Harbor12345 HARBOR_ADMIN_PASSWD: Harbor12345
UI_SECRET: tempString
MAX_JOB_WORKERS: 3
SECRET_KEY: 1234567890123456
AUTH_MODE: db_auth
SELF_REGISTRATION: "on"
before_install: before_install:
- sudo ./tests/hostcfg.sh - sudo ./tests/hostcfg.sh
@ -53,32 +55,42 @@ 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 # - 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 - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose
- chmod +x docker-compose - chmod +x docker-compose
- sudo mv docker-compose /usr/local/bin - sudo mv docker-compose /usr/local/bin
- sudo sed -i '$a DOCKER_OPTS=\"$DOCKER_OPTS --insecure-registry 127.0.0.1\"' /etc/default/docker - IP=`ip addr s eth0 |grep "inet "|awk '{print $2}' |awk -F "/" '{print $1}'`
- sudo sed -i '$a DOCKER_OPTS=\"--insecure-registry '$IP':5000\"' /etc/default/docker
- sudo service docker restart - sudo service docker restart
- go get github.com/dghubble/sling - go get github.com/dghubble/sling
- go get github.com/stretchr/testify - go get github.com/stretchr/testify
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
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 'tests' | grep -v /vendor/ | xargs -L1 fgt golint - sudo ./tests/testprepare.sh
- go list ./... | grep -v 'tests' | grep -v 'vendor' | xargs -L1 go vet - docker-compose -f Deploy/docker-compose.test.yml up -d
- go list ./... | grep -v 'tests' | grep -v 'vendor' | xargs -L1 go test -v - go list ./... | grep -v -E 'vendor|tests' | xargs -L1 fgt golint
- go list ./... | grep -v -E 'vendor|tests' | xargs -L1 go vet
- export MYSQL_HOST=$IP
- export REGISTRY_URL=$IP:5000
- echo $REGISTRY_URL
- ./tests/pushimage.sh
- ./Deploy/coverage4gotest.sh
- goveralls -coverprofile=profile.cov -service=travis-ci
- docker-compose -f Deploy/docker-compose.test.yml down
- docker-compose -f Deploy/docker-compose.yml up -d - docker-compose -f Deploy/docker-compose.yml up -d
- docker ps - docker ps
- go run tests/startuptest.go http://localhost/ - go run tests/startuptest.go http://localhost/
- go run tests/userlogintest.go -name ${HARBOR_ADMIN} -passwd ${HARBOR_ADMIN_PASSWD} - go run tests/userlogintest.go -name ${HARBOR_ADMIN} -passwd ${HARBOR_ADMIN_PASSWD}
# - sudo ./tests/testprepare.sh
# test for API # - go test -v ./tests/apitests
- sudo ./tests/testprepare.sh
- go test -v ./tests/apitests

50
CHANGELOG.md Normal file
View File

@ -0,0 +1,50 @@
# Changelog
## v0.4.0 (2016-09-23)
- Database schema changed, data migration/upgrade is needed for previous version.
- A project can be deleted when no images and policies are under it.
- Deleted users can be recreated.
- Replication policy can be deleted.
- Enhanced LDAP authentication, allowing multiple uid attributes.
- Pagination in UI.
- Improved authentication for remote image replication.
- Display release version in UI
- Offline installer.
- Various bug fixes.
## v0.3.5 (2016-08-13)
- Vendoring all dependencies and remove go get from dockerfile
- Installer using Docker Hub to download images
- Harbor base images moved to Photon OS (except for official images from third party)
- New Harbor logo
- Various bug fixes
## v0.3.0 (2016-07-15)
- Database schema changed, data migration/upgrade is needed for previous version.
- New UI
- Image replication across multiple registry instances
- Integration with registry v2.4.0 to support image deletion and garbage collection
- Database migration tool
- Bug fixes
## v0.1.1 (2016-04-08)
- Refactored database schema
- Migrate to docker-compose v2 template
- Update token service to support layer mount
- Various bug fixes
## v0.1.0 (2016-03-11)
Initial release, key features include
- Role based access control (RBAC)
- LDAP / AD integration
- Graphical user interface (GUI)
- Auditting and logging
- RESTful API
- Internationalization

35
Deploy/coverage4gotest.sh Executable file
View File

@ -0,0 +1,35 @@
#!/bin/bash
set -e
echo "mode: set" >>profile.cov
deps=""
# listDeps lists packages referenced by package in $1,
# excluding golang standard library and packages in
# direcotry vendor
function listDeps(){
pkg=$1
deps=$pkg
ds=$(echo $(go list -f '{{.Imports}}' $pkg) | sed 's/[][]//g')
for d in $ds
do
if echo $d | grep -q "github.com/vmware/harbor" && echo $d | grep -qv "vendor"
then
deps="$deps,$d"
fi
done
}
packages=$(go list ./... | grep -v -E 'vendor|tests')
for package in $packages
do
listDeps $package
go test -cover -coverprofile=profile.tmp -coverpkg "$deps" $package
if [ -f profile.tmp ]
then
cat profile.tmp | tail -n +2 >> profile.cov
rm profile.tmp
fi
done

View File

@ -38,8 +38,14 @@ insert into role (role_code, name) values
create table user ( create table user (
user_id int NOT NULL AUTO_INCREMENT, user_id int NOT NULL AUTO_INCREMENT,
username varchar(15), # The max length of username controlled by API is 20,
email varchar(128), # and 11 is reserved for marking the deleted users.
# The mark of deleted user is "#user_id".
# The 11 consist of 10 for the max value of user_id(4294967295)
# in MySQL and 1 of '#'.
username varchar(32),
# 11 bytes is reserved for marking the deleted users.
email varchar(255),
password varchar(40) NOT NULL, password varchar(40) NOT NULL,
realname varchar (20) NOT NULL, realname varchar (20) NOT NULL,
comment varchar (30), comment varchar (30),
@ -61,7 +67,9 @@ insert into user (username, email, password, realname, comment, deleted, sysadmi
create table project ( create table project (
project_id int NOT NULL AUTO_INCREMENT, project_id int NOT NULL AUTO_INCREMENT,
owner_id int NOT NULL, owner_id int NOT NULL,
name varchar (30) NOT NULL, # The max length of name controlled by API is 30,
# and 11 is reserved for marking the deleted project.
name varchar (41) NOT NULL,
creation_time timestamp, creation_time timestamp,
update_time timestamp, update_time timestamp,
deleted tinyint (1) DEFAULT 0 NOT NULL, deleted tinyint (1) DEFAULT 0 NOT NULL,
@ -99,10 +107,25 @@ create table access_log (
operation varchar(20) NOT NULL, operation varchar(20) NOT NULL,
op_time timestamp, op_time timestamp,
primary key (log_id), primary key (log_id),
INDEX pid_optime (project_id, op_time),
FOREIGN KEY (user_id) REFERENCES user(user_id), FOREIGN KEY (user_id) REFERENCES user(user_id),
FOREIGN KEY (project_id) REFERENCES project (project_id) FOREIGN KEY (project_id) REFERENCES project (project_id)
); );
create table repository (
repository_id int NOT NULL AUTO_INCREMENT,
name varchar(255) NOT NULL,
project_id int NOT NULL,
owner_id int NOT NULL,
description text,
pull_count int DEFAULT 0 NOT NULL,
star_count int DEFAULT 0 NOT NULL,
creation_time timestamp default CURRENT_TIMESTAMP,
update_time timestamp default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
primary key (repository_id),
UNIQUE (name)
);
create table replication_policy ( create table replication_policy (
id int NOT NULL AUTO_INCREMENT, id int NOT NULL AUTO_INCREMENT,
name varchar(256), name varchar(256),
@ -110,6 +133,7 @@ create table replication_policy (
target_id int NOT NULL, target_id int NOT NULL,
enabled tinyint(1) NOT NULL DEFAULT 1, enabled tinyint(1) NOT NULL DEFAULT 1,
description text, description text,
deleted tinyint (1) DEFAULT 0 NOT NULL,
cron_str varchar(256), cron_str varchar(256),
start_time timestamp NULL, start_time timestamp NULL,
creation_time timestamp default CURRENT_TIMESTAMP, creation_time timestamp default CURRENT_TIMESTAMP,
@ -122,7 +146,7 @@ create table replication_target (
name varchar(64), name varchar(64),
url varchar(64), url varchar(64),
username varchar(40), username varchar(40),
password varchar(40), password varchar(128),
/* /*
target_type indicates the type of target registry, target_type indicates the type of target registry,
0 means it's a harbor instance, 0 means it's a harbor instance,
@ -144,7 +168,8 @@ create table replication_job (
creation_time timestamp default CURRENT_TIMESTAMP, creation_time timestamp default CURRENT_TIMESTAMP,
update_time timestamp default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, update_time timestamp default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
PRIMARY KEY (id), PRIMARY KEY (id),
INDEX policy (policy_id) INDEX policy (policy_id),
INDEX poid_uptime (policy_id, update_time)
); );
create table properties ( create table properties (
@ -157,4 +182,4 @@ CREATE TABLE IF NOT EXISTS `alembic_version` (
`version_num` varchar(32) NOT NULL `version_num` varchar(32) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8; ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into alembic_version values ('0.3.0'); insert into alembic_version values ('0.4.0');

View File

@ -16,7 +16,9 @@ email_password = abc
email_from = admin <sample_admin@mydomain.com> email_from = admin <sample_admin@mydomain.com>
email_ssl = false email_ssl = false
##The password of Harbor admin, change this before any production use. ##The initial password of Harbor admin, only works for the first time when Harbor starts.
#It has no effect after the first launch of Harbor.
#Change the admin password from UI after launching Harbor.
harbor_admin_password = Harbor12345 harbor_admin_password = Harbor12345
##By default the auth mode is db_auth, i.e. the credentials are stored in a local database. ##By default the auth mode is db_auth, i.e. the credentials are stored in a local database.
@ -26,10 +28,24 @@ auth_mode = db_auth
#The url for an ldap endpoint. #The url for an ldap endpoint.
ldap_url = ldaps://ldap.mydomain.com ldap_url = ldaps://ldap.mydomain.com
#The basedn template to look up a user in LDAP and verify the user's password. #A user's DN who has the permission to search the LDAP/AD server.
#For AD server, uses this template: #If your LDAP/AD server does not support anonymous search, you should configure this DN and ldap_search_pwd.
#ldap_basedn = CN=%s,OU=Dept1,DC=mydomain,DC=com #ldap_searchdn = uid=searchuser,ou=people,dc=mydomain,dc=com
ldap_basedn = uid=%s,ou=people,dc=mydomain,dc=com
#the password of the ldap_searchdn
#ldap_search_pwd = password
#The base DN from which to look up a user in LDAP/AD
ldap_basedn = ou=people,dc=mydomain,dc=com
#Search filter for LDAP/AD, make sure the syntax of the filter is correct.
#ldap_filter = (objectClass=person)
# The attribute used in a search to match a user, it could be uid, cn, email, sAMAccountName or other attributes depending on your LDAP/AD
ldap_uid = uid
#the scope to search for users, 1-LDAP_SCOPE_BASE, 2-LDAP_SCOPE_ONELEVEL, 3-LDAP_SCOPE_SUBTREE
ldap_scope = 3
#The password for the root user of mysql db, change this before any production use. #The password for the root user of mysql db, change this before any production use.
db_password = root123 db_password = root123
@ -44,7 +60,12 @@ use_compressed_js = on
#Maximum number of job workers in job service #Maximum number of job workers in job service
max_job_workers = 3 max_job_workers = 3
#The expiration of token used by token service, default is 30 minutes #Secret key for encryption/decryption of password of remote registry, its length has to be 16 chars
#**NOTE** if this changes, previously encrypted password will not be decrypted!
#Change this key before any production use.
secret_key = secretkey1234567
#The expiration time (in minute) of token created by token service, default is 30 minutes
token_expiration = 30 token_expiration = 30
#Determine whether the job service should verify the ssl cert when it connects to a remote registry. #Determine whether the job service should verify the ssl cert when it connects to a remote registry.

View File

@ -1,12 +1,18 @@
#!/bin/bash #!/bin/bash
set -e set -e
echo "This shell will minify the Javascript in Harbor project." echo "This shell will minify the Javascript in Harbor project."
echo "Usage: #jsminify [src] [dest]" echo "Usage: #jsminify [src] [dest] [basedir]"
#prepare workspace #prepare workspace
rm -rf $2 /tmp/harbor.app.temp.js rm -rf $2 /tmp/harbor.app.temp.js
BASEPATH=/go/bin if [ -z $3 ]
then
BASEPATH=/go/bin
else
BASEPATH=$3
fi
#concat the js files from js include file #concat the js files from js include file
echo "Concat js files..." echo "Concat js files..."
@ -20,6 +26,12 @@ do
fi fi
done done
# If you want run this script on Mac OS X,
# I suggest you install gnu-sed (whth --with-default-names option).
# $ brew install gnu-sed --with-default-names
# Reference:
# http://stackoverflow.com/a/27834828/3167471
#remove space #remove space
echo "Remove space.." echo "Remove space.."
sed 's/ \+/ /g' -i /tmp/harbor.app.temp.js sed 's/ \+/ /g' -i /tmp/harbor.app.temp.js

View File

@ -7,7 +7,7 @@ import string
import os import os
import sys import sys
import argparse import argparse
import commands import subprocess
from io import open from io import open
if sys.version_info[:3][0] == 2: if sys.version_info[:3][0] == 2:
@ -18,6 +18,10 @@ if sys.version_info[:3][0] == 3:
import configparser as ConfigParser import configparser as ConfigParser
import io as StringIO import io as StringIO
def validate(conf):
if len(conf.get("configuration", "secret_key")) != 16:
raise Exception("Error: The length of secret key has to be 16 characters!")
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('-conf', dest='cfgfile', default = 'harbor.cfg',type=str,help="the path of Harbor configuration file") parser.add_argument('-conf', dest='cfgfile', default = 'harbor.cfg',type=str,help="the path of Harbor configuration file")
args = parser.parse_args() args = parser.parse_args()
@ -30,6 +34,8 @@ conf.seek(0, os.SEEK_SET)
rcp = ConfigParser.RawConfigParser() rcp = ConfigParser.RawConfigParser()
rcp.readfp(conf) rcp.readfp(conf)
validate(rcp)
hostname = rcp.get("configuration", "hostname") hostname = rcp.get("configuration", "hostname")
ui_url = rcp.get("configuration", "ui_url_protocol") + "://" + hostname ui_url = rcp.get("configuration", "ui_url_protocol") + "://" + hostname
email_server = rcp.get("configuration", "email_server") email_server = rcp.get("configuration", "email_server")
@ -41,7 +47,21 @@ email_ssl = rcp.get("configuration", "email_ssl")
harbor_admin_password = rcp.get("configuration", "harbor_admin_password") harbor_admin_password = rcp.get("configuration", "harbor_admin_password")
auth_mode = rcp.get("configuration", "auth_mode") auth_mode = rcp.get("configuration", "auth_mode")
ldap_url = rcp.get("configuration", "ldap_url") ldap_url = rcp.get("configuration", "ldap_url")
# this two options are either both set or unset
if rcp.has_option("configuration", "ldap_searchdn"):
ldap_searchdn = rcp.get("configuration", "ldap_searchdn")
ldap_search_pwd = rcp.get("configuration", "ldap_search_pwd")
else:
ldap_searchdn = ""
ldap_search_pwd = ""
ldap_basedn = rcp.get("configuration", "ldap_basedn") ldap_basedn = rcp.get("configuration", "ldap_basedn")
# ldap_filter is null by default
if rcp.has_option("configuration", "ldap_filter"):
ldap_filter = rcp.get("configuration", "ldap_filter")
else:
ldap_filter = ""
ldap_uid = rcp.get("configuration", "ldap_uid")
ldap_scope = rcp.get("configuration", "ldap_scope")
db_password = rcp.get("configuration", "db_password") db_password = rcp.get("configuration", "db_password")
self_registration = rcp.get("configuration", "self_registration") self_registration = rcp.get("configuration", "self_registration")
use_compressed_js = rcp.get("configuration", "use_compressed_js") use_compressed_js = rcp.get("configuration", "use_compressed_js")
@ -56,16 +76,9 @@ crt_email = rcp.get("configuration", "crt_email")
max_job_workers = rcp.get("configuration", "max_job_workers") max_job_workers = rcp.get("configuration", "max_job_workers")
token_expiration = rcp.get("configuration", "token_expiration") token_expiration = rcp.get("configuration", "token_expiration")
verify_remote_cert = rcp.get("configuration", "verify_remote_cert") verify_remote_cert = rcp.get("configuration", "verify_remote_cert")
secret_key = rcp.get("configuration", "secret_key")
######## ########
#Read version form .git
status, output = commands.getstatusoutput('git describe --tags')
if status == 0:
version = output
else:
version = 'UNKNOWN'
#######
ui_secret = ''.join(random.choice(string.ascii_letters+string.digits) for i in range(16)) ui_secret = ''.join(random.choice(string.ascii_letters+string.digits) for i in range(16))
base_dir = os.path.dirname(__file__) base_dir = os.path.dirname(__file__)
@ -112,13 +125,18 @@ render(os.path.join(templates_dir, "ui", "env"),
auth_mode=auth_mode, auth_mode=auth_mode,
harbor_admin_password=harbor_admin_password, harbor_admin_password=harbor_admin_password,
ldap_url=ldap_url, ldap_url=ldap_url,
ldap_searchdn =ldap_searchdn,
ldap_search_pwd =ldap_search_pwd,
ldap_basedn=ldap_basedn, ldap_basedn=ldap_basedn,
ldap_filter=ldap_filter,
ldap_uid=ldap_uid,
ldap_scope=ldap_scope,
self_registration=self_registration, self_registration=self_registration,
use_compressed_js=use_compressed_js, use_compressed_js=use_compressed_js,
ui_secret=ui_secret, ui_secret=ui_secret,
secret_key=secret_key,
verify_remote_cert=verify_remote_cert, verify_remote_cert=verify_remote_cert,
token_expiration=token_expiration, token_expiration=token_expiration)
version=version)
render(os.path.join(templates_dir, "ui", "app.conf"), render(os.path.join(templates_dir, "ui", "app.conf"),
ui_conf, ui_conf,
@ -143,6 +161,7 @@ render(os.path.join(templates_dir, "jobservice", "env"),
db_password=db_password, db_password=db_password,
ui_secret=ui_secret, ui_secret=ui_secret,
max_job_workers=max_job_workers, max_job_workers=max_job_workers,
secret_key=secret_key,
ui_url=ui_url, ui_url=ui_url,
verify_remote_cert=verify_remote_cert) verify_remote_cert=verify_remote_cert)
@ -188,7 +207,6 @@ def openssl_is_installed(stat):
return False return False
if customize_crt == 'on': if customize_crt == 'on':
import subprocess
shell_stat = subprocess.check_call(["which", "openssl"], stdout=FNULL, stderr=subprocess.STDOUT) shell_stat = subprocess.check_call(["which", "openssl"], stdout=FNULL, stderr=subprocess.STDOUT)
if openssl_is_installed(shell_stat): if openssl_is_installed(shell_stat):
private_key_pem = os.path.join(config_dir, "ui", "private_key.pem") private_key_pem = os.path.join(config_dir, "ui", "private_key.pem")

View File

@ -3,6 +3,7 @@ MYSQL_PORT=3306
MYSQL_USR=root MYSQL_USR=root
MYSQL_PWD=$db_password MYSQL_PWD=$db_password
UI_SECRET=$ui_secret UI_SECRET=$ui_secret
SECRET_KEY=$secret_key
CONFIG_PATH=/etc/jobservice/app.conf CONFIG_PATH=/etc/jobservice/app.conf
REGISTRY_URL=http://registry:5000 REGISTRY_URL=http://registry:5000
VERIFY_REMOTE_CERT=$verify_remote_cert VERIFY_REMOTE_CERT=$verify_remote_cert

View File

@ -10,8 +10,14 @@ HARBOR_ADMIN_PASSWORD=$harbor_admin_password
HARBOR_URL=$ui_url HARBOR_URL=$ui_url
AUTH_MODE=$auth_mode AUTH_MODE=$auth_mode
LDAP_URL=$ldap_url LDAP_URL=$ldap_url
LDAP_SEARCH_DN=$ldap_searchdn
LDAP_SEARCH_PWD=$ldap_search_pwd
LDAP_BASE_DN=$ldap_basedn LDAP_BASE_DN=$ldap_basedn
LDAP_FILTER=$ldap_filter
LDAP_UID=$ldap_uid
LDAP_SCOPE=$ldap_scope
UI_SECRET=$ui_secret UI_SECRET=$ui_secret
SECRET_KEY=$secret_key
SELF_REGISTRATION=$self_registration SELF_REGISTRATION=$self_registration
USE_COMPRESSED_JS=$use_compressed_js USE_COMPRESSED_JS=$use_compressed_js
LOG_LEVEL=debug LOG_LEVEL=debug
@ -20,4 +26,3 @@ EXT_ENDPOINT=$ui_url
TOKEN_URL=http://ui TOKEN_URL=http://ui
VERIFY_REMOTE_CERT=$verify_remote_cert VERIFY_REMOTE_CERT=$verify_remote_cert
TOKEN_EXPIRATION=$token_expiration TOKEN_EXPIRATION=$token_expiration
VERSION=$version

2678
LICENSE

File diff suppressed because it is too large Load Diff

21
NOTICE
View File

@ -1,11 +1,10 @@
Harbor 0.1.0 Beta NOTICE
Copyright (c) 2016 VMware, Inc. All Rights Reserved. Harbor version 0.4.0 Beta
This product is licensed to you under the Apache License, Version 2.0 (the "License"). Copyright (c) 2016 VMware, Inc. All Rights Reserved.
You may not use this product except in compliance with the License.
This product is licensed to you under the Apache License, Version 2.0 (the "License"). You may not use this product except in compliance with the License.
This product may include a number of subcomponents with
separate copyright notices and license terms. Your use of the source This product may include a number of subcomponents with separate copyright notices and license terms. Your use of these subcomponents is subject to the terms and conditions of the subcomponent's license, as noted in the LICENSE file.
code for the these subcomponents is subject to the terms and
conditions of the subcomponent's license, as noted in the LICENSE file.

View File

@ -1,6 +1,7 @@
# Harbor # Harbor
[![Build Status](https://travis-ci.org/vmware/harbor.svg?branch=master)](https://travis-ci.org/vmware/harbor) [![Build Status](https://travis-ci.org/vmware/harbor.svg?branch=master)](https://travis-ci.org/vmware/harbor)
[![Coverage Status](https://coveralls.io/repos/github/vmware/harbor/badge.svg?branch=dev)](https://coveralls.io/github/vmware/harbor?branch=dev)
<img alt="Harbor" src="docs/img/harbor_logo.png"> <img alt="Harbor" src="docs/img/harbor_logo.png">

9
api/api_test.go Normal file
View File

@ -0,0 +1,9 @@
package api
import (
"testing"
)
func TestMain(t *testing.T) {
}

View File

@ -31,6 +31,11 @@ import (
"github.com/astaxie/beego" "github.com/astaxie/beego"
) )
const (
defaultPageSize int64 = 10
maxPageSize int64 = 100
)
// BaseAPI wraps common methods for controllers to host API // BaseAPI wraps common methods for controllers to host API
type BaseAPI struct { type BaseAPI struct {
beego.Controller beego.Controller
@ -151,6 +156,60 @@ func (b *BaseAPI) GetIDFromURL() int64 {
return id return id
} }
// set "Link" and "X-Total-Count" header for pagination request
func (b *BaseAPI) setPaginationHeader(total, page, pageSize int64) {
b.Ctx.ResponseWriter.Header().Set("X-Total-Count", strconv.FormatInt(total, 10))
link := ""
// set previous link
if page > 1 && (page-1)*pageSize <= total {
u := *(b.Ctx.Request.URL)
q := u.Query()
q.Set("page", strconv.FormatInt(page-1, 10))
u.RawQuery = q.Encode()
if len(link) != 0 {
link += ", "
}
link += fmt.Sprintf("<%s>; rel=\"prev\"", u.String())
}
// set next link
if pageSize*page < total {
u := *(b.Ctx.Request.URL)
q := u.Query()
q.Set("page", strconv.FormatInt(page+1, 10))
u.RawQuery = q.Encode()
if len(link) != 0 {
link += ", "
}
link += fmt.Sprintf("<%s>; rel=\"next\"", u.String())
}
if len(link) != 0 {
b.Ctx.ResponseWriter.Header().Set("Link", link)
}
}
func (b *BaseAPI) getPaginationParams() (page, pageSize int64) {
page, err := b.GetInt64("page", 1)
if err != nil || page <= 0 {
b.CustomAbort(http.StatusBadRequest, "invalid page")
}
pageSize, err = b.GetInt64("page_size", defaultPageSize)
if err != nil || pageSize <= 0 {
b.CustomAbort(http.StatusBadRequest, "invalid page_size")
}
if pageSize > maxPageSize {
pageSize = maxPageSize
log.Debugf("the parameter page_size %d exceeds the max %d, set it to max", pageSize, maxPageSize)
}
return page, pageSize
}
func getIsInsecure() bool { func getIsInsecure() bool {
insecure := false insecure := false

106
api/dataprepare_test.go Normal file
View File

@ -0,0 +1,106 @@
/*
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 api
import (
"github.com/vmware/harbor/dao"
"github.com/vmware/harbor/models"
"os"
)
const (
//Prepare Test info
TestUserName = "testUser0001"
TestUserPwd = "testUser0001"
TestUserEmail = "testUser0001@mydomain.com"
TestProName = "testProject0001"
TestTargetName = "testTarget0001"
)
func CommonAddUser() {
commonUser := models.User{
Username: TestUserName,
Email: TestUserPwd,
Password: TestUserEmail,
}
_, _ = dao.Register(commonUser)
}
func CommonGetUserID() int {
queryUser := &models.User{
Username: TestUserName,
}
commonUser, _ := dao.GetUser(*queryUser)
return commonUser.UserID
}
func CommonDelUser() {
queryUser := &models.User{
Username: TestUserName,
}
commonUser, _ := dao.GetUser(*queryUser)
_ = dao.DeleteUser(commonUser.UserID)
}
func CommonAddProject() {
queryUser := &models.User{
Username: "admin",
}
adminUser, _ := dao.GetUser(*queryUser)
commonProject := &models.Project{
Name: TestProName,
OwnerID: adminUser.UserID,
}
_, _ = dao.AddProject(*commonProject)
}
func CommonDelProject() {
commonProject, _ := dao.GetProjectByName(TestProName)
_ = dao.DeleteProject(commonProject.ProjectID)
}
func CommonAddTarget() {
endPoint := os.Getenv("REGISTRY_URL")
commonTarget := &models.RepTarget{
URL: endPoint,
Name: TestTargetName,
Username: adminName,
Password: adminPwd,
}
_, _ = dao.AddRepTarget(*commonTarget)
}
func CommonGetTarget() int {
target, _ := dao.GetRepTargetByName(TestTargetName)
return int(target.ID)
}
func CommonDelTarget() {
target, _ := dao.GetRepTargetByName(TestTargetName)
_ = dao.DeleteRepTarget(target.ID)
}
func CommonPolicyEabled(policyID int, enabled int) {
_ = dao.UpdateRepPolicyEnablement(int64(policyID), enabled)
}

853
api/harborapi_test.go Normal file
View File

@ -0,0 +1,853 @@
//These APIs provide services for manipulating Harbor project.
package api
import (
"encoding/json"
"fmt"
"github.com/vmware/harbor/dao"
"github.com/vmware/harbor/models"
"github.com/vmware/harbor/tests/apitests/apilib"
"io/ioutil"
"net/http/httptest"
"path/filepath"
"runtime"
"github.com/astaxie/beego"
"github.com/dghubble/sling"
//for test env prepare
_ "github.com/vmware/harbor/auth/db"
_ "github.com/vmware/harbor/auth/ldap"
)
const (
jsonAcceptHeader = "application/json"
testAcceptHeader = "text/plain"
adminName = "admin"
adminPwd = "Harbor12345"
)
var admin, unknownUsr, testUser *usrInfo
type api struct {
basePath string
}
func newHarborAPI() *api {
return &api{
basePath: "",
}
}
func newHarborAPIWithBasePath(basePath string) *api {
return &api{
basePath: basePath,
}
}
type usrInfo struct {
Name string
Passwd string
}
func init() {
dao.InitDB()
_, file, _, _ := runtime.Caller(1)
apppath, _ := filepath.Abs(filepath.Dir(filepath.Join(file, ".."+string(filepath.Separator))))
beego.BConfig.WebConfig.Session.SessionOn = true
beego.TestBeegoInit(apppath)
beego.Router("/api/search/", &SearchAPI{})
beego.Router("/api/projects/", &ProjectAPI{}, "get:List;post:Post;head:Head")
beego.Router("/api/projects/:id", &ProjectAPI{}, "delete:Delete;get:Get")
beego.Router("/api/users/?:id", &UserAPI{})
beego.Router("/api/users/:id([0-9]+)/password", &UserAPI{}, "put:ChangePassword")
beego.Router("/api/users/:id/sysadmin", &UserAPI{}, "put:ToggleUserAdminRole")
beego.Router("/api/projects/:id/publicity", &ProjectAPI{}, "put:ToggleProjectPublic")
beego.Router("/api/projects/:id([0-9]+)/logs/filter", &ProjectAPI{}, "post:FilterAccessLog")
beego.Router("/api/projects/:pid([0-9]+)/members/?:mid", &ProjectMemberAPI{}, "get:Get;post:Post;delete:Delete;put:Put")
beego.Router("/api/statistics", &StatisticAPI{})
beego.Router("/api/users/?:id", &UserAPI{})
beego.Router("/api/logs", &LogAPI{})
beego.Router("/api/repositories", &RepositoryAPI{})
beego.Router("/api/repositories/tags", &RepositoryAPI{}, "get:GetTags")
beego.Router("/api/repositories/manifests", &RepositoryAPI{}, "get:GetManifests")
beego.Router("/api/repositories/top", &RepositoryAPI{}, "get:GetTopRepos")
beego.Router("/api/targets/", &TargetAPI{}, "get:List")
beego.Router("/api/targets/", &TargetAPI{}, "post:Post")
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/policies/replication/:id([0-9]+)", &RepPolicyAPI{})
beego.Router("/api/policies/replication", &RepPolicyAPI{}, "get:List")
beego.Router("/api/policies/replication", &RepPolicyAPI{}, "post:Post;delete:Delete")
beego.Router("/api/policies/replication/:id([0-9]+)/enablement", &RepPolicyAPI{}, "put:UpdateEnablement")
_ = updateInitPassword(1, "Harbor12345")
//Init user Info
admin = &usrInfo{adminName, adminPwd}
unknownUsr = &usrInfo{"unknown", "unknown"}
testUser = &usrInfo{TestUserName, TestUserPwd}
}
func request(_sling *sling.Sling, acceptHeader string, authInfo ...usrInfo) (int, []byte, error) {
_sling = _sling.Set("Accept", acceptHeader)
req, err := _sling.Request()
if err != nil {
return 400, nil, err
}
if len(authInfo) > 0 {
req.SetBasicAuth(authInfo[0].Name, authInfo[0].Passwd)
}
w := httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, req)
body, err := ioutil.ReadAll(w.Body)
return w.Code, body, err
}
//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 api) SearchGet (q string) (apilib.Search, error) {
func (a api) SearchGet(q string) (apilib.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,omitempty"`
}
_sling = _sling.QueryStruct(&QueryParams{Query: q})
_, body, err := request(_sling, jsonAcceptHeader)
var successPayload = new(apilib.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 api) ProjectsPost (prjUsr usrInfo, project apilib.Project) (int, error) {
func (a api) ProjectsPost(prjUsr usrInfo, project apilib.ProjectReq) (int, error) {
_sling := sling.New().Post(a.basePath)
// create path and map variables
path := "/api/projects/"
_sling = _sling.Path(path)
// body params
_sling = _sling.BodyJSON(project)
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, prjUsr)
return httpStatusCode, err
}
func (a api) StatisticGet(user usrInfo) (apilib.StatisticMap, error) {
_sling := sling.New().Get(a.basePath)
// create path and map variables
path := "/api/statistics/"
fmt.Printf("project statistic path: %s\n", path)
_sling = _sling.Path(path)
var successPayload = new(apilib.StatisticMap)
code, body, err := request(_sling, jsonAcceptHeader, user)
if 200 == code && nil == err {
err = json.Unmarshal(body, &successPayload)
}
return *successPayload, err
}
func (a api) LogGet(user usrInfo, startTime, endTime, lines string) (int, []apilib.AccessLog, error) {
_sling := sling.New().Get(a.basePath)
// create path and map variables
path := "/api/logs/"
fmt.Printf("logs path: %s\n", path)
_sling = _sling.Path(path)
type QueryParams struct {
StartTime string `url:"start_time,omitempty"`
EndTime string `url:"end_time,omitempty"`
Lines string `url:"lines,omitempty"`
}
_sling = _sling.QueryStruct(&QueryParams{StartTime: startTime, EndTime: endTime, Lines: lines})
var successPayload []apilib.AccessLog
code, body, err := request(_sling, jsonAcceptHeader, user)
if 200 == code && nil == err {
err = json.Unmarshal(body, &successPayload)
}
return code, successPayload, err
}
////Delete a repository or a tag in a repository.
////Delete a repository or a tag in a repository.
////This endpoint let user delete repositories and tags with repo name and tag.\n
////@param repoName The name of repository which will be deleted.
////@param tag Tag of a repository.
////@return void
////func (a api) RepositoriesDelete(prjUsr UsrInfo, repoName string, tag string) (int, error) {
//func (a api) RepositoriesDelete(prjUsr UsrInfo, repoName string, tag string) (int, error) {
// _sling := sling.New().Delete(a.basePath)
// // create path and map variables
// path := "/api/repositories"
// _sling = _sling.Path(path)
// type QueryParams struct {
// RepoName string `url:"repo_name,omitempty"`
// Tag string `url:"tag,omitempty"`
// }
// _sling = _sling.QueryStruct(&QueryParams{RepoName: repoName, Tag: tag})
// // 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()
// req.SetBasicAuth(prjUsr.Name, prjUsr.Passwd)
// //fmt.Printf("request %+v", req)
// client := &http.Client{}
// httpResponse, err := client.Do(req)
// defer httpResponse.Body.Close()
// if err != nil {
// // handle error
// }
// return httpResponse.StatusCode, err
//}
//Delete project by projectID
func (a api) ProjectsDelete(prjUsr usrInfo, projectID string) (int, error) {
_sling := sling.New().Delete(a.basePath)
//create api path
path := "api/projects/" + projectID
_sling = _sling.Path(path)
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, prjUsr)
return httpStatusCode, err
}
//Check if the project name user provided already exists
func (a api) ProjectsHead(prjUsr usrInfo, projectName string) (int, error) {
_sling := sling.New().Head(a.basePath)
//create api path
path := "api/projects"
_sling = _sling.Path(path)
type QueryParams struct {
ProjectName string `url:"project_name,omitempty"`
}
_sling = _sling.QueryStruct(&QueryParams{ProjectName: projectName})
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, prjUsr)
return httpStatusCode, err
}
//Return specific project detail infomation
func (a api) ProjectsGetByPID(projectID string) (int, apilib.Project, error) {
_sling := sling.New().Get(a.basePath)
//create api path
path := "api/projects/" + projectID
_sling = _sling.Path(path)
var successPayload apilib.Project
httpStatusCode, body, err := request(_sling, jsonAcceptHeader)
if err == nil && httpStatusCode == 200 {
err = json.Unmarshal(body, &successPayload)
}
return httpStatusCode, successPayload, err
}
//Search projects by projectName and isPublic
func (a api) ProjectsGet(projectName string, isPublic int32) (int, []apilib.Project, error) {
_sling := sling.New().Get(a.basePath)
//create api path
path := "api/projects"
_sling = _sling.Path(path)
type QueryParams struct {
ProjectName string `url:"project_name,omitempty"`
IsPubilc int32 `url:"is_public,omitempty"`
}
_sling = _sling.QueryStruct(&QueryParams{ProjectName: projectName, IsPubilc: isPublic})
var successPayload []apilib.Project
httpStatusCode, body, err := request(_sling, jsonAcceptHeader)
if err == nil && httpStatusCode == 200 {
err = json.Unmarshal(body, &successPayload)
}
return httpStatusCode, successPayload, err
}
//Update properties for a selected project.
func (a api) ToggleProjectPublicity(prjUsr usrInfo, projectID string, ispublic int32) (int, error) {
// create path and map variables
path := "/api/projects/" + projectID + "/publicity/"
_sling := sling.New().Put(a.basePath)
_sling = _sling.Path(path)
type QueryParams struct {
Public int32 `json:"public,omitempty"`
}
_sling = _sling.BodyJSON(&QueryParams{Public: ispublic})
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, prjUsr)
return httpStatusCode, err
}
//Get access logs accompany with a relevant project.
func (a api) ProjectLogsFilter(prjUsr usrInfo, projectID string, accessLog apilib.AccessLogFilter) (int, []byte, error) {
//func (a api) ProjectLogsFilter(prjUsr usrInfo, projectID string, accessLog apilib.AccessLog) (int, apilib.AccessLog, error) {
_sling := sling.New().Post(a.basePath)
path := "/api/projects/" + projectID + "/logs/filter"
_sling = _sling.Path(path)
// body params
_sling = _sling.BodyJSON(accessLog)
//var successPayload []apilib.AccessLog
httpStatusCode, body, err := request(_sling, jsonAcceptHeader, prjUsr)
/*
if err == nil && httpStatusCode == 200 {
err = json.Unmarshal(body, &successPayload)
}
*/
return httpStatusCode, body, err
// return httpStatusCode, successPayload, err
}
//-------------------------Member Test---------------------------------------//
//Return relevant role members of projectID
func (a api) GetProjectMembersByProID(prjUsr usrInfo, projectID string) (int, []apilib.User, error) {
_sling := sling.New().Get(a.basePath)
path := "/api/projects/" + projectID + "/members/"
_sling = _sling.Path(path)
var successPayload []apilib.User
httpStatusCode, body, err := request(_sling, jsonAcceptHeader, prjUsr)
if err == nil && httpStatusCode == 200 {
err = json.Unmarshal(body, &successPayload)
}
return httpStatusCode, successPayload, err
}
//Add project role member accompany with projectID
func (a api) AddProjectMember(prjUsr usrInfo, projectID string, roles apilib.RoleParam) (int, error) {
_sling := sling.New().Post(a.basePath)
path := "/api/projects/" + projectID + "/members/"
_sling = _sling.Path(path)
_sling = _sling.BodyJSON(roles)
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, prjUsr)
return httpStatusCode, err
}
//Delete project role member accompany with projectID
func (a api) DeleteProjectMember(authInfo usrInfo, projectID string, userID string) (int, error) {
_sling := sling.New().Delete(a.basePath)
path := "/api/projects/" + projectID + "/members/" + userID
_sling = _sling.Path(path)
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, err
}
//Get role memberInfo by projectId and UserId
func (a api) GetMemByPIDUID(authInfo usrInfo, projectID string, userID string) (int, error) {
_sling := sling.New().Get(a.basePath)
path := "/api/projects/" + projectID + "/members/" + userID
_sling = _sling.Path(path)
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, err
}
//Put:update current project role members accompany with relevant project and user
func (a api) PutProjectMember(authInfo usrInfo, projectID string, userID string, roles apilib.RoleParam) (int, error) {
_sling := sling.New().Put(a.basePath)
path := "/api/projects/" + projectID + "/members/" + userID
_sling = _sling.Path(path)
_sling = _sling.BodyJSON(roles)
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, err
}
//-------------------------Repositories Test---------------------------------------//
//Return relevant repos of projectID
func (a api) GetRepos(authInfo usrInfo, projectID string) (int, error) {
_sling := sling.New().Get(a.basePath)
path := "/api/repositories/"
_sling = _sling.Path(path)
type QueryParams struct {
ProjectID string `url:"project_id"`
}
_sling = _sling.QueryStruct(&QueryParams{ProjectID: projectID})
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, err
}
//Get tags of a relevant repository
func (a api) GetReposTags(authInfo usrInfo, repoName string) (int, error) {
_sling := sling.New().Get(a.basePath)
path := "/api/repositories/tags"
_sling = _sling.Path(path)
type QueryParams struct {
RepoName string `url:"repo_name"`
}
_sling = _sling.QueryStruct(&QueryParams{RepoName: repoName})
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, err
}
//Get manifests of a relevant repository
func (a api) GetReposManifests(authInfo usrInfo, repoName string, tag string) (int, error) {
_sling := sling.New().Get(a.basePath)
path := "/api/repositories/manifests"
_sling = _sling.Path(path)
type QueryParams struct {
RepoName string `url:"repo_name"`
Tag string `url:"tag"`
}
_sling = _sling.QueryStruct(&QueryParams{RepoName: repoName, Tag: tag})
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, err
}
//Get public repositories which are accessed most
func (a api) GetReposTop(authInfo usrInfo, count string) (int, error) {
_sling := sling.New().Get(a.basePath)
path := "/api/repositories/top"
_sling = _sling.Path(path)
type QueryParams struct {
Count string `url:"count"`
}
_sling = _sling.QueryStruct(&QueryParams{Count: count})
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, err
}
//-------------------------Targets Test---------------------------------------//
//Create a new replication target
func (a api) AddTargets(authInfo usrInfo, repTarget apilib.RepTargetPost) (int, error) {
_sling := sling.New().Post(a.basePath)
path := "/api/targets"
_sling = _sling.Path(path)
_sling = _sling.BodyJSON(repTarget)
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, err
}
//List filters targets by name
func (a api) ListTargets(authInfo usrInfo, targetName string) (int, []apilib.RepTarget, error) {
_sling := sling.New().Get(a.basePath)
path := "/api/targets?name=" + targetName
_sling = _sling.Path(path)
var successPayload []apilib.RepTarget
httpStatusCode, body, err := request(_sling, jsonAcceptHeader, authInfo)
if err == nil && httpStatusCode == 200 {
err = json.Unmarshal(body, &successPayload)
}
return httpStatusCode, successPayload, err
}
//Ping target by targetID
func (a api) PingTargetsByID(authInfo usrInfo, targetID string) (int, error) {
_sling := sling.New().Post(a.basePath)
path := "/api/targets/ping?id=" + targetID
_sling = _sling.Path(path)
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, err
}
//Get target by targetID
func (a api) GetTargetByID(authInfo usrInfo, targetID string) (int, error) {
_sling := sling.New().Get(a.basePath)
path := "/api/targets/" + targetID
_sling = _sling.Path(path)
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, err
}
//Update target by targetID
func (a api) PutTargetByID(authInfo usrInfo, targetID string, repTarget apilib.RepTargetPost) (int, error) {
_sling := sling.New().Put(a.basePath)
path := "/api/targets/" + targetID
_sling = _sling.Path(path)
_sling = _sling.BodyJSON(repTarget)
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, err
}
//List the target relevant policies by targetID
func (a api) GetTargetPoliciesByID(authInfo usrInfo, targetID string) (int, error) {
_sling := sling.New().Get(a.basePath)
path := "/api/targets/" + targetID + "/policies/"
_sling = _sling.Path(path)
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, err
}
//Delete target by targetID
func (a api) DeleteTargetsByID(authInfo usrInfo, targetID string) (int, error) {
_sling := sling.New().Delete(a.basePath)
path := "/api/targets/" + targetID
_sling = _sling.Path(path)
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, err
}
//--------------------Replication_Policy Test--------------------------------//
//Create a new replication policy
func (a api) AddPolicy(authInfo usrInfo, repPolicy apilib.RepPolicyPost) (int, error) {
_sling := sling.New().Post(a.basePath)
path := "/api/policies/replication/"
_sling = _sling.Path(path)
_sling = _sling.BodyJSON(repPolicy)
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, err
}
//List policies by policyName and projectID
func (a api) ListPolicies(authInfo usrInfo, policyName string, proID string) (int, []apilib.RepPolicy, error) {
_sling := sling.New().Get(a.basePath)
path := "/api/policies/replication/"
_sling = _sling.Path(path)
type QueryParams struct {
PolicyName string `url:"name"`
ProjectID string `url:"project_id"`
}
_sling = _sling.QueryStruct(&QueryParams{PolicyName: policyName, ProjectID: proID})
var successPayload []apilib.RepPolicy
httpStatusCode, body, err := request(_sling, jsonAcceptHeader, authInfo)
if err == nil && httpStatusCode == 200 {
err = json.Unmarshal(body, &successPayload)
}
return httpStatusCode, successPayload, err
}
//Get replication policy by policyID
func (a api) GetPolicyByID(authInfo usrInfo, policyID string) (int, error) {
_sling := sling.New().Get(a.basePath)
path := "/api/policies/replication/" + policyID
_sling = _sling.Path(path)
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, err
}
//Update policyInfo by policyID
func (a api) PutPolicyInfoByID(authInfo usrInfo, policyID string, policyUpdate apilib.RepPolicyUpdate) (int, error) {
_sling := sling.New().Put(a.basePath)
path := "/api/policies/replication/" + policyID
_sling = _sling.Path(path)
_sling = _sling.BodyJSON(policyUpdate)
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, err
}
//Update policy enablement flag by policyID
func (a api) PutPolicyEnableByID(authInfo usrInfo, policyID string, policyEnable apilib.RepPolicyEnablementReq) (int, error) {
_sling := sling.New().Put(a.basePath)
path := "/api/policies/replication/" + policyID + "/enablement"
_sling = _sling.Path(path)
_sling = _sling.BodyJSON(policyEnable)
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, err
}
//Delete policy by policyID
func (a api) DeletePolicyByID(authInfo usrInfo, policyID string) (int, error) {
_sling := sling.New().Delete(a.basePath)
path := "/api/policies/replication/" + policyID
_sling = _sling.Path(path)
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, err
}
//Return projects created by Harbor
//func (a HarborApi) ProjectsGet (projectName string, isPublic int32) ([]Project, error) {
// }
//Check if the project name user provided already exists.
//func (a HarborApi) ProjectsHead (projectName string) (error) {
//}
//Get access logs accompany with a relevant project.
//func (a HarborApi) ProjectsProjectIdLogsFilterPost (projectID int32, accessLog AccessLog) ([]AccessLog, error) {
//}
//Return a project&#39;s relevant role members.
//func (a HarborApi) ProjectsProjectIdMembersGet (projectID int32) ([]Role, error) {
//}
//Add project role member accompany with relevant project and user.
//func (a HarborApi) ProjectsProjectIdMembersPost (projectID int32, roles RoleParam) (error) {
//}
//Delete project role members accompany with relevant project and user.
//func (a HarborApi) ProjectsProjectIdMembersUserIdDelete (projectID int32, userId int32) (error) {
//}
//Return role members accompany with relevant project and user.
//func (a HarborApi) ProjectsProjectIdMembersUserIdGet (projectID int32, userId int32) ([]Role, error) {
//}
//Update project role members accompany with relevant project and user.
//func (a HarborApi) ProjectsProjectIdMembersUserIdPut (projectID int32, userId int32, roles RoleParam) (error) {
//}
//Update properties for a selected project.
//func (a HarborApi) ProjectsProjectIdPut (projectID int32, project Project) (error) {
//}
//Get repositories accompany with relevant project and repo name.
//func (a HarborApi) RepositoriesGet (projectID int32, q string) ([]Repository, error) {
//}
//Get manifests of a relevant repository.
//func (a HarborApi) RepositoriesManifestGet (repoName string, tag string) (error) {
//}
//Get tags of a relevant repository.
//func (a HarborApi) RepositoriesTagsGet (repoName string) (error) {
//}
//Get registered users of Harbor.
func (a api) UsersGet(userName string, authInfo usrInfo) (int, []apilib.User, error) {
_sling := sling.New().Get(a.basePath)
// create path and map variables
path := "/api/users/"
_sling = _sling.Path(path)
// body params
type QueryParams struct {
UserName string `url:"username, omitempty"`
}
_sling = _sling.QueryStruct(&QueryParams{UserName: userName})
httpStatusCode, body, err := request(_sling, jsonAcceptHeader, authInfo)
var successPayLoad []apilib.User
if 200 == httpStatusCode && nil == err {
err = json.Unmarshal(body, &successPayLoad)
}
return httpStatusCode, successPayLoad, err
}
//Get registered users by userid.
func (a api) UsersGetByID(userName string, authInfo usrInfo, userID int) (int, apilib.User, error) {
_sling := sling.New().Get(a.basePath)
// create path and map variables
path := "/api/users/" + fmt.Sprintf("%d", userID)
_sling = _sling.Path(path)
// body params
type QueryParams struct {
UserName string `url:"username, omitempty"`
}
_sling = _sling.QueryStruct(&QueryParams{UserName: userName})
httpStatusCode, body, err := request(_sling, jsonAcceptHeader, authInfo)
var successPayLoad apilib.User
if 200 == httpStatusCode && nil == err {
err = json.Unmarshal(body, &successPayLoad)
}
return httpStatusCode, successPayLoad, err
}
//Creates a new user account.
func (a api) UsersPost(user apilib.User, authInfo ...usrInfo) (int, error) {
_sling := sling.New().Post(a.basePath)
// create path and map variables
path := "/api/users/"
_sling = _sling.Path(path)
// body params
_sling = _sling.BodyJSON(user)
var httpStatusCode int
var err error
if len(authInfo) > 0 {
httpStatusCode, _, err = request(_sling, jsonAcceptHeader, authInfo[0])
} else {
httpStatusCode, _, err = request(_sling, jsonAcceptHeader)
}
return httpStatusCode, err
}
//Update a registered user to change profile.
func (a api) UsersPut(userID int, profile apilib.UserProfile, authInfo usrInfo) (int, error) {
_sling := sling.New().Put(a.basePath)
// create path and map variables
path := "/api/users/" + fmt.Sprintf("%d", userID)
_sling = _sling.Path(path)
// body params
_sling = _sling.BodyJSON(profile)
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, err
}
//Update a registered user to be an administrator of Harbor.
func (a api) UsersToggleAdminRole(userID int, authInfo usrInfo, hasAdminRole int32) (int, error) {
_sling := sling.New().Put(a.basePath)
// create path and map variables
path := "/api/users/" + fmt.Sprintf("%d", userID) + "/sysadmin"
_sling = _sling.Path(path)
type QueryParams struct {
HasAdminRole int32 `json:"has_admin_role,omitempty"`
}
_sling = _sling.BodyJSON(&QueryParams{HasAdminRole: hasAdminRole})
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, err
}
//Update password of a registered user.
func (a api) UsersUpdatePassword(userID int, password apilib.Password, authInfo usrInfo) (int, error) {
_sling := sling.New().Put(a.basePath)
// create path and map variables
path := "/api/users/" + fmt.Sprintf("%d", userID) + "/password"
_sling = _sling.Path(path)
// body params
_sling = _sling.BodyJSON(password)
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, err
}
//Mark a registered user as be removed.
func (a api) UsersDelete(userID int, authInfo usrInfo) (int, error) {
_sling := sling.New().Delete(a.basePath)
// create path and map variables
path := "/api/users/" + fmt.Sprintf("%d", userID)
_sling = _sling.Path(path)
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, err
}
func updateInitPassword(userID int, password string) error {
queryUser := models.User{UserID: userID}
user, err := dao.GetUser(queryUser)
if err != nil {
return fmt.Errorf("Failed to get user, userID: %d %v", userID, err)
}
if user == nil {
return fmt.Errorf("User id: %d does not exist.", userID)
}
if user.Salt == "" {
salt, err := dao.GenerateRandomString()
if err != nil {
return fmt.Errorf("Failed to generate salt for encrypting password, %v", err)
}
user.Salt = salt
user.Password = password
err = dao.ChangeUserPassword(*user)
if err != nil {
return fmt.Errorf("Failed to update user encrypted password, userID: %d, err: %v", userID, err)
}
} else {
}
return nil
}

51
api/internal.go Normal file
View File

@ -0,0 +1,51 @@
/*
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 api
import (
"net/http"
"github.com/vmware/harbor/dao"
"github.com/vmware/harbor/utils/log"
)
// InternalAPI handles request of harbor admin...
type InternalAPI struct {
BaseAPI
}
// Prepare validates the URL and parms
func (ia *InternalAPI) Prepare() {
var currentUserID int
currentUserID = ia.ValidateUser()
isAdmin, err := dao.IsAdminRole(currentUserID)
if err != nil {
log.Errorf("Error occurred in IsAdminRole:%v", err)
ia.CustomAbort(http.StatusInternalServerError, "Internal error.")
}
if !isAdmin {
log.Error("Guests doesn't have the permisson to request harbor internal API.")
ia.CustomAbort(http.StatusForbidden, "Guests doesn't have the permisson to request harbor internal API.")
}
}
// SyncRegistry ...
func (ia *InternalAPI) SyncRegistry() {
err := SyncRegistry()
if err != nil {
ia.CustomAbort(http.StatusInternalServerError, "internal error")
}
}

9
api/jobs/job_test.go Normal file
View File

@ -0,0 +1,9 @@
package api
import (
"testing"
)
func TestMain(t *testing.T) {
}

View File

@ -46,6 +46,27 @@ type ReplicationReq struct {
TagList []string `json:"tags"` TagList []string `json:"tags"`
} }
// Prepare ...
func (rj *ReplicationJob) Prepare() {
rj.authenticate()
}
func (rj *ReplicationJob) authenticate() {
cookie, err := rj.Ctx.Request.Cookie(models.UISecretCookie)
if err != nil && err != http.ErrNoCookie {
log.Errorf("failed to get cookie %s: %v", models.UISecretCookie, err)
rj.CustomAbort(http.StatusInternalServerError, "")
}
if err == http.ErrNoCookie {
rj.CustomAbort(http.StatusUnauthorized, "")
}
if cookie.Value != config.UISecret() {
rj.CustomAbort(http.StatusForbidden, "")
}
}
// Post creates replication jobs according to the policy. // Post creates replication jobs according to the policy.
func (rj *ReplicationJob) Post() { func (rj *ReplicationJob) Post() {
var data ReplicationReq var data ReplicationReq

124
api/log_test.go Normal file
View File

@ -0,0 +1,124 @@
package api
import (
"fmt"
"github.com/stretchr/testify/assert"
"github.com/vmware/harbor/tests/apitests/apilib"
"strconv"
"testing"
"time"
)
func TestLogGet(t *testing.T) {
fmt.Println("Testing Log API")
assert := assert.New(t)
apiTest := newHarborAPI()
//prepare for test
var project apilib.ProjectReq
project.ProjectName = "my_project"
project.Public = 1
//add the project first.
fmt.Println("add the project first.")
reply, err := apiTest.ProjectsPost(*admin, project)
if err != nil {
t.Error("Error while creat project", err.Error())
t.Log(err)
} else {
assert.Equal(int(201), reply, "Case 2: Project creation status should be 201")
}
//case 1: right parameters, expect the right output
now := fmt.Sprintf("%v", time.Now().Unix())
statusCode, result, err := apiTest.LogGet(*admin, "0", now, "3")
if err != nil {
t.Error("Error while get log information", err.Error())
t.Log(err)
} else {
assert.Equal(1, len(result), "lines of logs should be equal")
assert.Equal(int32(1), result[0].LogId, "LogId should be equal")
assert.Equal("my_project/", result[0].RepoName, "RepoName should be equal")
assert.Equal("N/A", result[0].RepoTag, "RepoTag should be equal")
assert.Equal("create", result[0].Operation, "Operation should be equal")
}
//case 2: wrong format of start_time parameter, expect the wrong output
now = fmt.Sprintf("%v", time.Now().Unix())
statusCode, result, err = apiTest.LogGet(*admin, "ss", now, "3")
if err != nil {
t.Error("Error occured while get log information since the format of start_time parameter is not right.", err.Error())
t.Log(err)
} else {
assert.Equal(int(400), statusCode, "Http status code should be 400")
}
//case 3: wrong format of end_time parameter, expect the wrong output
statusCode, result, err = apiTest.LogGet(*admin, "0", "cc", "3")
if err != nil {
t.Error("Error occured while get log information since the format of end_time parameter is not right.", err.Error())
t.Log(err)
} else {
assert.Equal(int(400), statusCode, "Http status code should be 400")
}
//case 4: wrong format of lines parameter, expect the wrong output
now = fmt.Sprintf("%v", time.Now().Unix())
statusCode, result, err = apiTest.LogGet(*admin, "0", now, "s")
if err != nil {
t.Error("Error occured while get log information since the format of lines parameter is not right.", err.Error())
t.Log(err)
} else {
assert.Equal(int(400), statusCode, "Http status code should be 400")
}
//case 5: wrong format of lines parameter, expect the wrong output
now = fmt.Sprintf("%v", time.Now().Unix())
statusCode, result, err = apiTest.LogGet(*admin, "0", now, "-5")
if err != nil {
t.Error("Error occured while get log information since the format of lines parameter is not right.", err.Error())
t.Log(err)
} else {
assert.Equal(int(400), statusCode, "Http status code should be 400")
}
//case 6: all parameters are null, expect the right output
statusCode, result, err = apiTest.LogGet(*admin, "", "", "")
if err != nil {
t.Error("Error while get log information", err.Error())
t.Log(err)
} else {
assert.Equal(1, len(result), "lines of logs should be equal")
assert.Equal(int32(1), result[0].LogId, "LogId should be equal")
assert.Equal("my_project/", result[0].RepoName, "RepoName should be equal")
assert.Equal("N/A", result[0].RepoTag, "RepoTag should be equal")
assert.Equal("create", result[0].Operation, "Operation should be equal")
}
//get the project
var projects []apilib.Project
var addProjectID int32
httpStatusCode, projects, err := apiTest.ProjectsGet(project.ProjectName, 1)
if err != nil {
t.Error("Error while search project by proName and isPublic", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
addProjectID = projects[0].ProjectId
}
//delete the project
projectID := strconv.Itoa(int(addProjectID))
httpStatusCode, err = apiTest.ProjectsDelete(*admin, projectID)
if err != nil {
t.Error("Error while delete project", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "Case 1: Project creation status should be 200")
//t.Log(result)
}
fmt.Printf("\n")
}

View File

@ -142,13 +142,22 @@ func (pma *ProjectMemberAPI) Post() {
return return
} }
for _, rid := range req.Roles { if len(req.Roles) <= 0 || len(req.Roles) > 1 {
err = dao.AddProjectMember(projectID, userID, int(rid)) pma.CustomAbort(http.StatusBadRequest, "only one role is supported")
if err != nil { }
log.Errorf("Failed to update DB to add project user role, project id: %d, user id: %d, role id: %d", projectID, userID, rid)
pma.RenderError(http.StatusInternalServerError, "Failed to update data in database") rid := req.Roles[0]
return if !(rid == models.PROJECTADMIN ||
} rid == models.DEVELOPER ||
rid == models.GUEST) {
pma.CustomAbort(http.StatusBadRequest, "invalid role")
}
err = dao.AddProjectMember(projectID, userID, rid)
if err != nil {
log.Errorf("Failed to update DB to add project user role, project id: %d, user id: %d, role id: %d", projectID, userID, rid)
pma.RenderError(http.StatusInternalServerError, "Failed to update data in database")
return
} }
} }

189
api/member_test.go Normal file
View File

@ -0,0 +1,189 @@
package api
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/harbor/tests/apitests/apilib"
"strconv"
)
func TestMemGet(t *testing.T) {
var result []apilib.User
var httpStatusCode int
var err error
assert := assert.New(t)
apiTest := newHarborAPI()
projectID := "1"
fmt.Println("Testing Member Get API")
//-------------------case 1 : response code = 200------------------------//
httpStatusCode, result, err = apiTest.GetProjectMembersByProID(*admin, projectID)
if err != nil {
t.Error("Error whihle get members by projectID", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
assert.Equal(int(1), result[0].UserId, "User Id should be 1")
assert.Equal("admin", result[0].Username, "User name should be admin")
}
//---------case 2: Response Code=401,User need to log in first.----------//
fmt.Println("case 2: Response Code=401,User need to log in first.")
httpStatusCode, result, err = apiTest.GetProjectMembersByProID(*unknownUsr, projectID)
if err != nil {
t.Error("Error while get members by projectID", err.Error())
t.Log(err)
} else {
assert.Equal(int(401), httpStatusCode, "Case 2: Project creation status should be 401")
}
//------------case 3: Response Code=404,Project does not exist-----------//
fmt.Println("case 3: Response Code=404,Project does not exist")
projectID = "11"
httpStatusCode, result, err = apiTest.GetProjectMembersByProID(*admin, projectID)
if err != nil {
t.Error("Error while get members by projectID", err.Error())
t.Log(err)
} else {
assert.Equal(int(404), httpStatusCode, "Case 3: Project creation status should be 404")
}
fmt.Printf("\n")
}
/**
* Add project role member accompany with projectID
* role_id = 1 : ProjectAdmin
* role_id = 2 : Developer
* role_id = 3 : Guest
*/
func TestMemPost(t *testing.T) {
var httpStatusCode int
var err error
assert := assert.New(t)
apiTest := newHarborAPI()
projectID := "1"
CommonAddUser()
roles := &apilib.RoleParam{[]int32{1}, TestUserName}
fmt.Printf("Add User \"%s\" successfully!\n", TestUserName)
fmt.Println("Testing Member Post API")
//-------------------case 1 : response code = 200------------------------//
fmt.Println("case 1: response code = 200")
httpStatusCode, err = apiTest.AddProjectMember(*admin, projectID, *roles)
if err != nil {
t.Error("Error whihle add project role member", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
}
//---------case 2: Response Code=409,User is ready in project.----------//
fmt.Println("case 2: Response Code=409,User is ready in project.")
httpStatusCode, err = apiTest.AddProjectMember(*admin, projectID, *roles)
if err != nil {
t.Error("Error while add project role member", err.Error())
t.Log(err)
} else {
assert.Equal(int(409), httpStatusCode, "Case 2: httpStatusCode should be 409")
}
//---------case 3: Response Code=404,User does not exist.----------//
fmt.Println("case 3: Response Code=404,User does not exist.")
errorRoles := &apilib.RoleParam{[]int32{1}, "T"}
httpStatusCode, err = apiTest.AddProjectMember(*admin, projectID, *errorRoles)
if err != nil {
t.Error("Error while add project role member", err.Error())
t.Log(err)
} else {
assert.Equal(int(404), httpStatusCode, "Case 3: httpStatusCode status should be 404")
}
/*
//---------case 4: Response Code=403,User in session does not have permission to the project..----------//
fmt.Println("case 4:User in session does not have permission to the project.")
httpStatusCode, err = apiTest.AddProjectMember(*testUser, projectID, *roles)
if err != nil {
t.Error("Error while add project role member", err.Error())
t.Log(err)
} else {
assert.Equal(int(403), httpStatusCode, "Case 3: httpStatusCode status should be 403")
}
*/
}
func TestGetMemByPIDUID(t *testing.T) {
var httpStatusCode int
var err error
assert := assert.New(t)
apiTest := newHarborAPI()
projectID := "1"
userID := strconv.Itoa(CommonGetUserID())
fmt.Println("Testing Member Get API by PID and UID")
//-------------------case 1 : response code = 200------------------------//
fmt.Println("case 1: response code = 200")
httpStatusCode, err = apiTest.GetMemByPIDUID(*admin, projectID, userID)
if err != nil {
t.Error("Error whihle get project role member", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
}
}
func TestPutMem(t *testing.T) {
var httpStatusCode int
var err error
assert := assert.New(t)
apiTest := newHarborAPI()
projectID := "1"
userID := strconv.Itoa(CommonGetUserID())
roles := &apilib.RoleParam{[]int32{3}, TestUserName}
fmt.Println("Testing Member Put API")
//-------------------case 1 : response code = 200------------------------//
fmt.Println("case 1: response code = 200")
httpStatusCode, err = apiTest.PutProjectMember(*admin, projectID, userID, *roles)
if err != nil {
t.Error("Error whihle put project role member", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
}
}
func TestDeleteMemUser(t *testing.T) {
var httpStatusCode int
var err error
assert := assert.New(t)
apiTest := newHarborAPI()
projectID := "1"
fmt.Println("Testing Member Delete API")
//-------------------case 1 : response code = 200------------------------//
fmt.Println("case 1: response code = 200")
id := strconv.Itoa(CommonGetUserID())
httpStatusCode, err = apiTest.DeleteProjectMember(*admin, projectID, id)
if err != nil {
t.Error("Error whihle add project role member", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
}
CommonDelUser()
}

View File

@ -31,13 +31,14 @@ import (
// ProjectAPI handles request to /api/projects/{} /api/projects/{}/logs // ProjectAPI handles request to /api/projects/{} /api/projects/{}/logs
type ProjectAPI struct { type ProjectAPI struct {
BaseAPI BaseAPI
userID int userID int
projectID int64 projectID int64
projectName string
} }
type projectReq struct { type projectReq struct {
ProjectName string `json:"project_name"` ProjectName string `json:"project_name"`
Public bool `json:"public"` Public int `json:"public"`
} }
const projectNameMaxLen int = 30 const projectNameMaxLen int = 30
@ -54,14 +55,16 @@ func (p *ProjectAPI) Prepare() {
log.Errorf("Error parsing project id: %s, error: %v", idStr, err) log.Errorf("Error parsing project id: %s, error: %v", idStr, err)
p.CustomAbort(http.StatusBadRequest, "invalid project id") p.CustomAbort(http.StatusBadRequest, "invalid project id")
} }
exist, err := dao.ProjectExists(p.projectID)
project, err := dao.GetProjectByID(p.projectID)
if err != nil { if err != nil {
log.Errorf("Error occurred in ProjectExists, error: %v", err) log.Errorf("failed to get project %d: %v", p.projectID, err)
p.CustomAbort(http.StatusInternalServerError, "Internal error.") p.CustomAbort(http.StatusInternalServerError, "Internal error.")
} }
if !exist { if project == nil {
p.CustomAbort(http.StatusNotFound, fmt.Sprintf("project does not exist, id: %v", p.projectID)) p.CustomAbort(http.StatusNotFound, fmt.Sprintf("project does not exist, id: %v", p.projectID))
} }
p.projectName = project.Name
} }
} }
@ -70,11 +73,8 @@ func (p *ProjectAPI) Post() {
p.userID = p.ValidateUser() p.userID = p.ValidateUser()
var req projectReq var req projectReq
var public int
p.DecodeJSONReq(&req) p.DecodeJSONReq(&req)
if req.Public { public := req.Public
public = 1
}
err := validateProjectReq(req) err := validateProjectReq(req)
if err != nil { if err != nil {
log.Errorf("Invalid project request, error: %v", err) log.Errorf("Invalid project request, error: %v", err)
@ -152,15 +152,82 @@ func (p *ProjectAPI) Get() {
p.ServeJSON() p.ServeJSON()
} }
// Delete ...
func (p *ProjectAPI) Delete() {
if p.projectID == 0 {
p.CustomAbort(http.StatusBadRequest, "project ID is required")
}
userID := p.ValidateUser()
if !hasProjectAdminRole(userID, p.projectID) {
p.CustomAbort(http.StatusForbidden, "")
}
contains, err := projectContainsRepo(p.projectName)
if err != nil {
log.Errorf("failed to check whether project %s contains any repository: %v", p.projectName, err)
p.CustomAbort(http.StatusInternalServerError, "")
}
if contains {
p.CustomAbort(http.StatusPreconditionFailed, "project contains repositores, can not be deleted")
}
contains, err = projectContainsPolicy(p.projectID)
if err != nil {
log.Errorf("failed to check whether project %s contains any policy: %v", p.projectName, err)
p.CustomAbort(http.StatusInternalServerError, "")
}
if contains {
p.CustomAbort(http.StatusPreconditionFailed, "project contains policies, can not be deleted")
}
if err = dao.DeleteProject(p.projectID); err != nil {
log.Errorf("failed to delete project %d: %v", p.projectID, err)
p.CustomAbort(http.StatusInternalServerError, "")
}
go func() {
if err := dao.AddAccessLog(models.AccessLog{
UserID: userID,
ProjectID: p.projectID,
RepoName: p.projectName,
Operation: "delete",
}); err != nil {
log.Errorf("failed to add access log: %v", err)
}
}()
}
func projectContainsRepo(name string) (bool, error) {
repositories, err := getReposByProject(name)
if err != nil {
return false, err
}
return len(repositories) > 0, nil
}
func projectContainsPolicy(id int64) (bool, error) {
policies, err := dao.GetRepPolicyByProject(id)
if err != nil {
return false, err
}
return len(policies) > 0, nil
}
// List ... // List ...
func (p *ProjectAPI) List() { func (p *ProjectAPI) List() {
var projectList []models.Project var total int64
projectName := p.GetString("project_name")
if len(projectName) > 0 {
projectName = "%" + projectName + "%"
}
var public int var public int
var err error var err error
page, pageSize := p.getPaginationParams()
var projectList []models.Project
projectName := p.GetString("project_name")
isPublic := p.GetString("is_public") isPublic := p.GetString("is_public")
if len(isPublic) > 0 { if len(isPublic) > 0 {
public, err = strconv.Atoi(isPublic) public, err = strconv.Atoi(isPublic)
@ -171,7 +238,16 @@ func (p *ProjectAPI) List() {
} }
isAdmin := false isAdmin := false
if public == 1 { if public == 1 {
projectList, err = dao.GetPublicProjects(projectName) total, err = dao.GetTotalOfProjects(projectName, 1)
if err != nil {
log.Errorf("failed to get total of projects: %v", err)
p.CustomAbort(http.StatusInternalServerError, "")
}
projectList, err = dao.GetProjects(projectName, 1, pageSize, pageSize*(page-1))
if err != nil {
log.Errorf("failed to get projects: %v", err)
p.CustomAbort(http.StatusInternalServerError, "")
}
} else { } else {
//if the request is not for public projects, user must login or provide credential //if the request is not for public projects, user must login or provide credential
p.userID = p.ValidateUser() p.userID = p.ValidateUser()
@ -181,15 +257,30 @@ func (p *ProjectAPI) List() {
p.CustomAbort(http.StatusInternalServerError, "Internal error.") p.CustomAbort(http.StatusInternalServerError, "Internal error.")
} }
if isAdmin { if isAdmin {
projectList, err = dao.GetAllProjects(projectName) total, err = dao.GetTotalOfProjects(projectName)
if err != nil {
log.Errorf("failed to get total of projects: %v", err)
p.CustomAbort(http.StatusInternalServerError, "")
}
projectList, err = dao.GetProjects(projectName, pageSize, pageSize*(page-1))
if err != nil {
log.Errorf("failed to get projects: %v", err)
p.CustomAbort(http.StatusInternalServerError, "")
}
} else { } else {
projectList, err = dao.GetUserRelevantProjects(p.userID, projectName) total, err = dao.GetTotalOfUserRelevantProjects(p.userID, projectName)
if err != nil {
log.Errorf("failed to get total of projects: %v", err)
p.CustomAbort(http.StatusInternalServerError, "")
}
projectList, err = dao.GetUserRelevantProjects(p.userID, projectName, pageSize, pageSize*(page-1))
if err != nil {
log.Errorf("failed to get projects: %v", err)
p.CustomAbort(http.StatusInternalServerError, "")
}
} }
} }
if err != nil {
log.Errorf("Error occured in get projects info, error: %v", err)
p.CustomAbort(http.StatusInternalServerError, "Internal error.")
}
for i := 0; i < len(projectList); i++ { for i := 0; i < len(projectList); i++ {
if public != 1 { if public != 1 {
if isAdmin { if isAdmin {
@ -199,8 +290,17 @@ func (p *ProjectAPI) List() {
projectList[i].Togglable = true projectList[i].Togglable = true
} }
} }
projectList[i].RepoCount = getRepoCountByProject(projectList[i].Name)
repos, err := dao.GetRepositoryByProjectName(projectList[i].Name)
if err != nil {
log.Errorf("failed to get repositories of project %s: %v", projectList[i].Name, err)
p.CustomAbort(http.StatusInternalServerError, "")
}
projectList[i].RepoCount = len(repos)
} }
p.setPaginationHeader(total, page, pageSize)
p.Data["json"] = projectList p.Data["json"] = projectList
p.ServeJSON() p.ServeJSON()
} }
@ -209,7 +309,6 @@ func (p *ProjectAPI) List() {
func (p *ProjectAPI) ToggleProjectPublic() { func (p *ProjectAPI) ToggleProjectPublic() {
p.userID = p.ValidateUser() p.userID = p.ValidateUser()
var req projectReq var req projectReq
var public int
projectID, err := strconv.ParseInt(p.Ctx.Input.Param(":id"), 10, 64) projectID, err := strconv.ParseInt(p.Ctx.Input.Param(":id"), 10, 64)
if err != nil { if err != nil {
@ -219,9 +318,7 @@ func (p *ProjectAPI) ToggleProjectPublic() {
} }
p.DecodeJSONReq(&req) p.DecodeJSONReq(&req)
if req.Public { public := req.Public
public = 1
}
if !isProjectAdmin(p.userID, projectID) { if !isProjectAdmin(p.userID, projectID) {
log.Warningf("Current user, id: %d does not have project admin role for project, id: %d", p.userID, projectID) log.Warningf("Current user, id: %d does not have project admin role for project, id: %d", p.userID, projectID)
p.RenderError(http.StatusForbidden, "") p.RenderError(http.StatusForbidden, "")
@ -238,25 +335,35 @@ func (p *ProjectAPI) ToggleProjectPublic() {
func (p *ProjectAPI) FilterAccessLog() { func (p *ProjectAPI) FilterAccessLog() {
p.userID = p.ValidateUser() p.userID = p.ValidateUser()
var filter models.AccessLog var query models.AccessLog
p.DecodeJSONReq(&filter) p.DecodeJSONReq(&query)
username := filter.Username if !checkProjectPermission(p.userID, p.projectID) {
keywords := filter.Keywords log.Warningf("Current user, user id: %d does not have permission to read accesslog of project, id: %d", p.userID, p.projectID)
p.RenderError(http.StatusForbidden, "")
beginTime := time.Unix(filter.BeginTimestamp, 0) return
endTime := time.Unix(filter.EndTimestamp, 0)
query := models.AccessLog{ProjectID: p.projectID, Username: "%" + username + "%", Keywords: keywords, BeginTime: beginTime, BeginTimestamp: filter.BeginTimestamp, EndTime: endTime, EndTimestamp: filter.EndTimestamp}
log.Infof("Query AccessLog: begin: %v, end: %v, keywords: %s", query.BeginTime, query.EndTime, query.Keywords)
accessLogList, err := dao.GetAccessLogs(query)
if err != nil {
log.Errorf("Error occurred in GetAccessLogs, error: %v", err)
p.CustomAbort(http.StatusInternalServerError, "Internal error.")
} }
p.Data["json"] = accessLogList query.ProjectID = p.projectID
query.BeginTime = time.Unix(query.BeginTimestamp, 0)
query.EndTime = time.Unix(query.EndTimestamp, 0)
page, pageSize := p.getPaginationParams()
total, err := dao.GetTotalOfAccessLogs(query)
if err != nil {
log.Errorf("failed to get total of access log: %v", err)
p.CustomAbort(http.StatusInternalServerError, "")
}
logs, err := dao.GetAccessLogs(query, pageSize, pageSize*(page-1))
if err != nil {
log.Errorf("failed to get access log: %v", err)
p.CustomAbort(http.StatusInternalServerError, "")
}
p.setPaginationHeader(total, page, pageSize)
p.Data["json"] = logs
p.ServeJSON() p.ServeJSON()
} }

317
api/project_test.go Normal file
View File

@ -0,0 +1,317 @@
package api
import (
"fmt"
"github.com/stretchr/testify/assert"
"github.com/vmware/harbor/tests/apitests/apilib"
"strconv"
"testing"
"time"
)
var addProject *apilib.ProjectReq
var addPID int
func InitAddPro() {
addProject = &apilib.ProjectReq{"test_project", 1}
}
func TestAddProject(t *testing.T) {
fmt.Println("\nTesting Add Project(ProjectsPost) API")
assert := assert.New(t)
apiTest := newHarborAPI()
//prepare for test
InitAddPro()
//case 1: admin not login, expect project creation fail.
result, err := apiTest.ProjectsPost(*unknownUsr, *addProject)
if err != nil {
t.Error("Error while creat project", err.Error())
t.Log(err)
} else {
assert.Equal(int(401), result, "Case 1: Project creation status should be 401")
//t.Log(result)
}
//case 2: admin successful login, expect project creation success.
fmt.Println("case 2: admin successful login, expect project creation success.")
result, err = apiTest.ProjectsPost(*admin, *addProject)
if err != nil {
t.Error("Error while creat project", err.Error())
t.Log(err)
} else {
assert.Equal(int(201), result, "Case 2: Project creation status should be 201")
//t.Log(result)
}
//case 3: duplicate project name, create project fail
fmt.Println("case 3: duplicate project name, create project fail")
result, err = apiTest.ProjectsPost(*admin, *addProject)
if err != nil {
t.Error("Error while creat project", err.Error())
t.Log(err)
} else {
assert.Equal(int(409), result, "Case 3: Project creation status should be 409")
//t.Log(result)
}
//case 4: reponse code = 400 : Project name is illegal in length
fmt.Println("case 4 : reponse code = 400 : Project name is illegal in length ")
result, err = apiTest.ProjectsPost(*admin, apilib.ProjectReq{"t", 1})
if err != nil {
t.Error("Error while creat project", err.Error())
t.Log(err)
} else {
assert.Equal(int(400), result, "case 4 : reponse code = 400 : Project name is illegal in length ")
}
fmt.Printf("\n")
}
//Get project by proName
func TestProGetByName(t *testing.T) {
fmt.Println("\nTest for Project GET API by project name")
assert := assert.New(t)
apiTest := newHarborAPI()
var result []apilib.Project
//----------------------------case 1 : Response Code=200----------------------------//
fmt.Println("case 1: respose code:200")
httpStatusCode, result, err := apiTest.ProjectsGet(addProject.ProjectName, 1)
if err != nil {
t.Error("Error while search project by proName and isPublic", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
assert.Equal(addProject.ProjectName, result[0].ProjectName, "Project name is wrong")
assert.Equal(int32(1), result[0].Public, "Public is wrong")
//find add projectID
addPID = int(result[0].ProjectId)
}
//----------------------------case 2 : Response Code=401:is_public=0----------------------------//
fmt.Println("case 2: respose code:401,isPublic = 0")
httpStatusCode, result, err = apiTest.ProjectsGet("library", 0)
if err != nil {
t.Error("Error while search project by proName and isPublic", err.Error())
t.Log(err)
} else {
assert.Equal(int(401), httpStatusCode, "httpStatusCode should be 200")
}
fmt.Printf("\n")
}
//Get project by proID
func TestProGetByID(t *testing.T) {
fmt.Println("\nTest for Project GET API by project id")
assert := assert.New(t)
apiTest := newHarborAPI()
var result apilib.Project
projectID := strconv.Itoa(addPID)
//----------------------------case 1 : Response Code=200----------------------------//
fmt.Println("case 1: respose code:200")
httpStatusCode, result, err := apiTest.ProjectsGetByPID(projectID)
if err != nil {
t.Error("Error while search project by proID", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
assert.Equal(addProject.ProjectName, result.ProjectName, "ProjectName is wrong")
assert.Equal(int32(1), result.Public, "Public is wrong")
}
fmt.Printf("\n")
}
func TestDeleteProject(t *testing.T) {
fmt.Println("\nTesting Delete Project(ProjectsPost) API")
assert := assert.New(t)
apiTest := newHarborAPI()
projectID := strconv.Itoa(addPID)
//--------------------------case 1: Response Code=401,User need to log in first.-----------------------//
fmt.Println("case 1: Response Code=401,User need to log in first.")
httpStatusCode, err := apiTest.ProjectsDelete(*unknownUsr, projectID)
if err != nil {
t.Error("Error while delete project", err.Error())
t.Log(err)
} else {
assert.Equal(int(401), httpStatusCode, "Case 1: Project creation status should be 401")
}
//--------------------------case 2: Response Code=200---------------------------------//
fmt.Println("case2: respose code:200")
httpStatusCode, err = apiTest.ProjectsDelete(*admin, projectID)
if err != nil {
t.Error("Error while delete project", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "Case 2: Project creation status should be 200")
}
//--------------------------case 3: Response Code=404,Project does not exist---------------------------------//
fmt.Println("case 3: Response Code=404,Project does not exist")
projectID = "11"
httpStatusCode, err = apiTest.ProjectsDelete(*admin, projectID)
if err != nil {
t.Error("Error while delete project", err.Error())
t.Log(err)
} else {
assert.Equal(int(404), httpStatusCode, "Case 3: Project creation status should be 404")
}
//--------------------------case 4: Response Code=400,Invalid project id.---------------------------------//
fmt.Println("case 4: Response Code=400,Invalid project id.")
projectID = "cc"
httpStatusCode, err = apiTest.ProjectsDelete(*admin, projectID)
if err != nil {
t.Error("Error while delete project", err.Error())
t.Log(err)
} else {
assert.Equal(int(400), httpStatusCode, "Case 4: Project creation status should be 400")
}
fmt.Printf("\n")
}
func TestProHead(t *testing.T) {
fmt.Println("\nTest for Project HEAD API")
assert := assert.New(t)
apiTest := newHarborAPI()
//----------------------------case 1 : Response Code=200----------------------------//
fmt.Println("case 1: respose code:200")
httpStatusCode, err := apiTest.ProjectsHead(*admin, "library")
if err != nil {
t.Error("Error while search project by proName", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
}
//----------------------------case 2 : Response Code=404:Project name does not exist.----------------------------//
fmt.Println("case 2: respose code:404,Project name does not exist.")
httpStatusCode, err = apiTest.ProjectsHead(*admin, "libra")
if err != nil {
t.Error("Error while search project by proName", err.Error())
t.Log(err)
} else {
assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404")
}
//----------------------------case 3 : Response Code=401:User need to log in first..----------------------------//
fmt.Println("case 3: respose code:401,User need to log in first..")
httpStatusCode, err = apiTest.ProjectsHead(*unknownUsr, "libra")
if err != nil {
t.Error("Error while search project by proName", err.Error())
t.Log(err)
} else {
assert.Equal(int(401), httpStatusCode, "httpStatusCode should be 401")
}
fmt.Printf("\n")
}
func TestToggleProjectPublicity(t *testing.T) {
fmt.Println("\nTest for Project PUT API: Update properties for a selected project")
assert := assert.New(t)
apiTest := newHarborAPI()
//-------------------case1: Response Code=200------------------------------//
fmt.Println("case 1: respose code:200")
httpStatusCode, err := apiTest.ToggleProjectPublicity(*admin, "1", 1)
if err != nil {
t.Error("Error while search project by proId", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
}
//-------------------case2: Response Code=401 User need to log in first. ------------------------------//
fmt.Println("case 2: respose code:401, User need to log in first.")
httpStatusCode, err = apiTest.ToggleProjectPublicity(*unknownUsr, "1", 1)
if err != nil {
t.Error("Error while search project by proId", err.Error())
t.Log(err)
} else {
assert.Equal(int(401), httpStatusCode, "httpStatusCode should be 401")
}
//-------------------case3: Response Code=400 Invalid project id------------------------------//
fmt.Println("case 3: respose code:400, Invalid project id")
httpStatusCode, err = apiTest.ToggleProjectPublicity(*admin, "cc", 1)
if err != nil {
t.Error("Error while search project by proId", err.Error())
t.Log(err)
} else {
assert.Equal(int(400), httpStatusCode, "httpStatusCode should be 400")
}
//-------------------case4: Response Code=404 Not found the project------------------------------//
fmt.Println("case 4: respose code:404, Not found the project")
httpStatusCode, err = apiTest.ToggleProjectPublicity(*admin, "0", 1)
if err != nil {
t.Error("Error while search project by proId", err.Error())
t.Log(err)
} else {
assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404")
}
fmt.Printf("\n")
}
func TestProjectLogsFilter(t *testing.T) {
fmt.Println("\nTest for search access logs filtered by operations and date time ranges..")
assert := assert.New(t)
apiTest := newHarborAPI()
endTimestamp := time.Now().Unix()
startTimestamp := endTimestamp - 3600
accessLog := &apilib.AccessLogFilter{
Username: "admin",
Keywords: "",
BeginTimestamp: startTimestamp,
EndTimestamp: endTimestamp,
}
//-------------------case1: Response Code=200------------------------------//
fmt.Println("case 1: respose code:200")
projectID := "1"
httpStatusCode, _, err := apiTest.ProjectLogsFilter(*admin, projectID, *accessLog)
if err != nil {
t.Error("Error while search access logs")
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
}
//-------------------case2: Response Code=401:User need to log in first.------------------------------//
fmt.Println("case 2: respose code:401:User need to log in first.")
projectID = "1"
httpStatusCode, _, err = apiTest.ProjectLogsFilter(*unknownUsr, projectID, *accessLog)
if err != nil {
t.Error("Error while search access logs")
t.Log(err)
} else {
assert.Equal(int(401), httpStatusCode, "httpStatusCode should be 401")
}
//-------------------case3: Response Code=404:Project does not exist.-------------------------//
fmt.Println("case 3: respose code:404:Illegal format of provided ID value.")
projectID = "11111"
httpStatusCode, _, err = apiTest.ProjectLogsFilter(*admin, projectID, *accessLog)
if err != nil {
t.Error("Error while search access logs")
t.Log(err)
} else {
assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404")
}
fmt.Printf("\n")
}

View File

@ -56,43 +56,28 @@ func (ra *RepJobAPI) Prepare() {
} }
// List filters jobs according to the policy and repository // List filters jobs according to the parameters
func (ra *RepJobAPI) List() { func (ra *RepJobAPI) List() {
var policyID int64
var repository, status string
var startTime, endTime *time.Time
var num int
var err error
policyIDStr := ra.GetString("policy_id") policyID, err := ra.GetInt64("policy_id")
if len(policyIDStr) != 0 { if err != nil || policyID <= 0 {
policyID, err = strconv.ParseInt(policyIDStr, 10, 64) ra.CustomAbort(http.StatusBadRequest, "invalid policy_id")
if err != nil || policyID <= 0 {
ra.CustomAbort(http.StatusBadRequest, fmt.Sprintf("invalid policy ID: %s", policyIDStr))
}
} }
numStr := ra.GetString("num") policy, err := dao.GetRepPolicy(policyID)
if len(numStr) != 0 { if err != nil {
num, err = strconv.Atoi(numStr) log.Errorf("failed to get policy %d: %v", policyID, err)
if err != nil { ra.CustomAbort(http.StatusInternalServerError, "")
ra.CustomAbort(http.StatusBadRequest, fmt.Sprintf("invalid num: %s", numStr))
}
}
if num <= 0 {
num = 200
} }
endTimeStr := ra.GetString("end_time") if policy == nil {
if len(endTimeStr) != 0 { ra.CustomAbort(http.StatusNotFound, fmt.Sprintf("policy %d not found", policyID))
i, err := strconv.ParseInt(endTimeStr, 10, 64)
if err != nil {
ra.CustomAbort(http.StatusBadRequest, "invalid end_time")
}
t := time.Unix(i, 0)
endTime = &t
} }
repository := ra.GetString("repository")
status := ra.GetString("status")
var startTime *time.Time
startTimeStr := ra.GetString("start_time") startTimeStr := ra.GetString("start_time")
if len(startTimeStr) != 0 { if len(startTimeStr) != 0 {
i, err := strconv.ParseInt(startTimeStr, 10, 64) i, err := strconv.ParseInt(startTimeStr, 10, 64)
@ -103,21 +88,29 @@ func (ra *RepJobAPI) List() {
startTime = &t startTime = &t
} }
if startTime == nil && endTime == nil { var endTime *time.Time
// if start_time and end_time are both null, list jobs of last 10 days endTimeStr := ra.GetString("end_time")
t := time.Now().UTC().AddDate(0, 0, -10) if len(endTimeStr) != 0 {
startTime = &t i, err := strconv.ParseInt(endTimeStr, 10, 64)
if err != nil {
ra.CustomAbort(http.StatusBadRequest, "invalid end_time")
}
t := time.Unix(i, 0)
endTime = &t
} }
repository = ra.GetString("repository") page, pageSize := ra.getPaginationParams()
status = ra.GetString("status")
jobs, err := dao.FilterRepJobs(policyID, repository, status, startTime, endTime, num) jobs, total, err := dao.FilterRepJobs(policyID, repository, status,
startTime, endTime, pageSize, pageSize*(page-1))
if err != nil { if err != nil {
log.Errorf("failed to filter jobs according policy ID %d, repository %s, status %s: %v", policyID, repository, status, err) log.Errorf("failed to filter jobs according policy ID %d, repository %s, status %s, start time %v, end time %v: %v",
ra.RenderError(http.StatusInternalServerError, "Failed to query job") policyID, repository, status, startTime, endTime, err)
return ra.CustomAbort(http.StatusInternalServerError, "")
} }
ra.setPaginationHeader(total, page, pageSize)
ra.Data["json"] = jobs ra.Data["json"] = jobs
ra.ServeJSON() ra.ServeJSON()
} }
@ -154,7 +147,14 @@ func (ra *RepJobAPI) GetLog() {
ra.CustomAbort(http.StatusBadRequest, "id is nil") ra.CustomAbort(http.StatusBadRequest, "id is nil")
} }
resp, err := http.Get(buildJobLogURL(strconv.FormatInt(ra.jobID, 10))) req, err := http.NewRequest("GET", buildJobLogURL(strconv.FormatInt(ra.jobID, 10)), nil)
if err != nil {
log.Errorf("failed to create a request: %v", err)
ra.CustomAbort(http.StatusInternalServerError, "")
}
addAuthentication(req)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil { if err != nil {
log.Errorf("failed to get log for job %d: %v", ra.jobID, err) log.Errorf("failed to get log for job %d: %v", ra.jobID, err)
ra.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) ra.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))

View File

@ -91,15 +91,17 @@ func (pa *RepPolicyAPI) Post() {
policy := &models.RepPolicy{} policy := &models.RepPolicy{}
pa.DecodeJSONReqAndValidate(policy) pa.DecodeJSONReqAndValidate(policy)
po, err := dao.GetRepPolicyByName(policy.Name) /*
if err != nil { po, err := dao.GetRepPolicyByName(policy.Name)
log.Errorf("failed to get policy %s: %v", policy.Name, err) if err != nil {
pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) log.Errorf("failed to get policy %s: %v", policy.Name, err)
} pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if po != nil { if po != nil {
pa.CustomAbort(http.StatusConflict, "name is already used") pa.CustomAbort(http.StatusConflict, "name is already used")
} }
*/
project, err := dao.GetProjectByID(policy.ProjectID) project, err := dao.GetProjectByID(policy.ProjectID)
if err != nil { if err != nil {
@ -169,18 +171,20 @@ func (pa *RepPolicyAPI) Put() {
policy.ProjectID = originalPolicy.ProjectID policy.ProjectID = originalPolicy.ProjectID
pa.Validate(policy) pa.Validate(policy)
// check duplicate name /*
if policy.Name != originalPolicy.Name { // check duplicate name
po, err := dao.GetRepPolicyByName(policy.Name) if policy.Name != originalPolicy.Name {
if err != nil { po, err := dao.GetRepPolicyByName(policy.Name)
log.Errorf("failed to get policy %s: %v", policy.Name, err) if err != nil {
pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) log.Errorf("failed to get policy %s: %v", policy.Name, err)
} pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if po != nil { if po != nil {
pa.CustomAbort(http.StatusConflict, "name is already used") pa.CustomAbort(http.StatusConflict, "name is already used")
}
} }
} */
if policy.TargetID != originalPolicy.TargetID { if policy.TargetID != originalPolicy.TargetID {
//target of policy can not be modified when the policy is enabled //target of policy can not be modified when the policy is enabled
@ -349,3 +353,41 @@ func (pa *RepPolicyAPI) UpdateEnablement() {
}() }()
} }
} }
// Delete : policies which are disabled and have no running jobs
// can be deleted
func (pa *RepPolicyAPI) Delete() {
id := pa.GetIDFromURL()
policy, err := dao.GetRepPolicy(id)
if err != nil {
log.Errorf("failed to get policy %d: %v", id, err)
pa.CustomAbort(http.StatusInternalServerError, "")
}
if policy == nil || policy.Deleted == 1 {
pa.CustomAbort(http.StatusNotFound, "")
}
if policy.Enabled == 1 {
pa.CustomAbort(http.StatusPreconditionFailed, "plicy is enabled, can not be deleted")
}
jobs, err := dao.GetRepJobByPolicy(id)
if err != nil {
log.Errorf("failed to get jobs of policy %d: %v", id, err)
pa.CustomAbort(http.StatusInternalServerError, "")
}
for _, job := range jobs {
if job.Status == models.JobRunning ||
job.Status == models.JobRetrying ||
job.Status == models.JobPending {
pa.CustomAbort(http.StatusPreconditionFailed, "policy has running/retrying/pending jobs, can not be deleted")
}
}
if err = dao.DeleteRepPolicy(id); err != nil {
log.Errorf("failed to delete policy %d: %v", id, err)
pa.CustomAbort(http.StatusInternalServerError, "")
}
}

View File

@ -0,0 +1,267 @@
package api
import (
"fmt"
"github.com/stretchr/testify/assert"
"github.com/vmware/harbor/tests/apitests/apilib"
"strconv"
"testing"
)
const (
addPolicyName = "testPolicy"
)
var addPolicyID int
func TestPoliciesPost(t *testing.T) {
var httpStatusCode int
var err error
assert := assert.New(t)
apiTest := newHarborAPI()
//add target
CommonAddTarget()
targetID := int64(CommonGetTarget())
repPolicy := &apilib.RepPolicyPost{int64(1), targetID, addPolicyName}
fmt.Println("Testing Policies Post API")
//-------------------case 1 : response code = 201------------------------//
fmt.Println("case 1 : response code = 201")
httpStatusCode, err = apiTest.AddPolicy(*admin, *repPolicy)
if err != nil {
t.Error("Error while add policy", err.Error())
t.Log(err)
} else {
assert.Equal(int(201), httpStatusCode, "httpStatusCode should be 201")
}
//-------------------case 2 : response code = 409------------------------//
fmt.Println("case 1 : response code = 409:policy already exists")
httpStatusCode, err = apiTest.AddPolicy(*admin, *repPolicy)
if err != nil {
t.Error("Error while add policy", err.Error())
t.Log(err)
} else {
assert.Equal(int(409), httpStatusCode, "httpStatusCode should be 409")
}
//-------------------case 3 : response code = 401------------------------//
fmt.Println("case 3 : response code = 401: User need to log in first.")
httpStatusCode, err = apiTest.AddPolicy(*unknownUsr, *repPolicy)
if err != nil {
t.Error("Error while add policy", err.Error())
t.Log(err)
} else {
assert.Equal(int(401), httpStatusCode, "httpStatusCode should be 401")
}
//-------------------case 4 : response code = 400------------------------//
fmt.Println("case 4 : response code = 400:project_id invalid.")
repPolicy = &apilib.RepPolicyPost{TargetId: targetID, Name: addPolicyName}
httpStatusCode, err = apiTest.AddPolicy(*admin, *repPolicy)
if err != nil {
t.Error("Error while add policy", err.Error())
t.Log(err)
} else {
assert.Equal(int(400), httpStatusCode, "httpStatusCode should be 400")
}
//-------------------case 5 : response code = 400------------------------//
fmt.Println("case 5 : response code = 400:project_id does not exist.")
repPolicy.ProjectId = int64(1111)
httpStatusCode, err = apiTest.AddPolicy(*admin, *repPolicy)
if err != nil {
t.Error("Error while add policy", err.Error())
t.Log(err)
} else {
assert.Equal(int(400), httpStatusCode, "httpStatusCode should be 400")
}
//-------------------case 6 : response code = 400------------------------//
fmt.Println("case 6 : response code = 400:target_id invalid.")
repPolicy = &apilib.RepPolicyPost{ProjectId: int64(1), Name: addPolicyName}
httpStatusCode, err = apiTest.AddPolicy(*admin, *repPolicy)
if err != nil {
t.Error("Error while add policy", err.Error())
t.Log(err)
} else {
assert.Equal(int(400), httpStatusCode, "httpStatusCode should be 400")
}
//-------------------case 7 : response code = 400------------------------//
fmt.Println("case 6 : response code = 400:target_id does not exist.")
repPolicy.TargetId = int64(1111)
httpStatusCode, err = apiTest.AddPolicy(*admin, *repPolicy)
if err != nil {
t.Error("Error while add policy", err.Error())
t.Log(err)
} else {
assert.Equal(int(400), httpStatusCode, "httpStatusCode should be 400")
}
}
func TestPoliciesList(t *testing.T) {
var httpStatusCode int
var err error
var reslut []apilib.RepPolicy
assert := assert.New(t)
apiTest := newHarborAPI()
fmt.Println("Testing Policies Get/List API")
//-------------------case 1 : response code = 200------------------------//
fmt.Println("case 1 : response code = 200")
projectID := "1"
httpStatusCode, reslut, err = apiTest.ListPolicies(*admin, addPolicyName, projectID)
if err != nil {
t.Error("Error while get policies", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
addPolicyID = int(reslut[0].Id)
}
//-------------------case 2 : response code = 400------------------------//
fmt.Println("case 2 : response code = 400:invalid projectID")
projectID = "cc"
httpStatusCode, reslut, err = apiTest.ListPolicies(*admin, addPolicyName, projectID)
if err != nil {
t.Error("Error while get policies", err.Error())
t.Log(err)
} else {
assert.Equal(int(400), httpStatusCode, "httpStatusCode should be 400")
}
}
func TestPolicyGet(t *testing.T) {
var httpStatusCode int
var err error
assert := assert.New(t)
apiTest := newHarborAPI()
fmt.Println("Testing Policy Get API by PolicyID")
//-------------------case 1 : response code = 200------------------------//
fmt.Println("case 1 : response code = 200")
policyID := strconv.Itoa(addPolicyID)
httpStatusCode, err = apiTest.GetPolicyByID(*admin, policyID)
if err != nil {
t.Error("Error while get policy", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
}
}
func TestPolicyUpdateInfo(t *testing.T) {
var httpStatusCode int
var err error
targetID := int64(CommonGetTarget())
policyInfo := &apilib.RepPolicyUpdate{TargetId: targetID, Name: "testNewName"}
assert := assert.New(t)
apiTest := newHarborAPI()
fmt.Println("Testing Policy PUT API to update policyInfo")
//-------------------case 1 : response code = 200------------------------//
fmt.Println("case 1 : response code = 200")
policyID := strconv.Itoa(addPolicyID)
httpStatusCode, err = apiTest.PutPolicyInfoByID(*admin, policyID, *policyInfo)
if err != nil {
t.Error("Error while update policyInfo", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
}
}
func TestPolicyUpdateEnablement(t *testing.T) {
var httpStatusCode int
var err error
enablement := &apilib.RepPolicyEnablementReq{int32(0)}
assert := assert.New(t)
apiTest := newHarborAPI()
fmt.Println("Testing Policy PUT API to update policy enablement")
//-------------------case 1 : response code = 200------------------------//
fmt.Println("case 1 : response code = 200")
policyID := strconv.Itoa(addPolicyID)
httpStatusCode, err = apiTest.PutPolicyEnableByID(*admin, policyID, *enablement)
if err != nil {
t.Error("Error while put policy enablement", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
}
//-------------------case 2 : response code = 404------------------------//
fmt.Println("case 2 : response code = 404,Not Found")
policyID = "111"
httpStatusCode, err = apiTest.PutPolicyEnableByID(*admin, policyID, *enablement)
if err != nil {
t.Error("Error while put policy enablement", err.Error())
t.Log(err)
} else {
assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404")
}
}
func TestPolicyDelete(t *testing.T) {
var httpStatusCode int
var err error
assert := assert.New(t)
apiTest := newHarborAPI()
fmt.Println("Testing Policy Delete API")
//-------------------case 1 : response code = 412------------------------//
fmt.Println("case 1 : response code = 412:policy is enabled, can not be deleted")
CommonPolicyEabled(addPolicyID, 1)
policyID := strconv.Itoa(addPolicyID)
httpStatusCode, err = apiTest.DeletePolicyByID(*admin, policyID)
if err != nil {
t.Error("Error while delete policy", err.Error())
t.Log(err)
} else {
assert.Equal(int(412), httpStatusCode, "httpStatusCode should be 412")
}
//-------------------case 2 : response code = 200------------------------//
fmt.Println("case 2 : response code = 200")
CommonPolicyEabled(addPolicyID, 0)
policyID = strconv.Itoa(addPolicyID)
httpStatusCode, err = apiTest.DeletePolicyByID(*admin, policyID)
if err != nil {
t.Error("Error while delete policy", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
}
CommonDelTarget()
}

View File

@ -16,16 +16,14 @@
package api package api
import ( import (
"encoding/json"
"fmt" "fmt"
"io/ioutil"
"net/http" "net/http"
"os" "os"
"sort" "sort"
"strconv"
"strings"
"time"
"github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/manifest/schema2"
"github.com/vmware/harbor/dao" "github.com/vmware/harbor/dao"
"github.com/vmware/harbor/models" "github.com/vmware/harbor/models"
"github.com/vmware/harbor/service/cache" "github.com/vmware/harbor/service/cache"
@ -35,6 +33,7 @@ import (
registry_error "github.com/vmware/harbor/utils/registry/error" registry_error "github.com/vmware/harbor/utils/registry/error"
"github.com/vmware/harbor/utils"
"github.com/vmware/harbor/utils/registry/auth" "github.com/vmware/harbor/utils/registry/auth"
) )
@ -47,23 +46,23 @@ type RepositoryAPI struct {
// Get ... // Get ...
func (ra *RepositoryAPI) Get() { func (ra *RepositoryAPI) Get() {
projectID, err := ra.GetInt64("project_id") projectID, err := ra.GetInt64("project_id")
if err != nil { if err != nil || projectID <= 0 {
log.Errorf("Failed to get project id, error: %v", err) ra.CustomAbort(http.StatusBadRequest, "invalid project_id")
ra.RenderError(http.StatusBadRequest, "Invalid project id")
return
}
p, err := dao.GetProjectByID(projectID)
if err != nil {
log.Errorf("Error occurred in GetProjectById, error: %v", err)
ra.CustomAbort(http.StatusInternalServerError, "Internal error.")
}
if p == nil {
log.Warningf("Project with Id: %d does not exist", projectID)
ra.RenderError(http.StatusNotFound, "")
return
} }
if p.Public == 0 { page, pageSize := ra.getPaginationParams()
project, err := dao.GetProjectByID(projectID)
if err != nil {
log.Errorf("failed to get project %d: %v", projectID, err)
ra.CustomAbort(http.StatusInternalServerError, "")
}
if project == nil {
ra.CustomAbort(http.StatusNotFound, fmt.Sprintf("project %d not found", projectID))
}
if project.Public == 0 {
var userID int var userID int
if svc_utils.VerifySecret(ra.Ctx.Request) { if svc_utils.VerifySecret(ra.Ctx.Request) {
@ -73,37 +72,31 @@ func (ra *RepositoryAPI) Get() {
} }
if !checkProjectPermission(userID, projectID) { if !checkProjectPermission(userID, projectID) {
ra.RenderError(http.StatusForbidden, "") ra.CustomAbort(http.StatusForbidden, "")
return
} }
} }
repoList, err := cache.GetRepoFromCache() repositories, err := getReposByProject(project.Name, ra.GetString("q"))
if err != nil { if err != nil {
log.Errorf("Failed to get repo from cache, error: %v", err) log.Errorf("failed to get repository: %v", err)
ra.RenderError(http.StatusInternalServerError, "internal sever error") ra.CustomAbort(http.StatusInternalServerError, "")
} }
projectName := p.Name total := int64(len(repositories))
q := ra.GetString("q")
var resp []string if (page-1)*pageSize > total {
if len(q) > 0 { repositories = []string{}
for _, r := range repoList {
if strings.Contains(r, "/") && strings.Contains(r[strings.LastIndex(r, "/")+1:], q) && r[0:strings.LastIndex(r, "/")] == projectName {
resp = append(resp, r)
}
}
ra.Data["json"] = resp
} else if len(projectName) > 0 {
for _, r := range repoList {
if strings.Contains(r, "/") && r[0:strings.LastIndex(r, "/")] == projectName {
resp = append(resp, r)
}
}
ra.Data["json"] = resp
} else { } else {
ra.Data["json"] = repoList repositories = repositories[(page-1)*pageSize:]
} }
if page*pageSize <= total {
repositories = repositories[:pageSize]
}
ra.setPaginationHeader(total, page, pageSize)
ra.Data["json"] = repositories
ra.ServeJSON() ra.ServeJSON()
} }
@ -114,7 +107,7 @@ func (ra *RepositoryAPI) Delete() {
ra.CustomAbort(http.StatusBadRequest, "repo_name is nil") ra.CustomAbort(http.StatusBadRequest, "repo_name is nil")
} }
projectName := getProjectName(repoName) projectName, _ := utils.ParseRepository(repoName)
project, err := dao.GetProjectByName(projectName) project, err := dao.GetProjectByName(projectName)
if err != nil { if err != nil {
log.Errorf("failed to get project %s: %v", projectName, err) log.Errorf("failed to get project %s: %v", projectName, err)
@ -172,13 +165,15 @@ func (ra *RepositoryAPI) Delete() {
for _, t := range tags { for _, t := range tags {
if err := rc.DeleteTag(t); err != nil { if err := rc.DeleteTag(t); err != nil {
if regErr, ok := err.(*registry_error.Error); ok { if regErr, ok := err.(*registry_error.Error); ok {
ra.CustomAbort(regErr.StatusCode, regErr.Detail) if regErr.StatusCode != http.StatusNotFound {
ra.CustomAbort(regErr.StatusCode, regErr.Detail)
}
} else {
log.Errorf("error occurred while deleting tag %s:%s: %v", repoName, t, err)
ra.CustomAbort(http.StatusInternalServerError, "internal error")
} }
log.Errorf("error occurred while deleting tags of %s: %v", repoName, err)
ra.CustomAbort(http.StatusInternalServerError, "internal error")
} }
log.Infof("delete tag: %s %s", repoName, t) log.Infof("delete tag: %s:%s", repoName, t)
go TriggerReplicationByRepository(repoName, []string{t}, models.RepOpDelete) go TriggerReplicationByRepository(repoName, []string{t}, models.RepOpDelete)
go func(tag string) { go func(tag string) {
@ -188,6 +183,18 @@ func (ra *RepositoryAPI) Delete() {
}(t) }(t)
} }
exist, err := repositoryExist(repoName, rc)
if err != nil {
log.Errorf("failed to check the existence of repository %s: %v", repoName, err)
ra.CustomAbort(http.StatusInternalServerError, "")
}
if !exist {
if err = dao.DeleteRepository(repoName); err != nil {
log.Errorf("failed to delete repository %s: %v", repoName, err)
ra.CustomAbort(http.StatusInternalServerError, "")
}
}
go func() { go func() {
log.Debug("refreshing catalog cache") log.Debug("refreshing catalog cache")
if err := cache.RefreshCatalogCache(); err != nil { if err := cache.RefreshCatalogCache(); err != nil {
@ -208,7 +215,7 @@ func (ra *RepositoryAPI) GetTags() {
ra.CustomAbort(http.StatusBadRequest, "repo_name is nil") ra.CustomAbort(http.StatusBadRequest, "repo_name is nil")
} }
projectName := getProjectName(repoName) projectName, _ := utils.ParseRepository(repoName)
project, err := dao.GetProjectByName(projectName) project, err := dao.GetProjectByName(projectName)
if err != nil { if err != nil {
log.Errorf("failed to get project %s: %v", projectName, err) log.Errorf("failed to get project %s: %v", projectName, err)
@ -267,7 +274,16 @@ func (ra *RepositoryAPI) GetManifests() {
ra.CustomAbort(http.StatusBadRequest, "repo_name or tag is nil") ra.CustomAbort(http.StatusBadRequest, "repo_name or tag is nil")
} }
projectName := getProjectName(repoName) version := ra.GetString("version")
if len(version) == 0 {
version = "v2"
}
if version != "v1" && version != "v2" {
ra.CustomAbort(http.StatusBadRequest, "version should be v1 or v2")
}
projectName, _ := utils.ParseRepository(repoName)
project, err := dao.GetProjectByName(projectName) project, err := dao.GetProjectByName(projectName)
if err != nil { if err != nil {
log.Errorf("failed to get project %s: %v", projectName, err) log.Errorf("failed to get project %s: %v", projectName, err)
@ -291,10 +307,20 @@ func (ra *RepositoryAPI) GetManifests() {
ra.CustomAbort(http.StatusInternalServerError, "internal error") ra.CustomAbort(http.StatusInternalServerError, "internal error")
} }
item := models.RepoItem{} result := struct {
Manifest interface{} `json:"manifest"`
Config interface{} `json:"config,omitempty" `
}{}
mediaTypes := []string{schema1.MediaTypeManifest} mediaTypes := []string{}
_, _, payload, err := rc.PullManifest(tag, mediaTypes) switch version {
case "v1":
mediaTypes = append(mediaTypes, schema1.MediaTypeManifest)
case "v2":
mediaTypes = append(mediaTypes, schema2.MediaTypeManifest)
}
_, mediaType, payload, err := rc.PullManifest(tag, mediaTypes)
if err != nil { if err != nil {
if regErr, ok := err.(*registry_error.Error); ok { if regErr, ok := err.(*registry_error.Error); ok {
ra.CustomAbort(regErr.StatusCode, regErr.Detail) ra.CustomAbort(regErr.StatusCode, regErr.Detail)
@ -303,24 +329,33 @@ func (ra *RepositoryAPI) GetManifests() {
log.Errorf("error occurred while getting manifest of %s:%s: %v", repoName, tag, err) log.Errorf("error occurred while getting manifest of %s:%s: %v", repoName, tag, err)
ra.CustomAbort(http.StatusInternalServerError, "internal error") ra.CustomAbort(http.StatusInternalServerError, "internal error")
} }
mani := models.Manifest{}
err = json.Unmarshal(payload, &mani)
if err != nil {
log.Errorf("Failed to decode json from response for manifests, repo name: %s, tag: %s, error: %v", repoName, tag, err)
ra.RenderError(http.StatusInternalServerError, "Internal Server Error")
return
}
v1Compatibility := mani.History[0].V1Compatibility
err = json.Unmarshal([]byte(v1Compatibility), &item) manifest, _, err := registry.UnMarshal(mediaType, payload)
if err != nil { if err != nil {
log.Errorf("Failed to decode V1 field for repo, repo name: %s, tag: %s, error: %v", repoName, tag, err) log.Errorf("an error occurred while parsing manifest of %s:%s: %v", repoName, tag, err)
ra.RenderError(http.StatusInternalServerError, "Internal Server Error") ra.CustomAbort(http.StatusInternalServerError, "")
return
} }
item.DurationDays = strconv.Itoa(int(time.Since(item.Created).Hours()/24)) + " days"
ra.Data["json"] = item result.Manifest = manifest
deserializedmanifest, ok := manifest.(*schema2.DeserializedManifest)
if ok {
_, data, err := rc.PullBlob(deserializedmanifest.Target().Digest.String())
if err != nil {
log.Errorf("failed to get config of manifest %s:%s: %v", repoName, tag, err)
ra.CustomAbort(http.StatusInternalServerError, "")
}
b, err := ioutil.ReadAll(data)
if err != nil {
log.Errorf("failed to read config of manifest %s:%s: %v", repoName, tag, err)
ra.CustomAbort(http.StatusInternalServerError, "")
}
result.Config = string(b)
}
ra.Data["json"] = result
ra.ServeJSON() ra.ServeJSON()
} }
@ -375,25 +410,14 @@ func (ra *RepositoryAPI) getUsername() (string, error) {
//GetTopRepos handles request GET /api/repositories/top //GetTopRepos handles request GET /api/repositories/top
func (ra *RepositoryAPI) GetTopRepos() { func (ra *RepositoryAPI) GetTopRepos() {
var err error count, err := ra.GetInt("count", 10)
var countNum int if err != nil || count <= 0 {
count := ra.GetString("count") ra.CustomAbort(http.StatusBadRequest, "invalid count")
if len(count) == 0 {
countNum = 10
} else {
countNum, err = strconv.Atoi(count)
if err != nil {
log.Errorf("Get parameters error--count, err: %v", err)
ra.CustomAbort(http.StatusBadRequest, "bad request of count")
}
if countNum <= 0 {
log.Warning("count must be a positive integer")
ra.CustomAbort(http.StatusBadRequest, "count is 0 or negative")
}
} }
repos, err := dao.GetTopRepos(countNum)
repos, err := dao.GetTopRepos(count)
if err != nil { if err != nil {
log.Errorf("error occured in get top 10 repos: %v", err) log.Errorf("failed to get top repos: %v", err)
ra.CustomAbort(http.StatusInternalServerError, "internal server error") ra.CustomAbort(http.StatusInternalServerError, "internal server error")
} }
ra.Data["json"] = repos ra.Data["json"] = repos
@ -417,11 +441,3 @@ func newRepositoryClient(endpoint string, insecure bool, username, password, rep
} }
return client, nil return client, nil
} }
func getProjectName(repository string) string {
project := ""
if strings.Contains(repository, "/") {
project = repository[0:strings.LastIndex(repository, "/")]
}
return project
}

185
api/repository_test.go Normal file
View File

@ -0,0 +1,185 @@
package api
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
// "github.com/vmware/harbor/tests/apitests/apilib"
// "strconv"
)
func TestGetRepos(t *testing.T) {
var httpStatusCode int
var err error
assert := assert.New(t)
apiTest := newHarborAPI()
projectID := "1"
fmt.Println("Testing Repos Get API")
//-------------------case 1 : response code = 200------------------------//
fmt.Println("case 1 : response code = 200")
httpStatusCode, err = apiTest.GetRepos(*admin, projectID)
if err != nil {
t.Error("Error whihle get repos by projectID", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
}
//-------------------case 2 : response code = 400------------------------//
fmt.Println("case 2 : response code = 409,invalid project_id")
projectID = "ccc"
httpStatusCode, err = apiTest.GetRepos(*admin, projectID)
if err != nil {
t.Error("Error whihle get repos by projectID", err.Error())
t.Log(err)
} else {
assert.Equal(int(400), httpStatusCode, "httpStatusCode should be 400")
}
//-------------------case 3 : response code = 404------------------------//
fmt.Println("case 3 : response code = 404:project not found")
projectID = "111"
httpStatusCode, err = apiTest.GetRepos(*admin, projectID)
if err != nil {
t.Error("Error whihle get repos by projectID", err.Error())
t.Log(err)
} else {
assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404")
}
fmt.Printf("\n")
}
func TestGetReposTags(t *testing.T) {
var httpStatusCode int
var err error
var repoName string
assert := assert.New(t)
apiTest := newHarborAPI()
fmt.Println("Testing ReposTags Get API")
//-------------------case 1 : response code = 400------------------------//
fmt.Println("case 1 : response code = 400,repo_name is nil")
repoName = ""
httpStatusCode, err = apiTest.GetReposTags(*admin, repoName)
if err != nil {
t.Error("Error whihle get reposTags by repoName", err.Error())
t.Log(err)
} else {
assert.Equal(int(400), httpStatusCode, "httpStatusCode should be 400")
}
//-------------------case 2 : response code = 404------------------------//
fmt.Println("case 2 : response code = 404,repo not found")
repoName = "errorRepos"
httpStatusCode, err = apiTest.GetReposTags(*admin, repoName)
if err != nil {
t.Error("Error whihle get reposTags by repoName", err.Error())
t.Log(err)
} else {
assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404")
}
//-------------------case 3 : response code = 200------------------------//
fmt.Println("case 3 : response code = 200")
repoName = "library/hello-world"
httpStatusCode, err = apiTest.GetReposTags(*admin, repoName)
if err != nil {
t.Error("Error whihle get reposTags by repoName", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
}
fmt.Printf("\n")
}
func TestGetReposManifests(t *testing.T) {
var httpStatusCode int
var err error
var repoName string
var tag string
assert := assert.New(t)
apiTest := newHarborAPI()
fmt.Println("Testing ReposManifests Get API")
//-------------------case 1 : response code = 200------------------------//
fmt.Println("case 1 : response code = 200")
repoName = "library/hello-world"
tag = "latest"
httpStatusCode, err = apiTest.GetReposManifests(*admin, repoName, tag)
if err != nil {
t.Error("Error whihle get reposManifests by repoName and tag", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
}
//-------------------case 2 : response code = 404------------------------//
fmt.Println("case 2 : response code = 404:tags error,manifest unknown")
tag = "l"
httpStatusCode, err = apiTest.GetReposManifests(*admin, repoName, tag)
if err != nil {
t.Error("Error whihle get reposManifests by repoName and tag", err.Error())
t.Log(err)
} else {
assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404")
}
//-------------------case 3 : response code = 400------------------------//
fmt.Println("case 3 : response code = 400,repo_name or is nil")
repoName = ""
httpStatusCode, err = apiTest.GetReposManifests(*admin, repoName, tag)
if err != nil {
t.Error("Error whihle get reposManifests by repoName and tag", err.Error())
t.Log(err)
} else {
assert.Equal(int(400), httpStatusCode, "httpStatusCode should be 400")
}
//-------------------case 4 : response code = 404------------------------//
fmt.Println("case 4 : response code = 404,repo not found")
repoName = "111"
httpStatusCode, err = apiTest.GetReposManifests(*admin, repoName, tag)
if err != nil {
t.Error("Error whihle get reposManifests by repoName and tag", err.Error())
t.Log(err)
} else {
assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404")
}
fmt.Printf("\n")
}
func TestGetReposTop(t *testing.T) {
var httpStatusCode int
var err error
var count string
assert := assert.New(t)
apiTest := newHarborAPI()
fmt.Println("Testing ReposTop Get API")
//-------------------case 1 : response code = 200------------------------//
fmt.Println("case 1 : response code = 200")
count = "1"
httpStatusCode, err = apiTest.GetReposTop(*admin, count)
if err != nil {
t.Error("Error whihle get reposTop to show the most popular public repositories ", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
}
//-------------------case 2 : response code = 400------------------------//
fmt.Println("case 2 : response code = 400,invalid count")
count = "cc"
httpStatusCode, err = apiTest.GetReposTop(*admin, count)
if err != nil {
t.Error("Error whihle get reposTop to show the most popular public repositories ", err.Error())
t.Log(err)
} else {
assert.Equal(int(400), httpStatusCode, "httpStatusCode should be 400")
}
fmt.Printf("\n")
}

View File

@ -55,7 +55,7 @@ func (s *SearchAPI) Get() {
var projects []models.Project var projects []models.Project
if isSysAdmin { if isSysAdmin {
projects, err = dao.GetAllProjects("") projects, err = dao.GetProjects("")
if err != nil { if err != nil {
log.Errorf("failed to get all projects: %v", err) log.Errorf("failed to get all projects: %v", err)
s.CustomAbort(http.StatusInternalServerError, "internal error") s.CustomAbort(http.StatusInternalServerError, "internal error")
@ -85,11 +85,12 @@ func (s *SearchAPI) Get() {
} }
} }
repositories, err2 := cache.GetRepoFromCache() repositories, err := cache.GetRepoFromCache()
if err2 != nil { if err != nil {
log.Errorf("Failed to get repos from cache, error: %v", err2) log.Errorf("failed to list repositories: %v", err)
s.CustomAbort(http.StatusInternalServerError, "Failed to get repositories search result") s.CustomAbort(http.StatusInternalServerError, "")
} }
sort.Strings(repositories) sort.Strings(repositories)
repositoryResult := filterRepositories(repositories, projects, keyword) repositoryResult := filterRepositories(repositories, projects, keyword)
result := &searchResult{Project: projectResult, Repository: repositoryResult} result := &searchResult{Project: projectResult, Repository: repositoryResult}
@ -101,18 +102,19 @@ func filterRepositories(repositories []string, projects []models.Project, keywor
i, j := 0, 0 i, j := 0, 0
result := []map[string]interface{}{} result := []map[string]interface{}{}
for i < len(repositories) && j < len(projects) { for i < len(repositories) && j < len(projects) {
r := &utils.Repository{Name: repositories[i]} r := repositories[i]
d := strings.Compare(r.GetProject(), projects[j].Name) p, _ := utils.ParseRepository(r)
d := strings.Compare(p, projects[j].Name)
if d < 0 { if d < 0 {
i++ i++
continue continue
} else if d == 0 { } else if d == 0 {
i++ i++
if len(keyword) != 0 && !strings.Contains(r.Name, keyword) { if len(keyword) != 0 && !strings.Contains(r, keyword) {
continue continue
} }
entry := make(map[string]interface{}) entry := make(map[string]interface{})
entry["repository_name"] = r.Name entry["repository_name"] = r
entry["project_name"] = projects[j].Name entry["project_name"] = projects[j].Name
entry["project_id"] = projects[j].ProjectID entry["project_id"] = projects[j].ProjectID
entry["project_public"] = projects[j].Public entry["project_public"] = projects[j].Public

32
api/search_test.go Normal file
View File

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

View File

@ -17,14 +17,26 @@ package api
import ( import (
"net/http" "net/http"
"strings"
"github.com/vmware/harbor/dao" "github.com/vmware/harbor/dao"
"github.com/vmware/harbor/models"
"github.com/vmware/harbor/service/cache"
"github.com/vmware/harbor/utils/log" "github.com/vmware/harbor/utils/log"
) )
const (
// MPC : count of my projects
MPC = "my_project_count"
// MRC : count of my repositories
MRC = "my_repo_count"
// PPC : count of public projects
PPC = "public_project_count"
// PRC : count of public repositories
PRC = "public_repo_count"
// TPC : total count of projects
TPC = "total_project_count"
// TRC : total count of repositories
TRC = "total_repo_count"
)
// StatisticAPI handles request to /api/statistics/ // StatisticAPI handles request to /api/statistics/
type StatisticAPI struct { type StatisticAPI struct {
BaseAPI BaseAPI
@ -38,80 +50,60 @@ func (s *StatisticAPI) Prepare() {
// Get total projects and repos of the user // Get total projects and repos of the user
func (s *StatisticAPI) Get() { func (s *StatisticAPI) Get() {
statistic := map[string]int64{}
n, err := dao.GetTotalOfProjects("", 1)
if err != nil {
log.Errorf("failed to get total of public projects: %v", err)
s.CustomAbort(http.StatusInternalServerError, "")
}
statistic[PPC] = n
n, err = dao.GetTotalOfPublicRepositories("")
if err != nil {
log.Errorf("failed to get total of public repositories: %v", err)
s.CustomAbort(http.StatusInternalServerError, "")
}
statistic[PRC] = n
isAdmin, err := dao.IsAdminRole(s.userID) isAdmin, err := dao.IsAdminRole(s.userID)
if err != nil { if err != nil {
log.Errorf("Error occured in check admin, error: %v", err) log.Errorf("Error occured in check admin, error: %v", err)
s.CustomAbort(http.StatusInternalServerError, "Internal error.") s.CustomAbort(http.StatusInternalServerError, "Internal error.")
} }
var projectList []models.Project
if isAdmin { if isAdmin {
projectList, err = dao.GetAllProjects("") n, err := dao.GetTotalOfProjects("")
if err != nil {
log.Errorf("failed to get total of projects: %v", err)
s.CustomAbort(http.StatusInternalServerError, "")
}
statistic[MPC] = n
statistic[TPC] = n
n, err = dao.GetTotalOfRepositories("")
if err != nil {
log.Errorf("failed to get total of repositories: %v", err)
s.CustomAbort(http.StatusInternalServerError, "")
}
statistic[MRC] = n
statistic[TRC] = n
} else { } else {
projectList, err = dao.GetUserRelevantProjects(s.userID, "") n, err := dao.GetTotalOfUserRelevantProjects(s.userID, "")
} if err != nil {
if err != nil { log.Errorf("failed to get total of projects for user %d: %v", s.userID, err)
log.Errorf("Error occured in QueryProject, error: %v", err) s.CustomAbort(http.StatusInternalServerError, "")
s.CustomAbort(http.StatusInternalServerError, "Internal error.")
}
proMap := map[string]int{}
proMap["my_project_count"] = 0
proMap["my_repo_count"] = 0
proMap["public_project_count"] = 0
proMap["public_repo_count"] = 0
var publicProjects []models.Project
publicProjects, err = dao.GetPublicProjects("")
if err != nil {
log.Errorf("Error occured in QueryPublicProject, error: %v", err)
s.CustomAbort(http.StatusInternalServerError, "Internal error.")
}
proMap["public_project_count"] = len(publicProjects)
for i := 0; i < len(publicProjects); i++ {
proMap["public_repo_count"] += getRepoCountByProject(publicProjects[i].Name)
}
if isAdmin {
proMap["total_project_count"] = len(projectList)
proMap["total_repo_count"] = getTotalRepoCount()
}
for i := 0; i < len(projectList); i++ {
if isAdmin {
projectList[i].Role = models.PROJECTADMIN
} }
if projectList[i].Role == models.PROJECTADMIN || projectList[i].Role == models.DEVELOPER || statistic[MPC] = n
projectList[i].Role == models.GUEST {
proMap["my_project_count"]++ n, err = dao.GetTotalOfUserRelevantRepositories(s.userID, "")
proMap["my_repo_count"] += getRepoCountByProject(projectList[i].Name) if err != nil {
log.Errorf("failed to get total of repositories for user %d: %v", s.userID, err)
s.CustomAbort(http.StatusInternalServerError, "")
} }
statistic[MRC] = n
} }
s.Data["json"] = proMap
s.Data["json"] = statistic
s.ServeJSON() s.ServeJSON()
} }
//getReposByProject returns repo numbers of specified project
func getRepoCountByProject(projectName string) int {
repoList, err := cache.GetRepoFromCache()
if err != nil {
log.Errorf("Failed to get repo from cache, error: %v", err)
return 0
}
var resp int
if len(projectName) > 0 {
for _, r := range repoList {
if strings.Contains(r, "/") && r[0:strings.LastIndex(r, "/")] == projectName {
resp++
}
}
return resp
}
return 0
}
//getTotalRepoCount returns total repo count
func getTotalRepoCount() int {
repoList, err := cache.GetRepoFromCache()
if err != nil {
log.Errorf("Failed to get repo from cache, error: %v", err)
return 0
}
return len(repoList)
}

89
api/statistic_test.go Normal file
View File

@ -0,0 +1,89 @@
package api
import (
"fmt"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/harbor/tests/apitests/apilib"
)
func TestStatisticGet(t *testing.T) {
if err := SyncRegistry(); err != nil {
t.Fatalf("failed to sync repositories from registry: %v", err)
}
fmt.Println("Testing Statistic API")
assert := assert.New(t)
apiTest := newHarborAPI()
//prepare for test
var myProCount, pubProCount, totalProCount int32
result, err := apiTest.StatisticGet(*admin)
if err != nil {
t.Error("Error while get statistic information", err.Error())
t.Log(err)
} else {
myProCount = result.MyProjectCount
pubProCount = result.PublicProjectCount
totalProCount = result.TotalProjectCount
}
//post project
var project apilib.ProjectReq
project.ProjectName = "statistic_project"
project.Public = 1
//case 2: admin successful login, expect project creation success.
fmt.Println("case 2: admin successful login, expect project creation success.")
reply, err := apiTest.ProjectsPost(*admin, project)
if err != nil {
t.Error("Error while creat project", err.Error())
t.Log(err)
} else {
assert.Equal(reply, int(201), "Case 2: Project creation status should be 201")
}
//get and compare
result, err = apiTest.StatisticGet(*admin)
if err != nil {
t.Error("Error while get statistic information", err.Error())
t.Log(err)
} else {
assert.Equal(myProCount+1, result.MyProjectCount, "MyProjectCount should be equal")
assert.Equal(int32(2), result.MyRepoCount, "MyRepoCount should be equal")
assert.Equal(pubProCount+1, result.PublicProjectCount, "PublicProjectCount should be equal")
assert.Equal(int32(2), result.PublicRepoCount, "PublicRepoCount should be equal")
assert.Equal(totalProCount+1, result.TotalProjectCount, "TotalProCount should be equal")
assert.Equal(int32(2), result.TotalRepoCount, "TotalRepoCount should be equal")
}
//get the project
var projects []apilib.Project
var addProjectID int32
httpStatusCode, projects, err := apiTest.ProjectsGet(project.ProjectName, 1)
if err != nil {
t.Error("Error while search project by proName and isPublic", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
addProjectID = projects[0].ProjectId
}
//delete the project
projectID := strconv.Itoa(int(addProjectID))
httpStatusCode, err = apiTest.ProjectsDelete(*admin, projectID)
if err != nil {
t.Error("Error while delete project", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "Case 1: Project creation status should be 200")
//t.Log(result)
}
fmt.Printf("\n")
}

View File

@ -20,6 +20,7 @@ import (
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"os"
"strconv" "strconv"
"github.com/vmware/harbor/dao" "github.com/vmware/harbor/dao"
@ -34,10 +35,14 @@ import (
// TargetAPI handles request to /api/targets/ping /api/targets/{} // TargetAPI handles request to /api/targets/ping /api/targets/{}
type TargetAPI struct { type TargetAPI struct {
BaseAPI BaseAPI
secretKey string
} }
// Prepare validates the user // Prepare validates the user
func (t *TargetAPI) Prepare() { func (t *TargetAPI) Prepare() {
//TODO:move to config
t.secretKey = os.Getenv("SECRET_KEY")
userID := t.ValidateUser() userID := t.ValidateUser()
isSysAdmin, err := dao.IsAdminRole(userID) isSysAdmin, err := dao.IsAdminRole(userID)
if err != nil { if err != nil {
@ -76,7 +81,7 @@ func (t *TargetAPI) Ping() {
password = target.Password password = target.Password
if len(password) != 0 { if len(password) != 0 {
password, err = utils.ReversibleDecrypt(password) password, err = utils.ReversibleDecrypt(password, t.secretKey)
if err != nil { if err != nil {
log.Errorf("failed to decrypt password: %v", err) log.Errorf("failed to decrypt password: %v", err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
@ -136,7 +141,7 @@ func (t *TargetAPI) Get() {
// modify other fields of target he does not need to input the password again. // modify other fields of target he does not need to input the password again.
// The security issue can be fixed by enable https. // The security issue can be fixed by enable https.
if len(target.Password) != 0 { if len(target.Password) != 0 {
pwd, err := utils.ReversibleDecrypt(target.Password) pwd, err := utils.ReversibleDecrypt(target.Password, t.secretKey)
if err != nil { if err != nil {
log.Errorf("failed to decrypt password: %v", err) log.Errorf("failed to decrypt password: %v", err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
@ -162,7 +167,7 @@ func (t *TargetAPI) List() {
continue continue
} }
str, err := utils.ReversibleDecrypt(target.Password) str, err := utils.ReversibleDecrypt(target.Password, t.secretKey)
if err != nil { if err != nil {
log.Errorf("failed to decrypt password: %v", err) log.Errorf("failed to decrypt password: %v", err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
@ -201,7 +206,11 @@ func (t *TargetAPI) Post() {
} }
if len(target.Password) != 0 { if len(target.Password) != 0 {
target.Password = utils.ReversibleEncrypt(target.Password) target.Password, err = utils.ReversibleEncrypt(target.Password, t.secretKey)
if err != nil {
log.Errorf("failed to encrypt password: %v", err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
} }
id, err := dao.AddRepTarget(*target) id, err := dao.AddRepTarget(*target)
@ -275,7 +284,11 @@ func (t *TargetAPI) Put() {
target.ID = id target.ID = id
if len(target.Password) != 0 { if len(target.Password) != 0 {
target.Password = utils.ReversibleEncrypt(target.Password) target.Password, err = utils.ReversibleEncrypt(target.Password, t.secretKey)
if err != nil {
log.Errorf("failed to encrypt password: %v", err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
} }
if err := dao.UpdateRepTarget(*target); err != nil { if err := dao.UpdateRepTarget(*target); err != nil {

274
api/target_test.go Normal file
View File

@ -0,0 +1,274 @@
package api
import (
"fmt"
"os"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/harbor/tests/apitests/apilib"
)
const (
addTargetName = "testTargets"
)
var addTargetID int
func TestTargetsPost(t *testing.T) {
var httpStatusCode int
var err error
assert := assert.New(t)
apiTest := newHarborAPI()
endPoint := os.Getenv("REGISTRY_URL")
repTargets := &apilib.RepTargetPost{endPoint, addTargetName, adminName, adminPwd}
fmt.Println("Testing Targets Post API")
//-------------------case 1 : response code = 201------------------------//
fmt.Println("case 1 : response code = 201")
httpStatusCode, err = apiTest.AddTargets(*admin, *repTargets)
if err != nil {
t.Error("Error whihle add targets", err.Error())
t.Log(err)
} else {
assert.Equal(int(201), httpStatusCode, "httpStatusCode should be 201")
}
//-----------case 2 : response code = 409,name is already used-----------//
fmt.Println("case 2 : response code = 409,name is already used")
httpStatusCode, err = apiTest.AddTargets(*admin, *repTargets)
if err != nil {
t.Error("Error whihle add targets", err.Error())
t.Log(err)
} else {
assert.Equal(int(409), httpStatusCode, "httpStatusCode should be 409")
}
//-----------case 3 : response code = 409,name is already used-----------//
fmt.Println("case 3 : response code = 409,endPoint is already used")
repTargets.Username = "errName"
httpStatusCode, err = apiTest.AddTargets(*admin, *repTargets)
if err != nil {
t.Error("Error whihle add targets", err.Error())
t.Log(err)
} else {
assert.Equal(int(409), httpStatusCode, "httpStatusCode should be 409")
}
//--------case 4 : response code = 401,User need to log in first.--------//
fmt.Println("case 4 : response code = 401,User need to log in first.")
httpStatusCode, err = apiTest.AddTargets(*unknownUsr, *repTargets)
if err != nil {
t.Error("Error whihle add targets", err.Error())
t.Log(err)
} else {
assert.Equal(int(401), httpStatusCode, "httpStatusCode should be 401")
}
fmt.Printf("\n")
}
func TestTargetsGet(t *testing.T) {
var httpStatusCode int
var err error
var reslut []apilib.RepTarget
assert := assert.New(t)
apiTest := newHarborAPI()
fmt.Println("Testing Targets Get API")
//-------------------case 1 : response code = 200------------------------//
fmt.Println("case 1 : response code = 200")
httpStatusCode, reslut, err = apiTest.ListTargets(*admin, addTargetName)
if err != nil {
t.Error("Error whihle get targets", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
addTargetID = int(reslut[0].Id)
}
}
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 : response code = 200------------------------//
fmt.Println("case 1 : response code = 200")
id := strconv.Itoa(addTargetID)
httpStatusCode, err = apiTest.PingTargetsByID(*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.PingTargetsByID(*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")
}
//------------case 3 : response code = 400,targetID is invalid-----------//
fmt.Println("case 2 : response code = 400,target not found")
id = "cc"
httpStatusCode, err = apiTest.PingTargetsByID(*admin, id)
if err != nil {
t.Error("Error whihle ping target", err.Error())
t.Log(err)
} else {
assert.Equal(int(400), httpStatusCode, "httpStatusCode should be 400")
}
}
func TestTargetGetByID(t *testing.T) {
var httpStatusCode int
var err error
assert := assert.New(t)
apiTest := newHarborAPI()
fmt.Println("Testing Targets Get API by Id")
//-------------------case 1 : response code = 200------------------------//
fmt.Println("case 1 : response code = 200")
id := strconv.Itoa(addTargetID)
httpStatusCode, err = apiTest.GetTargetByID(*admin, id)
if err != nil {
t.Error("Error whihle get target by id", 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.GetTargetByID(*admin, id)
if err != nil {
t.Error("Error whihle get target by id", err.Error())
t.Log(err)
} else {
assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404")
}
}
func TestTargetsPut(t *testing.T) {
var httpStatusCode int
var err error
assert := assert.New(t)
apiTest := newHarborAPI()
endPoint := "1.1.1.1"
updateRepTargets := &apilib.RepTargetPost{endPoint, addTargetName, adminName, adminPwd}
id := strconv.Itoa(addTargetID)
fmt.Println("Testing Target Put API")
//-------------------case 1 : response code = 200------------------------//
fmt.Println("case 1 : response code = 200")
httpStatusCode, err = apiTest.PutTargetByID(*admin, id, *updateRepTargets)
if err != nil {
t.Error("Error whihle update target", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
}
//--------------case 2 : response code = 404,target not found------------//
id = "111"
fmt.Println("case 2 : response code = 404,target not found")
httpStatusCode, err = apiTest.PutTargetByID(*admin, id, *updateRepTargets)
if err != nil {
t.Error("Error whihle update target", err.Error())
t.Log(err)
} else {
assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404")
}
}
func TestTargetGetPolicies(t *testing.T) {
var httpStatusCode int
var err error
assert := assert.New(t)
apiTest := newHarborAPI()
fmt.Println("Testing Targets Get API to list policies")
//-------------------case 1 : response code = 200------------------------//
fmt.Println("case 1 : response code = 200")
id := strconv.Itoa(addTargetID)
httpStatusCode, err = apiTest.GetTargetPoliciesByID(*admin, id)
if err != nil {
t.Error("Error whihle get target by id", 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.GetTargetPoliciesByID(*admin, id)
if err != nil {
t.Error("Error whihle get target by id", err.Error())
t.Log(err)
} else {
assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404")
}
}
func TestTargetsDelete(t *testing.T) {
var httpStatusCode int
var err error
assert := assert.New(t)
apiTest := newHarborAPI()
id := strconv.Itoa(addTargetID)
fmt.Println("Testing Targets Delete API")
//-------------------case 1 : response code = 200------------------------//
fmt.Println("case 1 : response code = 200")
httpStatusCode, err = apiTest.DeleteTargetsByID(*admin, id)
if err != nil {
t.Error("Error whihle delete targets", 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.DeleteTargetsByID(*admin, id)
if err != nil {
t.Error("Error whihle delete targets", err.Error())
t.Log(err)
} else {
assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404")
}
}

View File

@ -315,13 +315,13 @@ func (ua *UserAPI) ToggleUserAdminRole() {
// validate only validate when user register // validate only validate when user register
func validate(user models.User) error { func validate(user models.User) error {
if isIllegalLength(user.Username, 0, 20) { if isIllegalLength(user.Username, 1, 20) {
return fmt.Errorf("Username with illegal length.") return fmt.Errorf("Username with illegal length.")
} }
if isContainIllegalChar(user.Username, []string{",", "~", "#", "$", "%"}) { if isContainIllegalChar(user.Username, []string{",", "~", "#", "$", "%"}) {
return fmt.Errorf("Username contains illegal characters.") return fmt.Errorf("Username contains illegal characters.")
} }
if isIllegalLength(user.Password, 0, 20) { if isIllegalLength(user.Password, 7, 20) {
return fmt.Errorf("Password with illegal length.") return fmt.Errorf("Password with illegal length.")
} }
if err := commonValidate(user); err != nil { if err := commonValidate(user); err != nil {

398
api/user_test.go Normal file
View File

@ -0,0 +1,398 @@
package api
import (
"fmt"
"github.com/stretchr/testify/assert"
"github.com/vmware/harbor/tests/apitests/apilib"
"testing"
)
var testUser0002ID, testUser0003ID int
var testUser0002, testUser0003 apilib.User
var testUser0002Auth, testUser0003Auth *usrInfo
func TestUsersPost(t *testing.T) {
fmt.Println("Testing User Add")
assert := assert.New(t)
apiTest := newHarborAPI()
//case 1: register a new user without admin auth, expect 400, because self registration is on
fmt.Println("Register user without admin auth")
code, err := apiTest.UsersPost(testUser0002)
if err != nil {
t.Error("Error occured while add a test User", err.Error())
t.Log(err)
} else {
assert.Equal(400, code, "Add user status should be 400")
}
//case 2: register a new user with admin auth, but username is empty, expect 400
fmt.Println("Register user with admin auth, but username is empty")
code, err = apiTest.UsersPost(testUser0002, *admin)
if err != nil {
t.Error("Error occured while add a user", err.Error())
t.Log(err)
} else {
assert.Equal(400, code, "Add user status should be 400")
}
//case 3: register a new user with admin auth, but bad username format, expect 400
testUser0002.Username = "test@$"
fmt.Println("Register user with admin auth, but bad username format")
code, err = apiTest.UsersPost(testUser0002, *admin)
if err != nil {
t.Error("Error occured while add a user", err.Error())
t.Log(err)
} else {
assert.Equal(400, code, "Add user status should be 400")
}
//case 4: register a new user with admin auth, but bad userpassword format, expect 400
testUser0002.Username = "testUser0002"
fmt.Println("Register user with admin auth, but empty password.")
code, err = apiTest.UsersPost(testUser0002, *admin)
if err != nil {
t.Error("Error occured while add a user", err.Error())
t.Log(err)
} else {
assert.Equal(400, code, "Add user status should be 400")
}
//case 5: register a new user with admin auth, but email is empty, expect 400
testUser0002.Password = "testUser0002"
fmt.Println("Register user with admin auth, but email is empty")
code, err = apiTest.UsersPost(testUser0002, *admin)
if err != nil {
t.Error("Error occured while add a user", err.Error())
t.Log(err)
} else {
assert.Equal(400, code, "Add user status should be 400")
}
//case 6: register a new user with admin auth, but bad email format, expect 400
testUser0002.Email = "test..."
fmt.Println("Register user with admin auth, but bad email format")
code, err = apiTest.UsersPost(testUser0002, *admin)
if err != nil {
t.Error("Error occured while add a user", err.Error())
t.Log(err)
} else {
assert.Equal(400, code, "Add user status should be 400")
}
//case 7: register a new user with admin auth, but userrealname is empty, expect 400
/*
testUser0002.Email = "testUser0002@mydomain.com"
fmt.Println("Register user with admin auth, but user realname is empty")
code, err = apiTest.UsersPost(testUser0002, *admin)
if err != nil {
t.Error("Error occured while add a user", err.Error())
t.Log(err)
} else {
assert.Equal(400, code, "Add user status should be 400")
}
*/
//case 8: register a new user with admin auth, but bad userrealname format, expect 400
testUser0002.Email = "testUser0002@mydomain.com"
testUser0002.Realname = "test$com"
fmt.Println("Register user with admin auth, but bad user realname format")
code, err = apiTest.UsersPost(testUser0002, *admin)
if err != nil {
t.Error("Error occured while add a user", err.Error())
t.Log(err)
} else {
assert.Equal(400, code, "Add user status should be 400")
}
//case 9: register a new user with admin auth, but bad user comment, expect 400
testUser0002.Realname = "testUser0002"
testUser0002.Comment = "vmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm"
fmt.Println("Register user with admin auth, but bad user comment format")
code, err = apiTest.UsersPost(testUser0002, *admin)
if err != nil {
t.Error("Error occured while add a user", err.Error())
t.Log(err)
} else {
assert.Equal(400, code, "Add user status should be 400")
}
//case 10: register a new user with admin auth, expect 201
fmt.Println("Register user with admin auth, right parameters")
testUser0002.Comment = "test user"
code, err = apiTest.UsersPost(testUser0002, *admin)
if err != nil {
t.Error("Error occured while add a user", err.Error())
t.Log(err)
} else {
assert.Equal(201, code, "Add user status should be 201")
}
//case 11: register duplicate user with admin auth, expect 409
fmt.Println("Register duplicate user with admin auth")
code, err = apiTest.UsersPost(testUser0002, *admin)
if err != nil {
t.Error("Error occured while add a user", err.Error())
t.Log(err)
} else {
assert.Equal(409, code, "Add user status should be 409")
}
//case 12: register a new user with admin auth, but duplicate email, expect 409
fmt.Println("Register user with admin auth, but duplicate email")
testUser0002.Username = "testUsertest"
testUser0002.Email = "testUser0002@mydomain.com"
code, err = apiTest.UsersPost(testUser0002, *admin)
if err != nil {
t.Error("Error occured while add a user", err.Error())
t.Log(err)
} else {
assert.Equal(409, code, "Add user status should be 409")
}
}
func TestUsersGet(t *testing.T) {
fmt.Println("Testing User Get")
assert := assert.New(t)
apiTest := newHarborAPI()
testUser0002.Username = "testUser0002"
//case 1: Get user2 with common auth, but no userid in path, expect 403
testUser0002Auth = &usrInfo{"testUser0002", "testUser0002"}
code, users, err := apiTest.UsersGet(testUser0002.Username, *testUser0002Auth)
if err != nil {
t.Error("Error occured while get users", err.Error())
t.Log(err)
} else {
assert.Equal(403, code, "Get users status should be 403")
}
//case 2: Get user2 with admin auth, expect 200
code, users, err = apiTest.UsersGet(testUser0002.Username, *admin)
if err != nil {
t.Error("Error occured while get users", err.Error())
t.Log(err)
} else {
assert.Equal(200, code, "Get users status should be 200")
assert.Equal(1, len(users), "Get users record should be 1 ")
testUser0002ID = users[0].UserId
}
}
func TestUsersGetByID(t *testing.T) {
fmt.Println("Testing User GetByID")
assert := assert.New(t)
apiTest := newHarborAPI()
//case 1: Get user2 with userID and his own auth, expect 200
code, user, err := apiTest.UsersGetByID(testUser0002.Username, *testUser0002Auth, testUser0002ID)
if err != nil {
t.Error("Error occured while get users", err.Error())
t.Log(err)
} else {
assert.Equal(200, code, "Get users status should be 200")
assert.Equal(testUser0002.Username, user.Username, "Get users username should be equal")
assert.Equal(testUser0002.Email, user.Email, "Get users email should be equal")
}
//case 2: Get user2 with user3 auth, expect 403
testUser0003.Username = "testUser0003"
testUser0003.Email = "testUser0003@mydomain.com"
testUser0003.Password = "testUser0003"
testUser0003.Realname = "testUser0003"
code, err = apiTest.UsersPost(testUser0003, *admin)
if err != nil {
t.Error("Error occured while add a user", err.Error())
t.Log(err)
} else {
assert.Equal(201, code, "Add user status should be 201")
}
testUser0003Auth = &usrInfo{"testUser0003", "testUser0003"}
code, user, err = apiTest.UsersGetByID(testUser0002.Username, *testUser0003Auth, testUser0002ID)
if err != nil {
t.Error("Error occured while get users", err.Error())
t.Log(err)
} else {
assert.Equal(403, code, "Get users status should be 403")
}
//case 3: Get user that does not exist with user2 auth, expect 404 not found.
code, user, err = apiTest.UsersGetByID(testUser0002.Username, *testUser0002Auth, 1000)
if err != nil {
t.Error("Error occured while change user profile", err.Error())
t.Log(err)
} else {
assert.Equal(404, code, "Get users status should be 404")
}
// Get user3ID in order to delete at the last of the test
code, users, err := apiTest.UsersGet(testUser0003.Username, *admin)
if err != nil {
t.Error("Error occured while change user profile", err.Error())
t.Log(err)
} else {
assert.Equal(200, code, "Get users status should be 200")
assert.Equal(1, len(users), "Get users record should be 1")
testUser0003ID = users[0].UserId
}
}
func TestUsersPut(t *testing.T) {
fmt.Println("Testing User Put")
assert := assert.New(t)
apiTest := newHarborAPI()
var profile apilib.UserProfile
//case 1: change user2 profile with user3 auth
code, err := apiTest.UsersPut(testUser0002ID, profile, *testUser0003Auth)
if err != nil {
t.Error("Error occured while change user profile", err.Error())
t.Log(err)
} else {
assert.Equal(403, code, "Get users status should be 403")
}
//case 2: change user2 profile with user2 auth, but bad parameters format.
code, err = apiTest.UsersPut(testUser0002ID, profile, *testUser0002Auth)
if err != nil {
t.Error("Error occured while change user profile", err.Error())
t.Log(err)
} else {
assert.Equal(400, code, "Get users status should be 400")
}
//case 3: change user2 profile with user2 auth, but duplicate email.
profile.Realname = "test user"
profile.Email = "testUser0003@mydomain.com"
profile.Comment = "change profile"
code, err = apiTest.UsersPut(testUser0002ID, profile, *testUser0002Auth)
if err != nil {
t.Error("Error occured while change user profile", err.Error())
t.Log(err)
} else {
assert.Equal(409, code, "Get users status should be 409")
}
//case 4: change user2 profile with user2 auth, right parameters format.
profile.Realname = "test user"
profile.Email = "testUser0002@vmware.com"
profile.Comment = "change profile"
code, err = apiTest.UsersPut(testUser0002ID, profile, *testUser0002Auth)
if err != nil {
t.Error("Error occured while change user profile", err.Error())
t.Log(err)
} else {
assert.Equal(200, code, "Get users status should be 200")
}
}
func TestUsersToggleAdminRole(t *testing.T) {
fmt.Println("Testing Toggle User Admin Role")
assert := assert.New(t)
apiTest := newHarborAPI()
//case 1: toggle user2 admin role without admin auth
code, err := apiTest.UsersToggleAdminRole(testUser0002ID, *testUser0002Auth, int32(1))
if err != nil {
t.Error("Error occured while change user profile", err.Error())
t.Log(err)
} else {
assert.Equal(403, code, "Get users status should be 403")
}
//case 2: toggle user2 admin role with admin auth
code, err = apiTest.UsersToggleAdminRole(testUser0002ID, *admin, int32(1))
if err != nil {
t.Error("Error occured while change user profile", err.Error())
t.Log(err)
} else {
assert.Equal(200, code, "Get users status should be 200")
}
}
func TestUsersUpdatePassword(t *testing.T) {
fmt.Println("Testing Update User Password")
assert := assert.New(t)
apiTest := newHarborAPI()
password := apilib.Password{OldPassword: "", NewPassword: ""}
//case 1: update user2 password with user3 auth
code, err := apiTest.UsersUpdatePassword(testUser0002ID, password, *testUser0003Auth)
if err != nil {
t.Error("Error occured while update user password", err.Error())
t.Log(err)
} else {
assert.Equal(403, code, "Update user password status should be 403")
}
//case 2: update user2 password with admin auth, but oldpassword is empty
code, err = apiTest.UsersUpdatePassword(testUser0002ID, password, *admin)
if err != nil {
t.Error("Error occured while change user profile", err.Error())
t.Log(err)
} else {
assert.Equal(400, code, "Get users status should be 400")
}
//case 3: update user2 password with admin auth, but oldpassword is wrong
password.OldPassword = "000"
code, err = apiTest.UsersUpdatePassword(testUser0002ID, password, *admin)
if err != nil {
t.Error("Error occured while change user profile", err.Error())
t.Log(err)
} else {
assert.Equal(403, code, "Get users status should be 403")
}
//case 4: update user2 password with admin auth, but newpassword is empty
password.OldPassword = "testUser0002"
code, err = apiTest.UsersUpdatePassword(testUser0002ID, password, *admin)
if err != nil {
t.Error("Error occured while change user profile", err.Error())
t.Log(err)
} else {
assert.Equal(400, code, "Get users status should be 400")
}
//case 5: update user2 password with admin auth, right parameters
password.NewPassword = "TestUser0002"
code, err = apiTest.UsersUpdatePassword(testUser0002ID, password, *admin)
if err != nil {
t.Error("Error occured while change user profile", err.Error())
t.Log(err)
} else {
assert.Equal(200, code, "Get users status should be 200")
testUser0002.Password = password.NewPassword
testUser0002Auth.Passwd = password.NewPassword
}
}
func TestUsersDelete(t *testing.T) {
fmt.Println("Testing User Delete")
assert := assert.New(t)
apiTest := newHarborAPI()
//case 1:delete user without admin auth
code, err := apiTest.UsersDelete(testUser0002ID, *testUser0003Auth)
if err != nil {
t.Error("Error occured while delete a testUser", err.Error())
t.Log(err)
} else {
assert.Equal(403, code, "Delete testUser status should be 403")
}
//case 2: delete user with admin auth, user2 has already been toggled to admin, but can not delete himself
code, err = apiTest.UsersDelete(testUser0002ID, *testUser0002Auth)
if err != nil {
t.Error("Error occured while delete a testUser", err.Error())
t.Log(err)
} else {
assert.Equal(403, code, "Delete testUser status should be 403")
}
//case 3: delete user with admin auth
code, err = apiTest.UsersDelete(testUser0002ID, *admin)
if err != nil {
t.Error("Error occured while delete a testUser", err.Error())
t.Log(err)
} else {
assert.Equal(200, code, "Delete testUser status should be 200")
}
//delete user3 with admin auth
code, err = apiTest.UsersDelete(testUser0003ID, *admin)
if err != nil {
t.Error("Error occured while delete a testUser", err.Error())
t.Log(err)
} else {
assert.Equal(200, code, "Delete testUser status should be 200")
}
}

View File

@ -20,13 +20,20 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net"
"net/http" "net/http"
"os" "os"
"sort"
"strings" "strings"
"time"
"github.com/vmware/harbor/dao" "github.com/vmware/harbor/dao"
"github.com/vmware/harbor/models" "github.com/vmware/harbor/models"
"github.com/vmware/harbor/service/cache"
"github.com/vmware/harbor/utils"
"github.com/vmware/harbor/utils/log" "github.com/vmware/harbor/utils/log"
"github.com/vmware/harbor/utils/registry"
registry_error "github.com/vmware/harbor/utils/registry/error"
) )
func checkProjectPermission(userID int, projectID int64) bool { func checkProjectPermission(userID int, projectID int64) bool {
@ -115,7 +122,14 @@ func TriggerReplication(policyID int64, repository string,
url := buildReplicationURL() url := buildReplicationURL()
resp, err := http.DefaultClient.Post(url, "application/json", bytes.NewBuffer(b)) req, err := http.NewRequest("POST", url, bytes.NewBuffer(b))
if err != nil {
return err
}
addAuthentication(req)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil { if err != nil {
return err return err
} }
@ -188,7 +202,16 @@ func postReplicationAction(policyID int64, acton string) error {
url := buildReplicationActionURL() url := buildReplicationActionURL()
resp, err := http.DefaultClient.Post(url, "application/json", bytes.NewBuffer(b)) req, err := http.NewRequest("POST", url, bytes.NewBuffer(b))
if err != nil {
return err
}
addAuthentication(req)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil { if err != nil {
return err return err
} }
@ -207,6 +230,217 @@ func postReplicationAction(policyID int64, acton string) error {
return fmt.Errorf("%d %s", resp.StatusCode, string(b)) return fmt.Errorf("%d %s", resp.StatusCode, string(b))
} }
func addAuthentication(req *http.Request) {
if req != nil {
req.AddCookie(&http.Cookie{
Name: models.UISecretCookie,
// TODO read secret from config
Value: os.Getenv("UI_SECRET"),
})
}
}
// SyncRegistry syncs the repositories of registry with database.
func SyncRegistry() error {
log.Debugf("Start syncing repositories from registry to DB... ")
reposInRegistry, err := catalog()
if err != nil {
log.Error(err)
return err
}
var repoRecordsInDB []models.RepoRecord
repoRecordsInDB, err = dao.GetAllRepositories()
if err != nil {
log.Errorf("error occurred while getting all registories. %v", err)
return err
}
var reposInDB []string
for _, repoRecordInDB := range repoRecordsInDB {
reposInDB = append(reposInDB, repoRecordInDB.Name)
}
var reposToAdd []string
var reposToDel []string
reposToAdd, reposToDel, err = diffRepos(reposInRegistry, reposInDB)
if err != nil {
return err
}
if len(reposToAdd) > 0 {
log.Debugf("Start adding repositories into DB... ")
for _, repoToAdd := range reposToAdd {
project, _ := utils.ParseRepository(repoToAdd)
user, err := dao.GetAccessLogCreator(repoToAdd)
if err != nil {
log.Errorf("Error happens when getting the repository owner from access log: %v", err)
}
if len(user) == 0 {
user = "anonymous"
}
pullCount, err := dao.CountPull(repoToAdd)
if err != nil {
log.Errorf("Error happens when counting pull count from access log: %v", err)
}
repoRecord := models.RepoRecord{Name: repoToAdd, OwnerName: user, ProjectName: project, PullCount: pullCount}
if err := dao.AddRepository(repoRecord); err != nil {
log.Errorf("Error happens when adding the missing repository: %v", err)
}
log.Debugf("Add repository: %s success.", repoToAdd)
}
}
if len(reposToDel) > 0 {
log.Debugf("Start deleting repositories from DB... ")
for _, repoToDel := range reposToDel {
if err := dao.DeleteRepository(repoToDel); err != nil {
log.Errorf("Error happens when deleting the repository: %v", err)
}
log.Debugf("Delete repository: %s success.", repoToDel)
}
}
log.Debugf("Sync repositories from registry to DB is done.")
return nil
}
func catalog() ([]string, error) {
repositories := []string{}
rc, err := initRegistryClient()
if err != nil {
return repositories, err
}
repositories, err = rc.Catalog()
if err != nil {
return repositories, err
}
return repositories, nil
}
func diffRepos(reposInRegistry []string, reposInDB []string) ([]string, []string, error) {
var needsAdd []string
var needsDel []string
sort.Strings(reposInRegistry)
sort.Strings(reposInDB)
i, j := 0, 0
repoInR, repoInD := "", ""
for i < len(reposInRegistry) && j < len(reposInDB) {
repoInR = reposInRegistry[i]
repoInD = reposInDB[j]
d := strings.Compare(repoInR, repoInD)
if d < 0 {
i++
exist, err := projectExists(repoInR)
if err != nil {
log.Errorf("failed to check the existence of project %s: %v", repoInR, err)
continue
}
if !exist {
continue
}
// TODO remove the workaround when the bug of registry is fixed
// TODO read it from config
endpoint := os.Getenv("REGISTRY_URL")
client, err := cache.NewRepositoryClient(endpoint, true,
"admin", repoInR, "repository", repoInR)
if err != nil {
return needsAdd, needsDel, err
}
exist, err = repositoryExist(repoInR, client)
if err != nil {
return needsAdd, needsDel, err
}
if !exist {
continue
}
needsAdd = append(needsAdd, repoInR)
} else if d > 0 {
needsDel = append(needsDel, repoInD)
j++
} else {
i++
j++
}
}
for i < len(reposInRegistry) {
repoInR = reposInRegistry[i]
i++
exist, err := projectExists(repoInR)
if err != nil {
log.Errorf("failed to check whether project of %s exists: %v", repoInR, err)
continue
}
if !exist {
continue
}
needsAdd = append(needsAdd, repoInR)
}
for j < len(reposInDB) {
needsDel = append(needsDel, reposInDB[j])
j++
}
return needsAdd, needsDel, nil
}
func projectExists(repository string) (bool, error) {
project, _ := utils.ParseRepository(repository)
return dao.ProjectExists(project)
}
func initRegistryClient() (r *registry.Registry, err error) {
endpoint := os.Getenv("REGISTRY_URL")
addr := endpoint
if strings.Contains(endpoint, "/") {
addr = endpoint[strings.LastIndex(endpoint, "/")+1:]
}
ch := make(chan int, 1)
go func() {
var err error
var c net.Conn
for {
c, err = net.DialTimeout("tcp", addr, 20*time.Second)
if err == nil {
c.Close()
ch <- 1
} else {
log.Errorf("failed to connect to registry client, retry after 2 seconds :%v", err)
time.Sleep(2 * time.Second)
}
}
}()
select {
case <-ch:
case <-time.After(60 * time.Second):
panic("Failed to connect to registry client after 60 seconds")
}
registryClient, err := cache.NewRegistryClient(endpoint, true, "admin",
"registry", "catalog", "*")
if err != nil {
return nil, err
}
return registryClient, nil
}
func buildReplicationURL() string { func buildReplicationURL() string {
url := getJobServiceURL() url := getJobServiceURL()
return fmt.Sprintf("%s/api/jobs/replication", url) return fmt.Sprintf("%s/api/jobs/replication", url)
@ -233,3 +467,40 @@ func getJobServiceURL() string {
return url return url
} }
func getReposByProject(name string, keyword ...string) ([]string, error) {
repositories := []string{}
repos, err := dao.GetRepositoryByProjectName(name)
if err != nil {
return repositories, err
}
needMatchKeyword := len(keyword) > 0 && len(keyword[0]) != 0
for _, repo := range repos {
if needMatchKeyword &&
!strings.Contains(repo.Name, keyword[0]) {
continue
}
repositories = append(repositories, repo.Name)
}
return repositories, nil
}
func getAllRepos() ([]string, error) {
return cache.GetRepoFromCache()
}
func repositoryExist(name string, client *registry.Repository) (bool, error) {
tags, err := client.ListTag()
if err != nil {
if regErr, ok := err.(*registry_error.Error); ok && regErr.StatusCode == http.StatusNotFound {
return false, nil
}
return false, err
}
return len(tags) != 0, nil
}

9
auth/auth_test.go Normal file
View File

@ -0,0 +1,9 @@
package auth
import (
"testing"
)
func TestMain(t *testing.T) {
}

9
auth/db/db_test.go Normal file
View File

@ -0,0 +1,9 @@
package db
import (
"testing"
)
func TestMain(t *testing.T) {
}

View File

@ -40,62 +40,96 @@ const metaChars = "&|!=~*<>()"
// be associated to other entities in the system. // be associated to other entities in the system.
func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) { func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
ldapURL := os.Getenv("LDAP_URL")
if ldapURL == "" {
return nil, errors.New("Can not get any available LDAP_URL.")
}
log.Debug("ldapURL:", ldapURL)
p := m.Principal p := m.Principal
for _, c := range metaChars { for _, c := range metaChars {
if strings.ContainsRune(p, c) { if strings.ContainsRune(p, c) {
return nil, fmt.Errorf("the principal contains meta char: %q", c) return nil, fmt.Errorf("the principal contains meta char: %q", c)
} }
} }
ldapURL := os.Getenv("LDAP_URL")
if ldapURL == "" {
return nil, errors.New("Can not get any available LDAP_URL.")
}
log.Debug("ldapURL:", ldapURL)
ldap, err := openldap.Initialize(ldapURL) ldap, err := openldap.Initialize(ldapURL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ldap.SetOption(openldap.LDAP_OPT_PROTOCOL_VERSION, openldap.LDAP_VERSION3) ldap.SetOption(openldap.LDAP_OPT_PROTOCOL_VERSION, openldap.LDAP_VERSION3)
ldapBaseDn := os.Getenv("LDAP_BASE_DN") ldapBaseDn := os.Getenv("LDAP_BASE_DN")
if ldapBaseDn == "" { if ldapBaseDn == "" {
return nil, errors.New("Can not get any available LDAP_BASE_DN.") return nil, errors.New("Can not get any available LDAP_BASE_DN.")
} }
log.Debug("baseDn:", ldapBaseDn)
baseDn := fmt.Sprintf(ldapBaseDn, m.Principal) ldapSearchDn := os.Getenv("LDAP_SEARCH_DN")
log.Debug("baseDn:", baseDn) if ldapSearchDn != "" {
log.Debug("Search DN: ", ldapSearchDn)
ldapSearchPwd := os.Getenv("LDAP_SEARCH_PWD")
err = ldap.Bind(ldapSearchDn, ldapSearchPwd)
if err != nil {
log.Debug("Bind search dn error", err)
return nil, err
}
}
err = ldap.Bind(baseDn, m.Password) attrName := os.Getenv("LDAP_UID")
filter := os.Getenv("LDAP_FILTER")
if filter != "" {
filter = "(&" + filter + "(" + attrName + "=" + m.Principal + "))"
} else {
filter = "(" + attrName + "=" + m.Principal + ")"
}
log.Debug("one or more filter", filter)
ldapScope := os.Getenv("LDAP_SCOPE")
var scope int
if ldapScope == "1" {
scope = openldap.LDAP_SCOPE_BASE
} else if ldapScope == "2" {
scope = openldap.LDAP_SCOPE_ONELEVEL
} else {
scope = openldap.LDAP_SCOPE_SUBTREE
}
attributes := []string{"uid", "cn", "mail", "email"}
result, err := ldap.SearchAll(ldapBaseDn, scope, filter, attributes)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(result.Entries()) == 0 {
log.Warningf("Not found an entry.")
return nil, nil
} else if len(result.Entries()) != 1 {
log.Warningf("Found more than one entry.")
return nil, nil
}
en := result.Entries()[0]
bindDN := en.Dn()
log.Debug("found entry:", en)
err = ldap.Bind(bindDN, m.Password)
if err != nil {
log.Debug("Bind user error", err)
return nil, err
}
defer ldap.Close() defer ldap.Close()
scope := openldap.LDAP_SCOPE_SUBTREE // LDAP_SCOPE_BASE, LDAP_SCOPE_ONELEVEL, LDAP_SCOPE_SUBTREE
filter := "objectClass=*"
attributes := []string{"mail"}
result, err := ldap.SearchAll(baseDn, scope, filter, attributes)
if err != nil {
return nil, err
}
u := models.User{} u := models.User{}
if len(result.Entries()) == 1 { for _, attr := range en.Attributes() {
en := result.Entries()[0] val := attr.Values()[0]
for _, attr := range en.Attributes() { switch attr.Name() {
val := attr.Values()[0] case "uid":
if attr.Name() == "mail" { u.Realname = val
u.Email = val case "cn":
} u.Realname = val
case "mail":
u.Email = val
case "email":
u.Email = val
} }
} }
u.Username = m.Principal u.Username = m.Principal
log.Debug("username:", u.Username, ",email:", u.Email) log.Debug("username:", u.Username, ",email:", u.Email)
exist, err := dao.UserExists(u, "username") exist, err := dao.UserExists(u, "username")
if err != nil { if err != nil {
return nil, err return nil, err

9
auth/ldap/ldap_test.go Normal file
View File

@ -0,0 +1,9 @@
package ldap
import (
"testing"
)
func TestMain(t *testing.T) {
}

View File

@ -0,0 +1,206 @@
package controllers
import (
"net/http"
"net/http/httptest"
//"net/url"
"path/filepath"
"runtime"
"testing"
"fmt"
"strings"
"github.com/astaxie/beego"
//"github.com/dghubble/sling"
"github.com/stretchr/testify/assert"
)
//const (
// adminName = "admin"
// adminPwd = "Harbor12345"
//)
//type usrInfo struct {
// Name string
// Passwd string
//}
//var admin *usrInfo
func init() {
_, file, _, _ := runtime.Caller(1)
apppath, _ := filepath.Abs(filepath.Dir(filepath.Join(file, ".."+string(filepath.Separator))))
beego.BConfig.WebConfig.Session.SessionOn = true
beego.TestBeegoInit(apppath)
beego.AddTemplateExt("htm")
beego.Router("/", &IndexController{})
beego.Router("/dashboard", &DashboardController{})
beego.Router("/project", &ProjectController{})
beego.Router("/repository", &RepositoryController{})
beego.Router("/sign_up", &SignUpController{})
beego.Router("/add_new", &AddNewController{})
beego.Router("/account_setting", &AccountSettingController{})
beego.Router("/change_password", &ChangePasswordController{})
beego.Router("/admin_option", &AdminOptionController{})
beego.Router("/forgot_password", &ForgotPasswordController{})
beego.Router("/reset_password", &ResetPasswordController{})
beego.Router("/search", &SearchController{})
beego.Router("/login", &CommonController{}, "post:Login")
beego.Router("/log_out", &CommonController{}, "get:LogOut")
beego.Router("/reset", &CommonController{}, "post:ResetPassword")
beego.Router("/userExists", &CommonController{}, "post:UserExists")
beego.Router("/sendEmail", &CommonController{}, "get:SendEmail")
beego.Router("/language", &CommonController{}, "get:SwitchLanguage")
beego.Router("/optional_menu", &OptionalMenuController{})
beego.Router("/navigation_header", &NavigationHeaderController{})
beego.Router("/navigation_detail", &NavigationDetailController{})
beego.Router("/sign_in", &SignInController{})
//Init user Info
//admin = &usrInfo{adminName, adminPwd}
}
// TestMain is a sample to run an endpoint test
func TestMain(t *testing.T) {
assert := assert.New(t)
// v := url.Values{}
// v.Set("principal", "admin")
// v.Add("password", "Harbor12345")
r, _ := http.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
assert.Equal(int(200), w.Code, "'/' httpStatusCode should be 200")
assert.Equal(true, strings.Contains(fmt.Sprintf("%s", w.Body), "<title>page_title_index</title>"), "http respond should have '<title>page_title_index</title>'")
r, _ = http.NewRequest("GET", "/dashboard", nil)
w = httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
assert.Equal(int(200), w.Code, "'/dashboard' httpStatusCode should be 200")
assert.Equal(true, strings.Contains(fmt.Sprintf("%s", w.Body), "<title>page_title_dashboard</title>"), "http respond should have '<title>page_title_dashboard</title>'")
r, _ = http.NewRequest("GET", "/project", nil)
w = httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
assert.Equal(int(200), w.Code, "'/project' httpStatusCode should be 200")
assert.Equal(true, strings.Contains(fmt.Sprintf("%s", w.Body), "<title>page_title_project</title>"), "http respond should have '<title>page_title_project</title>'")
r, _ = http.NewRequest("GET", "/repository", nil)
w = httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
assert.Equal(int(200), w.Code, "'/repository' httpStatusCode should be 200")
assert.Equal(true, strings.Contains(fmt.Sprintf("%s", w.Body), "<title>page_title_repository</title>"), "http respond should have '<title>page_title_repository</title>'")
r, _ = http.NewRequest("GET", "/sign_up", nil)
w = httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
assert.Equal(int(200), w.Code, "'/sign_up' httpStatusCode should be 200")
assert.Equal(true, strings.Contains(fmt.Sprintf("%s", w.Body), "<title>page_title_sign_up</title>"), "http respond should have '<title>page_title_sign_up</title>'")
r, _ = http.NewRequest("GET", "/add_new", nil)
w = httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
assert.Equal(int(401), w.Code, "'/add_new' httpStatusCode should be 401")
r, _ = http.NewRequest("GET", "/account_setting", nil)
w = httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
assert.Equal(int(200), w.Code, "'/account_setting' httpStatusCode should be 200")
assert.Equal(true, strings.Contains(fmt.Sprintf("%s", w.Body), "<title>page_title_account_setting</title>"), "http respond should have '<title>page_title_account_setting</title>'")
r, _ = http.NewRequest("GET", "/change_password", nil)
w = httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
assert.Equal(int(200), w.Code, "'/change_password' httpStatusCode should be 200")
assert.Equal(true, strings.Contains(fmt.Sprintf("%s", w.Body), "<title>page_title_change_password</title>"), "http respond should have '<title>page_title_change_password</title>'")
r, _ = http.NewRequest("GET", "/admin_option", nil)
w = httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
assert.Equal(int(200), w.Code, "'/admin_option' httpStatusCode should be 200")
assert.Equal(true, strings.Contains(fmt.Sprintf("%s", w.Body), "<title>page_title_admin_option</title>"), "http respond should have '<title>page_title_admin_option</title>'")
r, _ = http.NewRequest("GET", "/forgot_password", nil)
w = httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
assert.Equal(int(200), w.Code, "'/forgot_password' httpStatusCode should be 200")
assert.Equal(true, strings.Contains(fmt.Sprintf("%s", w.Body), "<title>page_title_forgot_password</title>"), "http respond should have '<title>page_title_forgot_password</title>'")
r, _ = http.NewRequest("GET", "/reset_password", nil)
w = httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
assert.Equal(int(302), w.Code, "'/reset_password' httpStatusCode should be 302")
r, _ = http.NewRequest("GET", "/search", nil)
w = httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
assert.Equal(int(200), w.Code, "'/search' httpStatusCode should be 200")
assert.Equal(true, strings.Contains(fmt.Sprintf("%s", w.Body), "<title>page_title_search</title>"), "http respond should have '<title>page_title_searc</title>'")
r, _ = http.NewRequest("POST", "/login", nil)
w = httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
assert.Equal(int(401), w.Code, "'/login' httpStatusCode should be 401")
r, _ = http.NewRequest("GET", "/log_out", nil)
w = httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
assert.Equal(int(200), w.Code, "'/log_out' httpStatusCode should be 200")
assert.Equal(true, strings.Contains(fmt.Sprintf("%s", w.Body), ""), "http respond should be empty")
r, _ = http.NewRequest("POST", "/reset", nil)
w = httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
assert.Equal(int(400), w.Code, "'/reset' httpStatusCode should be 400")
r, _ = http.NewRequest("POST", "/userExists", nil)
w = httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
assert.Equal(int(500), w.Code, "'/userExists' httpStatusCode should be 500")
r, _ = http.NewRequest("GET", "/sendEmail", nil)
w = httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
assert.Equal(int(400), w.Code, "'/sendEmail' httpStatusCode should be 400")
r, _ = http.NewRequest("GET", "/language", nil)
w = httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
assert.Equal(int(302), w.Code, "'/language' httpStatusCode should be 302")
r, _ = http.NewRequest("GET", "/optional_menu", nil)
w = httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
//fmt.Printf("/optional_menu: %s\n", w.Body)
assert.Equal(int(200), w.Code, "'/optional_menu' httpStatusCode should be 200")
//assert.Equal(true, strings.Contains(fmt.Sprintf("%s", w.Body), "<title> </title>"), "http respond should have '<title> </title>'")
r, _ = http.NewRequest("GET", "/navigation_header", nil)
w = httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
//fmt.Printf("/navigation_header: %s\n", w.Body)
assert.Equal(int(200), w.Code, "'/navigation_header' httpStatusCode should be 200")
//assert.Equal(true, strings.Contains(fmt.Sprintf("%s", w.Body), "<title> </title>"), "http respond should have '<title> </title>'")
r, _ = http.NewRequest("GET", "/navigation_detail", nil)
w = httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
//fmt.Printf("/navigation_detail: %s\n", w.Body)
assert.Equal(int(200), w.Code, "'/navigation_detail' httpStatusCode should be 200")
//assert.Equal(true, strings.Contains(fmt.Sprintf("%s", w.Body), "<title> </title>"), "http respond should have '<title> </title>'")
r, _ = http.NewRequest("GET", "/sign_in", nil)
w = httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
//fmt.Printf("/sign_in: %s\n", w.Body)
assert.Equal(int(200), w.Code, "'/sign_in' httpStatusCode should be 200")
//assert.Equal(true, strings.Contains(fmt.Sprintf("%s", w.Body), "<title> </title>"), "http respond should have '<title> </title>'")
}

View File

@ -38,39 +38,84 @@ func AddAccessLog(accessLog models.AccessLog) error {
return err return err
} }
//GetAccessLogs gets access logs according to different conditions // GetTotalOfAccessLogs ...
func GetAccessLogs(accessLog models.AccessLog) ([]models.AccessLog, error) { func GetTotalOfAccessLogs(query models.AccessLog) (int64, error) {
o := GetOrmer() o := GetOrmer()
sql := `select a.log_id, u.username, a.repo_name, a.repo_tag, a.operation, a.op_time
from access_log a left join user u on a.user_id = u.user_id
where a.project_id = ? `
queryParam := make([]interface{}, 1)
queryParam = append(queryParam, accessLog.ProjectID)
if accessLog.UserID != 0 { queryParam := []interface{}{}
sql += ` and a.user_id = ? `
queryParam = append(queryParam, accessLog.UserID) sql := `select count(*) from access_log al
where al.project_id = ?`
queryParam = append(queryParam, query.ProjectID)
if query.Username != "" {
sql = `select count(*) from access_log al
left join user u
on al.user_id = u.user_id
where al.project_id = ? and u.username like ? `
queryParam = append(queryParam, "%"+query.Username+"%")
} }
if accessLog.Operation != "" {
sql += ` and a.operation = ? ` sql += genFilterClauses(query, &queryParam)
queryParam = append(queryParam, accessLog.Operation)
var total int64
if err := o.Raw(sql, queryParam).QueryRow(&total); err != nil {
return 0, err
} }
if accessLog.Username != "" { return total, nil
}
//GetAccessLogs gets access logs according to different conditions
func GetAccessLogs(query models.AccessLog, limit, offset int64) ([]models.AccessLog, error) {
o := GetOrmer()
queryParam := []interface{}{}
sql := `select al.log_id, u.username, al.repo_name,
al.repo_tag, al.operation, al.op_time
from access_log al
left join user u
on al.user_id = u.user_id
where al.project_id = ? `
queryParam = append(queryParam, query.ProjectID)
if query.Username != "" {
sql += ` and u.username like ? ` sql += ` and u.username like ? `
queryParam = append(queryParam, accessLog.Username) queryParam = append(queryParam, "%"+query.Username+"%")
} }
if accessLog.RepoName != "" {
sql += ` and a.repo_name = ? ` sql += genFilterClauses(query, &queryParam)
queryParam = append(queryParam, accessLog.RepoName)
sql += ` order by al.op_time desc `
sql = paginateForRawSQL(sql, limit, offset)
logs := []models.AccessLog{}
_, err := o.Raw(sql, queryParam).QueryRows(&logs)
if err != nil {
return logs, err
} }
if accessLog.RepoTag != "" {
sql += ` and a.repo_tag = ? ` return logs, nil
queryParam = append(queryParam, accessLog.RepoTag) }
func genFilterClauses(query models.AccessLog, queryParam *[]interface{}) string {
sql := ""
if query.Operation != "" {
sql += ` and al.operation = ? `
*queryParam = append(*queryParam, query.Operation)
} }
if accessLog.Keywords != "" { if query.RepoName != "" {
sql += ` and a.operation in ( ` sql += ` and al.repo_name = ? `
keywordList := strings.Split(accessLog.Keywords, "/") *queryParam = append(*queryParam, query.RepoName)
}
if query.RepoTag != "" {
sql += ` and al.repo_tag = ? `
*queryParam = append(*queryParam, query.RepoTag)
}
if query.Keywords != "" {
sql += ` and al.operation in ( `
keywordList := strings.Split(query.Keywords, "/")
num := len(keywordList) num := len(keywordList)
for i := 0; i < num; i++ { for i := 0; i < num; i++ {
if keywordList[i] != "" { if keywordList[i] != "" {
@ -79,27 +124,20 @@ func GetAccessLogs(accessLog models.AccessLog) ([]models.AccessLog, error) {
} else { } else {
sql += `?,` sql += `?,`
} }
queryParam = append(queryParam, keywordList[i]) *queryParam = append(*queryParam, keywordList[i])
} }
} }
} }
if accessLog.BeginTimestamp > 0 { if query.BeginTimestamp > 0 {
sql += ` and a.op_time >= ? ` sql += ` and al.op_time >= ? `
queryParam = append(queryParam, accessLog.BeginTime) *queryParam = append(*queryParam, query.BeginTime)
} }
if accessLog.EndTimestamp > 0 { if query.EndTimestamp > 0 {
sql += ` and a.op_time <= ? ` sql += ` and al.op_time <= ? `
queryParam = append(queryParam, accessLog.EndTime) *queryParam = append(*queryParam, query.EndTime)
} }
sql += ` order by a.op_time desc ` return sql
var accessLogList []models.AccessLog
_, err := o.Raw(sql, queryParam).QueryRows(&accessLogList)
if err != nil {
return nil, err
}
return accessLogList, nil
} }
// AccessLog ... // AccessLog ...
@ -118,18 +156,48 @@ func AccessLog(username, projectName, repoName, repoTag, action string) error {
//GetRecentLogs returns recent logs according to parameters //GetRecentLogs returns recent logs according to parameters
func GetRecentLogs(userID, linesNum int, startTime, endTime string) ([]models.AccessLog, error) { func GetRecentLogs(userID, linesNum int, startTime, endTime string) ([]models.AccessLog, error) {
var recentLogList []models.AccessLog logs := []models.AccessLog{}
queryParam := make([]interface{}, 1)
isAdmin, err := IsAdminRole(userID)
if err != nil {
return logs, err
}
queryParam := []interface{}{}
sql := `select log_id, access_log.user_id, project_id, repo_name, repo_tag, GUID, operation, op_time, username
from access_log
join user
on access_log.user_id=user.user_id `
hasWhere := false
if !isAdmin {
sql += ` where project_id in
(select distinct project_id
from project_member
where user_id = ?) `
queryParam = append(queryParam, userID)
hasWhere = true
}
sql := "select log_id, access_log.user_id, project_id, repo_name, repo_tag, GUID, operation, op_time, username from access_log left join user on access_log.user_id=user.user_id where project_id in (select distinct project_id from project_member where user_id = ?)"
queryParam = append(queryParam, userID)
if startTime != "" { if startTime != "" {
sql += " and op_time >= ?" if hasWhere {
sql += " and op_time >= ?"
} else {
sql += " where op_time >= ?"
hasWhere = true
}
queryParam = append(queryParam, startTime) queryParam = append(queryParam, startTime)
} }
if endTime != "" { if endTime != "" {
sql += " and op_time <= ?" if hasWhere {
sql += " and op_time <= ?"
} else {
sql += " where op_time <= ?"
hasWhere = true
}
queryParam = append(queryParam, endTime) queryParam = append(queryParam, endTime)
} }
@ -138,56 +206,39 @@ func GetRecentLogs(userID, linesNum int, startTime, endTime string) ([]models.Ac
sql += " limit ?" sql += " limit ?"
queryParam = append(queryParam, linesNum) queryParam = append(queryParam, linesNum)
} }
o := GetOrmer()
_, err := o.Raw(sql, queryParam).QueryRows(&recentLogList) _, err = GetOrmer().Raw(sql, queryParam).QueryRows(&logs)
if err != nil { if err != nil {
return nil, err return logs, err
} }
return recentLogList, nil return logs, nil
} }
//GetTopRepos return top accessed public repos // GetAccessLogCreator ...
func GetTopRepos(countNum int) ([]models.TopRepo, error) { func GetAccessLogCreator(repoName string) (string, error) {
o := GetOrmer() o := GetOrmer()
// hide the where condition: project.public = 1, Can add to the sql when necessary. sql := "select * from user where user_id = (select user_id from access_log where operation = 'push' and repo_name = ? order by op_time desc limit 1)"
sql := "select repo_name, COUNT(repo_name) as access_count from access_log left join project on access_log.project_id=project.project_id where access_log.operation = 'pull' group by repo_name order by access_count desc limit ? "
queryParam := []interface{}{} var u []models.User
queryParam = append(queryParam, countNum) n, err := o.Raw(sql, repoName).QueryRows(&u)
var list []models.TopRepo
_, err := o.Raw(sql, queryParam).QueryRows(&list)
if err != nil { if err != nil {
return nil, err return "", err
} }
if len(list) == 0 { if n == 0 {
return list, nil return "", nil
} }
placeHolder := make([]string, len(list))
repos := make([]string, len(list)) return u[0].Username, nil
for i, v := range list { }
repos[i] = v.RepoName
placeHolder[i] = "?" // CountPull ...
} func CountPull(repoName string) (int64, error) {
placeHolderStr := strings.Join(placeHolder, ",") o := GetOrmer()
queryParam = nil num, err := o.QueryTable("access_log").Filter("repo_name", repoName).Filter("operation", "pull").Count()
queryParam = append(queryParam, repos) if err != nil {
var usrnameList []models.TopRepo log.Errorf("error in CountPull: %v ", err)
sql = `select a.username as creator, a.repo_name from (select access_log.repo_name, user.username, return 0, err
access_log.op_time from user left join access_log on user.user_id = access_log.user_id where }
access_log.operation = 'push' and access_log.repo_name in (######) order by access_log.repo_name, return num, nil
access_log.op_time ASC) a group by a.repo_name`
sql = strings.Replace(sql, "######", placeHolderStr, 1)
_, err = o.Raw(sql, queryParam).QueryRows(&usrnameList)
if err != nil {
return nil, err
}
for i := 0; i < len(list); i++ {
for _, v := range usrnameList {
if v.RepoName == list[i].RepoName {
// list[i].Creator = v.Creator
break
}
}
}
return list, nil
} }

View File

@ -16,6 +16,7 @@
package dao package dao
import ( import (
"fmt"
"net" "net"
"os" "os"
@ -44,7 +45,7 @@ func GenerateRandomString() (string, error) {
//InitDB initializes the database //InitDB initializes the database
func InitDB() { func InitDB() {
// orm.Debug = true // orm.Debug = true
orm.RegisterDriver("mysql", orm.DRMySQL) orm.RegisterDriver("mysql", orm.DRMySQL)
addr := os.Getenv("MYSQL_HOST") addr := os.Getenv("MYSQL_HOST")
port := os.Getenv("MYSQL_PORT") port := os.Getenv("MYSQL_PORT")
@ -89,3 +90,7 @@ func GetOrmer() orm.Ormer {
}) })
return globalOrm return globalOrm
} }
func paginateForRawSQL(sql string, limit, offset int64) string {
return fmt.Sprintf("%s limit %d offset %d", sql, limit, offset)
}

View File

@ -16,6 +16,7 @@
package dao package dao
import ( import (
"fmt"
"os" "os"
"testing" "testing"
"time" "time"
@ -112,7 +113,9 @@ func clearUp(username string) {
} }
const username string = "Tester01" const username string = "Tester01"
const password string = "Abc12345"
const projectName string = "test_project" const projectName string = "test_project"
const repositoryName string = "test_repository"
const repoTag string = "test1.1" const repoTag string = "test1.1"
const repoTag2 string = "test1.2" const repoTag2 string = "test1.2"
const SysAdmin int = 1 const SysAdmin int = 1
@ -157,7 +160,7 @@ func TestRegister(t *testing.T) {
user := models.User{ user := models.User{
Username: username, Username: username,
Email: "tester01@vmware.com", Email: "tester01@vmware.com",
Password: "Abc12345", Password: password,
Realname: "tester01", Realname: "tester01",
Comment: "register", Comment: "register",
} }
@ -184,6 +187,41 @@ func TestRegister(t *testing.T) {
} }
} }
func TestCheckUserPassword(t *testing.T) {
nonExistUser := models.User{
Username: "non-exist",
}
correctUser := models.User{
Username: username,
Password: password,
}
wrongPwd := models.User{
Username: username,
Password: "wrong",
}
u, err := CheckUserPassword(nonExistUser)
if err != nil {
t.Errorf("Failed in CheckUserPassword: %v", err)
}
if u != nil {
t.Errorf("Expected nil for Non exist user, but actual: %+v", u)
}
u, err = CheckUserPassword(wrongPwd)
if err != nil {
t.Errorf("Failed in CheckUserPassword: %v", err)
}
if u != nil {
t.Errorf("Expected nil for user with wrong password, but actual: %+v", u)
}
u, err = CheckUserPassword(correctUser)
if err != nil {
t.Errorf("Failed in CheckUserPassword: %v", err)
}
if u == nil {
t.Errorf("User should not be nil for correct user")
}
}
func TestUserExists(t *testing.T) { func TestUserExists(t *testing.T) {
var exists bool var exists bool
var err error var err error
@ -420,7 +458,7 @@ func TestGetAccessLog(t *testing.T) {
UserID: currentUser.UserID, UserID: currentUser.UserID,
ProjectID: currentProject.ProjectID, ProjectID: currentProject.ProjectID,
} }
accessLogs, err := GetAccessLogs(queryAccessLog) accessLogs, err := GetAccessLogs(queryAccessLog, 1000, 0)
if err != nil { if err != nil {
t.Errorf("Error occurred in GetAccessLog: %v", err) t.Errorf("Error occurred in GetAccessLog: %v", err)
} }
@ -432,6 +470,21 @@ func TestGetAccessLog(t *testing.T) {
} }
} }
func TestGetTotalOfAccessLogs(t *testing.T) {
queryAccessLog := models.AccessLog{
UserID: currentUser.UserID,
ProjectID: currentProject.ProjectID,
}
total, err := GetTotalOfAccessLogs(queryAccessLog)
if err != nil {
t.Fatalf("failed to get total of access log: %v", err)
}
if total != 1 {
t.Errorf("unexpected total %d != %d", total, 1)
}
}
func TestAddAccessLog(t *testing.T) { func TestAddAccessLog(t *testing.T) {
var err error var err error
var accessLogList []models.AccessLog var accessLogList []models.AccessLog
@ -448,7 +501,7 @@ func TestAddAccessLog(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("Error occurred in AddAccessLog: %v", err) t.Errorf("Error occurred in AddAccessLog: %v", err)
} }
accessLogList, err = GetAccessLogs(accessLog) accessLogList, err = GetAccessLogs(accessLog, 1000, 0)
if err != nil { if err != nil {
t.Errorf("Error occurred in GetAccessLog: %v", err) t.Errorf("Error occurred in GetAccessLog: %v", err)
} }
@ -477,7 +530,7 @@ func TestAccessLog(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("Error occurred in AccessLog: %v", err) t.Errorf("Error occurred in AccessLog: %v", err)
} }
accessLogList, err = GetAccessLogs(accessLog) accessLogList, err = GetAccessLogs(accessLog, 1000, 0)
if err != nil { if err != nil {
t.Errorf("Error occurred in GetAccessLog: %v", err) t.Errorf("Error occurred in GetAccessLog: %v", err)
} }
@ -492,6 +545,50 @@ func TestAccessLog(t *testing.T) {
} }
} }
func TestGetAccessLogCreator(t *testing.T) {
var err error
err = AccessLog(currentUser.Username, currentProject.Name, currentProject.Name+"/tomcat", repoTag2, "push")
if err != nil {
t.Errorf("Error occurred in AccessLog: %v", err)
}
err = AccessLog(currentUser.Username, currentProject.Name, currentProject.Name+"/tomcat", repoTag2, "push")
if err != nil {
t.Errorf("Error occurred in AccessLog: %v", err)
}
user, err := GetAccessLogCreator(currentProject.Name + "/tomcat")
if err != nil {
t.Errorf("Error occurred in GetAccessLogCreator: %v", err)
}
if user != currentUser.Username {
t.Errorf("The access log creator does not match, expected: %s, actual: %s", currentUser.Username, user)
}
}
func TestCountPull(t *testing.T) {
var err error
err = AccessLog(currentUser.Username, currentProject.Name, currentProject.Name+"/tomcat", repoTag2, "pull")
if err != nil {
t.Errorf("Error occurred in AccessLog: %v", err)
}
err = AccessLog(currentUser.Username, currentProject.Name, currentProject.Name+"/tomcat", repoTag2, "pull")
if err != nil {
t.Errorf("Error occurred in AccessLog: %v", err)
}
err = AccessLog(currentUser.Username, currentProject.Name, currentProject.Name+"/tomcat", repoTag2, "pull")
if err != nil {
t.Errorf("Error occurred in AccessLog: %v", err)
}
pullCount, err := CountPull(currentProject.Name + "/tomcat")
if err != nil {
t.Errorf("Error occurred in CountPull: %v", err)
}
if pullCount != 3 {
t.Errorf("The access log pull count does not match, expected: 3, actual: %d", pullCount)
}
}
func TestProjectExists(t *testing.T) { func TestProjectExists(t *testing.T) {
var exists bool var exists bool
var err error var err error
@ -609,6 +706,17 @@ func TestProjectPermission(t *testing.T) {
} }
} }
func TestGetTotalOfUserRelevantProjects(t *testing.T) {
total, err := GetTotalOfUserRelevantProjects(currentUser.UserID, "")
if err != nil {
t.Fatalf("failed to get total of user relevant projects: %v", err)
}
if total != 1 {
t.Errorf("unexpected total: %d != 1", total)
}
}
func TestGetUserRelevantProjects(t *testing.T) { func TestGetUserRelevantProjects(t *testing.T) {
projects, err := GetUserRelevantProjects(currentUser.UserID, "") projects, err := GetUserRelevantProjects(currentUser.UserID, "")
if err != nil { if err != nil {
@ -622,8 +730,19 @@ func TestGetUserRelevantProjects(t *testing.T) {
} }
} }
func TestGetAllProjects(t *testing.T) { func TestGetTotalOfProjects(t *testing.T) {
projects, err := GetAllProjects("") total, err := GetTotalOfProjects("")
if err != nil {
t.Fatalf("failed to get total of projects: %v", err)
}
if total != 2 {
t.Errorf("unexpected total: %d != 2", total)
}
}
func TestGetProjects(t *testing.T) {
projects, err := GetProjects("")
if err != nil { if err != nil {
t.Errorf("Error occurred in GetAllProjects: %v", err) t.Errorf("Error occurred in GetAllProjects: %v", err)
} }
@ -636,7 +755,7 @@ func TestGetAllProjects(t *testing.T) {
} }
func TestGetPublicProjects(t *testing.T) { func TestGetPublicProjects(t *testing.T) {
projects, err := GetPublicProjects("") projects, err := GetProjects("", 1)
if err != nil { if err != nil {
t.Errorf("Error occurred in getProjects: %v", err) t.Errorf("Error occurred in getProjects: %v", err)
} }
@ -672,6 +791,21 @@ func TestAddProjectMember(t *testing.T) {
} }
} }
func TestUpdateProjectMember(t *testing.T) {
err := UpdateProjectMember(currentProject.ProjectID, 1, models.GUEST)
if err != nil {
t.Errorf("Error occurred in UpdateProjectMember: %v", err)
}
roles, err := GetUserProjectRoles(1, currentProject.ProjectID)
if err != nil {
t.Errorf("Error occurred in GetUserProjectRoles: %v", err)
}
if roles[0].Name != "guest" {
t.Errorf("The user with ID 1 is not guest role after update, the acutal role: %s", roles[0].Name)
}
}
func TestDeleteProjectMember(t *testing.T) { func TestDeleteProjectMember(t *testing.T) {
err := DeleteProjectMember(currentProject.ProjectID, 1) err := DeleteProjectMember(currentProject.ProjectID, 1)
if err != nil { if err != nil {
@ -688,6 +822,23 @@ func TestDeleteProjectMember(t *testing.T) {
} }
} }
func TestGetRoleByID(t *testing.T) {
r, err := GetRoleByID(models.PROJECTADMIN)
if err != nil {
t.Errorf("Failed to call GetRoleByID: %v", err)
}
if r == nil || r.Name != "projectAdmin" || r.RoleCode != "MDRWS" {
t.Errorf("Role does not match for role id: %d, actual: %+v", models.PROJECTADMIN, r)
}
r, err = GetRoleByID(9999)
if err != nil {
t.Errorf("Failed to call GetRoleByID: %v", err)
}
if r != nil {
t.Errorf("Role should nil for non-exist id 9999, actual: %+v", r)
}
}
func TestToggleAdminRole(t *testing.T) { func TestToggleAdminRole(t *testing.T) {
err := ToggleUserAdminRole(currentUser.UserID, 1) err := ToggleUserAdminRole(currentUser.UserID, 1)
if err != nil { if err != nil {
@ -747,57 +898,9 @@ func TestGetRecentLogs(t *testing.T) {
} }
func TestGetTopRepos(t *testing.T) { func TestGetTopRepos(t *testing.T) {
_, err := GetTopRepos(10)
err := ToggleProjectPublicity(currentProject.ProjectID, publicityOn)
if err != nil { if err != nil {
t.Errorf("Error occurred in ToggleProjectPublicity: %v", err) t.Fatalf("error occured in getting top repos, error: %v", err)
}
err = AccessLog(currentUser.Username, currentProject.Name, currentProject.Name+"/ubuntu", repoTag2, "push")
if err != nil {
t.Errorf("Error occurred in AccessLog: %v", err)
}
err = AccessLog(currentUser.Username, currentProject.Name, currentProject.Name+"/ubuntu", repoTag2, "pull")
if err != nil {
t.Errorf("Error occurred in AccessLog: %v", err)
}
topRepos, err := GetTopRepos(10)
if err != nil {
t.Errorf("error occured in getting top repos, error: %v", err)
}
if topRepos[0].RepoName != currentProject.Name+"/ubuntu" {
t.Errorf("error occured in get top reop's name, expected: %v, actual: %v", currentProject.Name+"/ubuntu", topRepos[0].RepoName)
}
if topRepos[0].AccessCount != 1 {
t.Errorf("error occured in get top reop's access count, expected: %v, actual: %v", 1, topRepos[0].AccessCount)
}
/*
if topRepos[0].Creator != currentUser.Username {
t.Errorf("error occured in get top reop's creator, expected: %v, actual: %v", currentUser.Username, topRepos[0].Creator)
}
*/
err = ToggleProjectPublicity(currentProject.ProjectID, publicityOff)
if err != nil {
t.Errorf("Error occurred in ToggleProjectPublicity: %v", err)
}
o := GetOrmer()
_, err = o.QueryTable("access_log").Filter("operation__in", "push,pull").Delete()
if err != nil {
t.Errorf("error occurred in deleting access logs, %v", err)
}
}
func TestDeleteUser(t *testing.T) {
err := DeleteUser(currentUser.UserID)
if err != nil {
t.Errorf("Error occurred in DeleteUser: %v", err)
}
user, err := GetUser(*currentUser)
if err != nil {
t.Errorf("Error occurred in GetUser: %v", err)
}
if user != nil {
t.Errorf("user is not nil after deletion, user: %+v", user)
} }
} }
@ -1180,7 +1283,7 @@ func TestGetRepJobByPolicy(t *testing.T) {
} }
func TestFilterRepJobs(t *testing.T) { func TestFilterRepJobs(t *testing.T) {
jobs, err := FilterRepJobs(policyID, "", "", nil, nil, 1000) jobs, _, err := FilterRepJobs(policyID, "", "", nil, nil, 1000, 0)
if err != nil { if err != nil {
t.Errorf("Error occured in FilterRepJobs: %v, policy ID: %d", err, policyID) t.Errorf("Error occured in FilterRepJobs: %v, policy ID: %d", err, policyID)
return return
@ -1308,7 +1411,7 @@ func TestDeleteRepPolicy(t *testing.T) {
if err != nil && err != orm.ErrNoRows { if err != nil && err != orm.ErrNoRows {
t.Errorf("Error occured in GetRepPolicy:%v", err) t.Errorf("Error occured in GetRepPolicy:%v", err)
} }
if p != nil { if p != nil && p.Deleted != 1 {
t.Errorf("Able to find rep policy after deletion, id: %d", policyID) t.Errorf("Able to find rep policy after deletion, id: %d", policyID)
} }
} }
@ -1390,3 +1493,113 @@ func TestGetOrmer(t *testing.T) {
t.Errorf("Error get ormer.") t.Errorf("Error get ormer.")
} }
} }
func TestDeleteProject(t *testing.T) {
name := "project_for_test"
project := models.Project{
OwnerID: currentUser.UserID,
Name: name,
}
id, err := AddProject(project)
if err != nil {
t.Fatalf("failed to add project: %v", err)
}
if err = DeleteProject(id); err != nil {
t.Fatalf("failed to delete project: %v", err)
}
p := &models.Project{}
if err = GetOrmer().Raw(`select * from project where project_id = ?`, id).
QueryRow(p); err != nil {
t.Fatalf("failed to get project: %v", err)
}
if p.Deleted != 1 {
t.Errorf("unexpeced deleted column: %d != %d", p.Deleted, 1)
}
deletedName := fmt.Sprintf("%s#%d", name, id)
if p.Name != deletedName {
t.Errorf("unexpected name: %s != %s", p.Name, deletedName)
}
}
func TestAddRepository(t *testing.T) {
repoRecord := models.RepoRecord{
Name: currentProject.Name + "/" + repositoryName,
OwnerName: currentUser.Username,
ProjectName: currentProject.Name,
Description: "testing repo",
PullCount: 0,
StarCount: 0,
}
err := AddRepository(repoRecord)
if err != nil {
t.Errorf("Error occurred in AddRepository: %v", err)
}
newRepoRecord, err := GetRepositoryByName(currentProject.Name + "/" + repositoryName)
if err != nil {
t.Errorf("Error occurred in GetRepositoryByName: %v", err)
}
if newRepoRecord == nil {
t.Errorf("No repository found queried by repository name: %v", currentProject.Name+"/"+repositoryName)
}
}
var currentRepository *models.RepoRecord
func TestGetRepositoryByName(t *testing.T) {
var err error
currentRepository, err = GetRepositoryByName(currentProject.Name + "/" + repositoryName)
if err != nil {
t.Errorf("Error occurred in GetRepositoryByName: %v", err)
}
if currentRepository == nil {
t.Errorf("No repository found queried by repository name: %v", currentProject.Name+"/"+repositoryName)
}
if currentRepository.Name != currentProject.Name+"/"+repositoryName {
t.Errorf("Repository name does not match, expected: %s, actual: %s", currentProject.Name+"/"+repositoryName, currentProject.Name)
}
}
func TestIncreasePullCount(t *testing.T) {
if err := IncreasePullCount(currentRepository.Name); err != nil {
log.Errorf("Error happens when increasing pull count: %v", currentRepository.Name)
}
repository, err := GetRepositoryByName(currentRepository.Name)
if err != nil {
t.Errorf("Error occurred in GetRepositoryByName: %v", err)
}
if repository.PullCount != 1 {
t.Errorf("repository pull count is not 1 after IncreasePullCount, expected: 1, actual: %d", repository.PullCount)
}
}
func TestRepositoryExists(t *testing.T) {
var exists bool
exists = RepositoryExists(currentRepository.Name)
if !exists {
t.Errorf("The repository with name: %d, does not exist", currentRepository.Name)
}
}
func TestDeleteRepository(t *testing.T) {
err := DeleteRepository(currentRepository.Name)
if err != nil {
t.Errorf("Error occurred in DeleteRepository: %v", err)
}
repository, err := GetRepositoryByName(currentRepository.Name)
if err != nil {
t.Errorf("Error occurred in GetRepositoryByName: %v", err)
}
if repository != nil {
t.Errorf("repository is not nil after deletion, repository: %+v", repository)
}
}

View File

@ -182,66 +182,106 @@ func SearchProjects(userID int) ([]models.Project, error) {
return projects, nil return projects, nil
} }
// GetUserRelevantProjects returns the projects of the user which are not deleted and name like projectName //GetTotalOfUserRelevantProjects returns the total count of
func GetUserRelevantProjects(userID int, projectName string) ([]models.Project, error) { // user relevant projects
func GetTotalOfUserRelevantProjects(userID int, projectName string) (int64, error) {
o := GetOrmer() o := GetOrmer()
sql := `select distinct sql := `select count(*) from project p
p.project_id, p.owner_id, p.name,p.creation_time, p.update_time, p.public, pm.role role left join project_member pm
from project p on p.project_id = pm.project_id
left join project_member pm on p.project_id = pm.project_id where p.deleted = 0 and pm.user_id= ?`
where p.deleted = 0 and pm.user_id= ?`
queryParam := make([]interface{}, 1) queryParam := []interface{}{}
queryParam = append(queryParam, userID) queryParam = append(queryParam, userID)
if projectName != "" { if projectName != "" {
sql += " and p.name like ? " sql += " and p.name like ? "
queryParam = append(queryParam, projectName) queryParam = append(queryParam, "%"+projectName+"%")
} }
sql += " order by p.name "
var r []models.Project var total int64
_, err := o.Raw(sql, queryParam).QueryRows(&r) err := o.Raw(sql, queryParam).QueryRow(&total)
if err != nil {
return nil, err return total, err
}
return r, nil
} }
//GetPublicProjects returns all public projects whose name like projectName // GetUserRelevantProjects returns the user relevant projects
func GetPublicProjects(projectName string) ([]models.Project, error) { // args[0]: public, args[1]: limit, args[2]: offset
publicProjects, err := getProjects(1, projectName) func GetUserRelevantProjects(userID int, projectName string, args ...int64) ([]models.Project, error) {
if err != nil { return getProjects(userID, projectName, args...)
return nil, err
}
return publicProjects, nil
} }
// GetAllProjects returns all projects which are not deleted and name like projectName // GetTotalOfProjects returns the total count of projects
func GetAllProjects(projectName string) ([]models.Project, error) { func GetTotalOfProjects(name string, public ...int) (int64, error) {
allProjects, err := getProjects(0, projectName) qs := GetOrmer().
if err != nil { QueryTable(new(models.Project)).
return nil, err Filter("Deleted", 0)
if len(name) > 0 {
qs = qs.Filter("Name__icontains", name)
} }
return allProjects, nil
if len(public) > 0 {
qs = qs.Filter("Public", public[0])
}
return qs.Count()
} }
func getProjects(public int, projectName string) ([]models.Project, error) { // GetProjects returns project list
// args[0]: public, args[1]: limit, args[2]: offset
func GetProjects(name string, args ...int64) ([]models.Project, error) {
return getProjects(0, name, args...)
}
func getProjects(userID int, name string, args ...int64) ([]models.Project, error) {
projects := []models.Project{}
o := GetOrmer() o := GetOrmer()
sql := `select project_id, owner_id, creation_time, update_time, name, public sql := ""
from project queryParam := []interface{}{}
where deleted = 0`
queryParam := make([]interface{}, 1) if userID != 0 { //get user's projects
if public == 1 { sql = `select distinct p.project_id, p.owner_id, p.name,
sql += " and public = ? " p.creation_time, p.update_time, p.public, pm.role role
queryParam = append(queryParam, public) from project p
left join project_member pm
on p.project_id = pm.project_id
where p.deleted = 0 and pm.user_id= ?`
queryParam = append(queryParam, userID)
} else { // get all projects
sql = `select * from project p where p.deleted = 0 `
} }
if len(projectName) > 0 {
sql += " and name like ? " if name != "" {
queryParam = append(queryParam, projectName) sql += ` and p.name like ? `
queryParam = append(queryParam, "%"+name+"%")
} }
sql += " order by name "
var projects []models.Project switch len(args) {
if _, err := o.Raw(sql, queryParam).QueryRows(&projects); err != nil { case 1:
return nil, err sql += ` and p.public = ?`
queryParam = append(queryParam, args[0])
sql += ` order by p.name `
case 2:
sql += ` order by p.name `
sql = paginateForRawSQL(sql, args[0], args[1])
case 3:
sql += ` and p.public = ?`
queryParam = append(queryParam, args[0])
sql += ` order by p.name `
sql = paginateForRawSQL(sql, args[1], args[2])
} }
return projects, nil
_, err := o.Raw(sql, queryParam).QueryRows(&projects)
return projects, err
}
// DeleteProject ...
func DeleteProject(id int64) error {
sql := `update project
set deleted = 1, name = concat(name,"#",project_id)
where project_id = ?`
_, err := GetOrmer().Raw(sql, id).Exec()
return err
} }

View File

@ -155,17 +155,18 @@ func FilterRepPolicies(name string, projectID int64) ([]*models.RepPolicy, error
left join project p on rp.project_id=p.project_id left join project p on rp.project_id=p.project_id
left join replication_target rt on rp.target_id=rt.id left join replication_target rt on rp.target_id=rt.id
left join replication_job rj on rp.id=rj.policy_id and (rj.status="error" left join replication_job rj on rp.id=rj.policy_id and (rj.status="error"
or rj.status="retrying") ` or rj.status="retrying")
where rp.deleted = 0 `
if len(name) != 0 && projectID != 0 { if len(name) != 0 && projectID != 0 {
sql += `where rp.name like ? and rp.project_id = ? ` sql += `and rp.name like ? and rp.project_id = ? `
args = append(args, "%"+name+"%") args = append(args, "%"+name+"%")
args = append(args, projectID) args = append(args, projectID)
} else if len(name) != 0 { } else if len(name) != 0 {
sql += `where rp.name like ? ` sql += `and rp.name like ? `
args = append(args, "%"+name+"%") args = append(args, "%"+name+"%")
} else if projectID != 0 { } else if projectID != 0 {
sql += `where rp.project_id = ? ` sql += `and rp.project_id = ? `
args = append(args, projectID) args = append(args, projectID)
} }
@ -181,7 +182,7 @@ func FilterRepPolicies(name string, projectID int64) ([]*models.RepPolicy, error
// GetRepPolicyByName ... // GetRepPolicyByName ...
func GetRepPolicyByName(name string) (*models.RepPolicy, error) { func GetRepPolicyByName(name string) (*models.RepPolicy, error) {
o := GetOrmer() o := GetOrmer()
sql := `select * from replication_policy where name = ?` sql := `select * from replication_policy where deleted = 0 and name = ?`
var policy models.RepPolicy var policy models.RepPolicy
@ -198,7 +199,7 @@ func GetRepPolicyByName(name string) (*models.RepPolicy, error) {
// GetRepPolicyByProject ... // GetRepPolicyByProject ...
func GetRepPolicyByProject(projectID int64) ([]*models.RepPolicy, error) { func GetRepPolicyByProject(projectID int64) ([]*models.RepPolicy, error) {
o := GetOrmer() o := GetOrmer()
sql := `select * from replication_policy where project_id = ?` sql := `select * from replication_policy where deleted = 0 and project_id = ?`
var policies []*models.RepPolicy var policies []*models.RepPolicy
@ -212,7 +213,7 @@ func GetRepPolicyByProject(projectID int64) ([]*models.RepPolicy, error) {
// GetRepPolicyByTarget ... // GetRepPolicyByTarget ...
func GetRepPolicyByTarget(targetID int64) ([]*models.RepPolicy, error) { func GetRepPolicyByTarget(targetID int64) ([]*models.RepPolicy, error) {
o := GetOrmer() o := GetOrmer()
sql := `select * from replication_policy where target_id = ?` sql := `select * from replication_policy where deleted = 0 and target_id = ?`
var policies []*models.RepPolicy var policies []*models.RepPolicy
@ -226,7 +227,7 @@ func GetRepPolicyByTarget(targetID int64) ([]*models.RepPolicy, error) {
// GetRepPolicyByProjectAndTarget ... // GetRepPolicyByProjectAndTarget ...
func GetRepPolicyByProjectAndTarget(projectID, targetID int64) ([]*models.RepPolicy, error) { func GetRepPolicyByProjectAndTarget(projectID, targetID int64) ([]*models.RepPolicy, error) {
o := GetOrmer() o := GetOrmer()
sql := `select * from replication_policy where project_id = ? and target_id = ?` sql := `select * from replication_policy where deleted = 0 and project_id = ? and target_id = ?`
var policies []*models.RepPolicy var policies []*models.RepPolicy
@ -247,7 +248,11 @@ func UpdateRepPolicy(policy *models.RepPolicy) error {
// DeleteRepPolicy ... // DeleteRepPolicy ...
func DeleteRepPolicy(id int64) error { func DeleteRepPolicy(id int64) error {
o := GetOrmer() o := GetOrmer()
_, err := o.Delete(&models.RepPolicy{ID: id}) policy := &models.RepPolicy{
ID: id,
Deleted: 1,
}
_, err := o.Update(policy, "Deleted")
return err return err
} }
@ -312,12 +317,14 @@ func GetRepJobByPolicy(policyID int64) ([]*models.RepJob, error) {
return res, err return res, err
} }
// FilterRepJobs filters jobs by repo and policy ID // FilterRepJobs ...
func FilterRepJobs(policyID int64, repository, status string, startTime, func FilterRepJobs(policyID int64, repository, status string, startTime,
endTime *time.Time, limit int) ([]*models.RepJob, error) { endTime *time.Time, limit, offset int64) ([]*models.RepJob, int64, error) {
o := GetOrmer()
jobs := []*models.RepJob{}
qs := GetOrmer().QueryTable(new(models.RepJob))
qs := o.QueryTable(new(models.RepJob))
if policyID != 0 { if policyID != 0 {
qs = qs.Filter("PolicyID", policyID) qs = qs.Filter("PolicyID", policyID)
} }
@ -327,32 +334,28 @@ func FilterRepJobs(policyID int64, repository, status string, startTime,
if len(status) != 0 { if len(status) != 0 {
qs = qs.Filter("Status__icontains", status) qs = qs.Filter("Status__icontains", status)
} }
if startTime != nil { if startTime != nil {
fmt.Printf("%v\n", startTime)
qs = qs.Filter("CreationTime__gte", startTime) qs = qs.Filter("CreationTime__gte", startTime)
} }
if endTime != nil { if endTime != nil {
fmt.Printf("%v\n", endTime)
qs = qs.Filter("CreationTime__lte", endTime) qs = qs.Filter("CreationTime__lte", endTime)
} }
if limit != 0 { total, err := qs.Count()
qs = qs.Limit(limit) if err != nil {
return jobs, 0, err
} }
qs = qs.OrderBy("-UpdateTime") qs = qs.OrderBy("-UpdateTime")
var jobs []*models.RepJob _, err = qs.Limit(limit).Offset(offset).All(&jobs)
_, err := qs.All(&jobs)
if err != nil { if err != nil {
return nil, err return jobs, 0, err
} }
genTagListForJob(jobs...) genTagListForJob(jobs...)
return jobs, nil return jobs, total, nil
} }
// GetRepJobToStop get jobs that are possibly being handled by workers of a certain policy. // GetRepJobToStop get jobs that are possibly being handled by workers of a certain policy.

167
dao/repository.go Normal file
View File

@ -0,0 +1,167 @@
/*
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 dao
import (
"fmt"
"github.com/astaxie/beego/orm"
"github.com/vmware/harbor/models"
)
// AddRepository adds a repo to the database.
func AddRepository(repo models.RepoRecord) error {
o := GetOrmer()
sql := "insert into repository (owner_id, project_id, name, description, pull_count, star_count, creation_time, update_time) " +
"select (select user_id as owner_id from user where username=?), " +
"(select project_id as project_id from project where name=?), ?, ?, ?, ?, NOW(), NULL "
_, err := o.Raw(sql, repo.OwnerName, repo.ProjectName, repo.Name, repo.Description, repo.PullCount, repo.StarCount).Exec()
return err
}
// GetRepositoryByName ...
func GetRepositoryByName(name string) (*models.RepoRecord, error) {
o := GetOrmer()
r := models.RepoRecord{Name: name}
err := o.Read(&r, "Name")
if err == orm.ErrNoRows {
return nil, nil
}
return &r, err
}
// GetAllRepositories ...
func GetAllRepositories() ([]models.RepoRecord, error) {
o := GetOrmer()
var repos []models.RepoRecord
_, err := o.QueryTable("repository").All(&repos)
return repos, err
}
// DeleteRepository ...
func DeleteRepository(name string) error {
o := GetOrmer()
_, err := o.QueryTable("repository").Filter("name", name).Delete()
return err
}
// UpdateRepository ...
func UpdateRepository(repo models.RepoRecord) error {
o := GetOrmer()
_, err := o.Update(&repo)
return err
}
// IncreasePullCount ...
func IncreasePullCount(name string) (err error) {
o := GetOrmer()
num, err := o.QueryTable("repository").Filter("name", name).Update(
orm.Params{
"pull_count": orm.ColValue(orm.ColAdd, 1),
})
if num == 0 {
err = fmt.Errorf("Failed to increase repository pull count with name: %s %s", name, err.Error())
}
return err
}
//RepositoryExists returns whether the repository exists according to its name.
func RepositoryExists(name string) bool {
o := GetOrmer()
return o.QueryTable("repository").Filter("name", name).Exist()
}
// GetRepositoryByProjectName ...
func GetRepositoryByProjectName(name string) ([]*models.RepoRecord, error) {
sql := `select * from repository
where project_id = (
select project_id from project
where name = ?
)`
repos := []*models.RepoRecord{}
_, err := GetOrmer().Raw(sql, name).QueryRows(&repos)
return repos, err
}
//GetTopRepos returns the most popular repositories
func GetTopRepos(count int) ([]models.TopRepo, error) {
topRepos := []models.TopRepo{}
repositories := []*models.RepoRecord{}
if _, err := GetOrmer().QueryTable(&models.RepoRecord{}).
OrderBy("-PullCount", "Name").Limit(count).All(&repositories); err != nil {
return topRepos, err
}
for _, repository := range repositories {
topRepos = append(topRepos, models.TopRepo{
RepoName: repository.Name,
AccessCount: repository.PullCount,
})
}
return topRepos, nil
}
// GetTotalOfRepositories ...
func GetTotalOfRepositories(name string) (int64, error) {
qs := GetOrmer().QueryTable(&models.RepoRecord{})
if len(name) != 0 {
qs = qs.Filter("Name__contains", name)
}
return qs.Count()
}
// GetTotalOfPublicRepositories ...
func GetTotalOfPublicRepositories(name string) (int64, error) {
params := []interface{}{}
sql := `select count(*) from repository r
join project p
on r.project_id = p.project_id and p.public = 1 `
if len(name) != 0 {
sql += ` where r.name like ?`
params = append(params, "%"+name+"%")
}
var total int64
err := GetOrmer().Raw(sql, params).QueryRow(&total)
return total, err
}
// GetTotalOfUserRelevantRepositories ...
func GetTotalOfUserRelevantRepositories(userID int, name string) (int64, error) {
params := []interface{}{}
sql := `select count(*)
from repository r
join (
select p.project_id, p.public
from project p
join project_member pm
on p.project_id = pm.project_id
where pm.user_id = ?
) as pp
on r.project_id = pp.project_id `
params = append(params, userID)
if len(name) != 0 {
sql += ` where r.name like ?`
params = append(params, "%"+name+"%")
}
var total int64
err := GetOrmer().Raw(sql, params).QueryRow(&total)
return total, err
}

169
dao/repository_test.go Normal file
View File

@ -0,0 +1,169 @@
/*
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 dao
import (
"testing"
"github.com/vmware/harbor/models"
)
var (
project = "library"
name = "library/repository-test"
repository = &models.RepoRecord{
Name: name,
OwnerName: "admin",
ProjectName: project,
}
)
func TestGetRepositoryByProjectName(t *testing.T) {
if err := addRepository(repository); err != nil {
t.Fatalf("failed to add repository %s: %v", name, err)
}
defer func() {
if err := deleteRepository(name); err != nil {
t.Fatalf("failed to delete repository %s: %v", name, err)
}
}()
repositories, err := GetRepositoryByProjectName(project)
if err != nil {
t.Fatalf("failed to get repositories of project %s: %v",
project, err)
}
if len(repositories) == 0 {
t.Fatal("unexpected length of repositories: 0, at least 1")
}
exist := false
for _, repo := range repositories {
if repo.Name == name {
exist = true
break
}
}
if !exist {
t.Errorf("there is no repository whose name is %s", name)
}
}
func TestGetTotalOfRepositories(t *testing.T) {
total, err := GetTotalOfRepositories("")
if err != nil {
t.Fatalf("failed to get total of repositoreis: %v", err)
}
if err := addRepository(repository); err != nil {
t.Fatalf("failed to add repository %s: %v", name, err)
}
defer func() {
if err := deleteRepository(name); err != nil {
t.Fatalf("failed to delete repository %s: %v", name, err)
}
}()
n, err := GetTotalOfRepositories("")
if err != nil {
t.Fatalf("failed to get total of repositoreis: %v", err)
}
if n != total+1 {
t.Errorf("unexpected total: %d != %d", n, total+1)
}
}
func TestGetTotalOfPublicRepositories(t *testing.T) {
total, err := GetTotalOfPublicRepositories("")
if err != nil {
t.Fatalf("failed to get total of public repositoreis: %v", err)
}
if err := addRepository(repository); err != nil {
t.Fatalf("failed to add repository %s: %v", name, err)
}
defer func() {
if err := deleteRepository(name); err != nil {
t.Fatalf("failed to delete repository %s: %v", name, err)
}
}()
n, err := GetTotalOfPublicRepositories("")
if err != nil {
t.Fatalf("failed to get total of public repositoreis: %v", err)
}
if n != total+1 {
t.Errorf("unexpected total: %d != %d", n, total+1)
}
}
func TestGetTotalOfUserRelevantRepositories(t *testing.T) {
total, err := GetTotalOfUserRelevantRepositories(1, "")
if err != nil {
t.Fatalf("failed to get total of repositoreis for user %d: %v", 1, err)
}
if err := addRepository(repository); err != nil {
t.Fatalf("failed to add repository %s: %v", name, err)
}
defer func() {
if err := deleteRepository(name); err != nil {
t.Fatalf("failed to delete repository %s: %v", name, err)
}
}()
users, err := GetUserByProject(1, models.User{})
if err != nil {
t.Fatalf("failed to list members of project %d: %v", 1, err)
}
exist := false
for _, user := range users {
if user.UserID == 1 {
exist = true
break
}
}
if !exist {
if err = AddProjectMember(1, 1, models.DEVELOPER); err != nil {
t.Fatalf("failed to add user %d to be member of project %d: %v", 1, 1, err)
}
defer func() {
if err = DeleteProjectMember(1, 1); err != nil {
t.Fatalf("failed to delete user %d from member of project %d: %v", 1, 1, err)
}
}()
}
n, err := GetTotalOfUserRelevantRepositories(1, "")
if err != nil {
t.Fatalf("failed to get total of public repositoreis for user %d: %v", 1, err)
}
if n != total+1 {
t.Errorf("unexpected total: %d != %d", n, total+1)
}
}
func addRepository(repository *models.RepoRecord) error {
return AddRepository(*repository)
}
func deleteRepository(name string) error {
return DeleteRepository(name)
}

View File

@ -111,7 +111,7 @@ func ListUsers(query models.User) ([]models.User, error) {
// ToggleUserAdminRole gives a user admin role. // ToggleUserAdminRole gives a user admin role.
func ToggleUserAdminRole(userID, hasAdmin int) error { func ToggleUserAdminRole(userID, hasAdmin int) error {
o := GetOrmer() o := GetOrmer()
queryParams := make([]interface{}, 1) queryParams := make([]interface{}, 1)
sql := `update user set sysadmin_flag = ? where user_id = ?` sql := `update user set sysadmin_flag = ? where user_id = ?`
queryParams = append(queryParams, hasAdmin) queryParams = append(queryParams, hasAdmin)
queryParams = append(queryParams, userID) queryParams = append(queryParams, userID)
@ -185,37 +185,24 @@ func UpdateUserResetUUID(u models.User) error {
func CheckUserPassword(query models.User) (*models.User, error) { func CheckUserPassword(query models.User) (*models.User, error) {
currentUser, err := GetUser(query) currentUser, err := GetUser(query)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if currentUser == nil { if currentUser == nil {
return nil, nil return nil, nil
} }
sql := `select user_id, username, salt from user where deleted = 0` sql := `select user_id, username, salt from user where deleted = 0 and username = ? and password = ?`
queryParam := make([]interface{}, 1) queryParam := make([]interface{}, 1)
queryParam = append(queryParam, currentUser.Username)
if query.UserID != 0 { queryParam = append(queryParam, utils.Encrypt(query.Password, currentUser.Salt))
sql += ` and password = ? and user_id = ?`
queryParam = append(queryParam, utils.Encrypt(query.Password, currentUser.Salt))
queryParam = append(queryParam, query.UserID)
} else {
sql += ` and username = ? and password = ?`
queryParam = append(queryParam, currentUser.Username)
queryParam = append(queryParam, utils.Encrypt(query.Password, currentUser.Salt))
}
o := GetOrmer() o := GetOrmer()
var user []models.User var user []models.User
n, err := o.Raw(sql, queryParam).QueryRows(&user) n, err := o.Raw(sql, queryParam).QueryRows(&user)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if n == 0 { if n == 0 {
log.Warning("User principal does not match password. Current:", currentUser) log.Warning("User principal does not match password. Current:", currentUser)
return nil, nil return nil, nil
@ -227,7 +214,10 @@ func CheckUserPassword(query models.User) (*models.User, error) {
// DeleteUser ... // DeleteUser ...
func DeleteUser(userID int) error { func DeleteUser(userID int) error {
o := GetOrmer() o := GetOrmer()
_, err := o.Raw(`update user set deleted = 1 where user_id = ?`, userID).Exec() _, err := o.Raw(`update user
set deleted = 1, username = concat(username, "#", user_id),
email = concat(email, "#", user_id)
where user_id = ?`, userID).Exec()
return err return err
} }

69
dao/user_test.go Normal file
View File

@ -0,0 +1,69 @@
/*
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 dao
import (
"fmt"
"testing"
"github.com/vmware/harbor/models"
)
func TestDeleteUser(t *testing.T) {
username := "user_for_test"
email := "user_for_test@vmware.com"
password := "P@ssword"
realname := "user_for_test"
u := models.User{
Username: username,
Email: email,
Password: password,
Realname: realname,
}
id, err := Register(u)
if err != nil {
t.Fatalf("failed to register user: %v", err)
}
err = DeleteUser(int(id))
if err != nil {
t.Fatalf("Error occurred in DeleteUser: %v", err)
}
user := &models.User{}
sql := "select * from user where user_id = ?"
if err = GetOrmer().Raw(sql, id).
QueryRow(user); err != nil {
t.Fatalf("failed to query user: %v", err)
}
if user.Deleted != 1 {
t.Error("user is not deleted")
}
expected := fmt.Sprintf("%s#%d", u.Username, id)
if user.Username != expected {
t.Errorf("unexpected username: %s != %s", user.Username,
expected)
}
expected = fmt.Sprintf("%s#%d", u.Email, id)
if user.Email != expected {
t.Errorf("unexpected email: %s != %s", user.Email,
expected)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -1,36 +1,42 @@
# Installation and Configuration Guide # Installation and Configuration Guide
Harbor can be installed in one of two ways: Harbor can be installed by one of two installers:
1. From source code - This goes through a full build process, _and requires an Internet connection_. - **Online installer:** The installer downloads Harbor's images from Docker hub. For this reason, the installer is very small in size.
2. Pre-built installation package - This can save time (no building necessary!) as well as allows for installation on a host that is _not_ connected to the Internet.
This guide describes both of these approaches. - **Offline installer:** Use this installer when the host does not have Internet connection. The installer contains pre-built images so its size is larger.
Both installers can be downloaded from the [release page](https://github.com/vmware/harbor/releases). The installation process of both installers are the same, this guide describes the steps to install and confiugure Harbor.
In addition, the deployment instructions on Kubernetes has been created by the community. Refer to [Deploy Harbor on Kubernetes](kubernetes_deployment.md) for details. In addition, the deployment instructions on Kubernetes has been created by the community. Refer to [Deploy Harbor on Kubernetes](kubernetes_deployment.md) for details.
## Prerequisites for the target host ## Prerequisites for the target host
Harbor is deployed as several Docker containers, and, therefore, can be deployed on any Linux distribution that supports Docker. Harbor is deployed as several Docker containers, and, therefore, can be deployed on any Linux distribution that supports Docker. The target host requires Python, Docker, and Docker Compose to be installed.
The target host requires Python, Docker, and Docker Compose to be installed.
* Python should be version 2.7 or higher. Note that you may have to install Python on Linux distributions (Gentoo, Arch) that do not come with a Python interpreter installed by default * Python should be version 2.7 or higher. Note that you may have to install Python on Linux distributions (Gentoo, Arch) that do not come with a Python interpreter installed by default
* Docker engine should be version 1.10 or higher. For installation instructions, please refer to: https://docs.docker.com/engine/installation/ * Docker engine should be version 1.10 or higher. For installation instructions, please refer to: https://docs.docker.com/engine/installation/
* Docker Compose needs to be version 1.6.0 or higher. For installation instructions, please refer to: https://docs.docker.com/compose/install/ * Docker Compose needs to be version 1.6.0 or higher. For installation instructions, please refer to: https://docs.docker.com/compose/install/
## Installation from source code ## Installation Steps
_Note: To install from source, the target host must be connected to the Internet!_ The installation steps boil down to the following
The steps boil down to the following
1. Get the source code 1. Download the installer;
2. Configure **harbor.cfg** 2. Configure **harbor.cfg**;
3. **prepare** the configuration files 3. Run **install.sh** to install and start Harbor;
4. Start Harbor with Docker Compose
#### Getting the source code:
```sh #### Downloading the installer:
$ git clone https://github.com/vmware/harbor
The binary of the installer can be downloaded from the [release](https://github.com/vmware/harbor/releases) page. Choose either online or offline installer. Use *tar* command to extract the package.
Online installer:
``` ```
$ tar xvf harbor-online-installer-<version>.tgz
```
Offline installer:
```
$ tar xvf harbor-offline-installer-<version>.tgz
```
#### Configuring Harbor #### Configuring Harbor
Configuration parameters are located in the file **harbor.cfg**. Configuration parameters are located in the file **harbor.cfg**.
The parameters are described below - note that at the very least, you will need to change the **hostname** attribute. The parameters are described below - note that at the very least, you will need to change the **hostname** attribute.
@ -45,22 +51,32 @@ The parameters are described below - note that at the very least, you will need
* email_from = admin <sample_admin@mydomain.com> * email_from = admin <sample_admin@mydomain.com>
* email_ssl = false * email_ssl = false
* **harbor_admin_password**: The adminstrator's password. _Note that the default username/password are **admin/Harbor12345** ._ * **harbor_admin_password**: The adminstrator's initial password. This password only takes effect for the first time Harbor launches. After that, this setting is ignored and the adminstrator's password should be set in the UI. _Note that the default username/password are **admin/Harbor12345** ._
* **auth_mode**: The type of authentication that is used. By default it is **db_auth**, i.e. the credentials are stored in a database. For LDAP authentication, set this to **ldap_auth**. * **auth_mode**: The type of authentication that is used. By default it is **db_auth**, i.e. the credentials are stored in a database. For LDAP authentication, set this to **ldap_auth**.
* **ldap_url**: The LDAP endpoint URL (e.g. `ldaps://ldap.mydomain.com`). _Only used when **auth_mode** is set to *ldap_auth* ._ * **ldap_url**: The LDAP endpoint URL (e.g. `ldaps://ldap.mydomain.com`). _Only used when **auth_mode** is set to *ldap_auth* ._
* **ldap_basedn**: The basedn template for verifying the user's credential against an LDAP (e.g. `uid=%s,ou=people,dc=mydomain,dc=com` ) or an AD (e.g. `CN=%s,OU=Dept1,DC=mydomain,DC=com`) server. _Only used when **auth_mode** is set to *ldap_auth* ._ * **ldap_searchdn**: The DN of a user who has the permission to search an LDAP/AD server (e.g. `uid=admin,ou=people,dc=mydomain,dc=com`).
* **ldap_search_pwd**: The password of the user specified by *ldap_searchdn*.
* **ldap_basedn**: The base DN to look up a user, e.g. `ou=people,dc=mydomain,dc=com`. _Only used when **auth_mode** is set to *ldap_auth* ._
* **ldap_filter**:The search filter for looking up a user, e.g. `(objectClass=person)`.
* **ldap_uid**: The attribute used to match a user during a ldap search, it could be uid, cn, email or other attributes.
* **ldap_scope**: The scope to search for a user, 1-LDAP_SCOPE_BASE, 2-LDAP_SCOPE_ONELEVEL, 3-LDAP_SCOPE_SUBTREE. Default is 3.
* **db_password**: The root password for the mySQL database used for **db_auth**. _Change this password for any production use!_ * **db_password**: The root password for the mySQL database used for **db_auth**. _Change this password for any production use!_
* **self_registration**: (**on** or **off**. Default is **on**) Enable / Disable the ability for a user to register themselves. When disabled, new users can only be created by the Admin user, only an admin user can create new users in Harbor. _NOTE: When **auth_mode** is set to **ldap_auth**, self-registration feature is **always** disabled, and this flag is ignored._ * **self_registration**: (**on** or **off**. Default is **on**) Enable / Disable the ability for a user to register themselves. When disabled, new users can only be created by the Admin user, only an admin user can create new users in Harbor. _NOTE: When **auth_mode** is set to **ldap_auth**, self-registration feature is **always** disabled, and this flag is ignored._
* **use_compressed_js**: (**on** or **off**. Default is **on**) For production use, turn this flag to **on**. In development mode, set it to **off** so that js files can be modified separately. * **use_compressed_js**: (**on** or **off**. Default is **on**) For production use, turn this flag to **on**. In development mode, set it to **off** so that js files can be modified separately.
* **max_job_workers**: (default value is **3**) The maximum number of replication workers in job service. For each image replication job, a worker synchronizes all tags of a repository to the remote destination. Increasing this number allows more concurrent replication jobs in the system. However, since each worker consumes a certain amount of network/CPU/IO resources, please carefully pick the value of this attribute based on the hardware resource of the host. * **max_job_workers**: (default value is **3**) The maximum number of replication workers in job service. For each image replication job, a worker synchronizes all tags of a repository to the remote destination. Increasing this number allows more concurrent replication jobs in the system. However, since each worker consumes a certain amount of network/CPU/IO resources, please carefully pick the value of this attribute based on the hardware resource of the host.
* **verify_remote_cert**: (**on** or **off**. Default is **on**) This flag determines whether or not to verify SSL/TLS certificate when Harbor communicates with a remote registry instance. Setting this attribute to **off** will bypass the SSL/TLS verification, which is often used when the remote instance has a self-signed or untrusted certificate. * **secret_key**: The key to encrypt or decrypt the password of a remote registry in a replication policy, its length has to be 16 characters. Change this key before any production use. *NOTE: After changing this key, previously encrypted password of a policy can not be decrypted.*
* **customize_crt**: (**on** or **off**. Default is **on**) When this attribute is **on**, the prepare script creates private key and root certificate for the generation/verification of the regitry's token. The following attributes:**crt_country**, **crt_state**, **crt_location**, **crt_organization**, **crt_organizationalunit**, **crt_commonname**, **crt_email** are used as parameters for generating the keys. Set this attribute to **off** when the key and root certificate are supplied by external sources. Refer to [Customize Key and Certificate of Harbor Token Service](customize_token_service.md) for more info.
* **token_expiration**: The expiration time (in minute) of a token created by token service, default is 30 minutes.
* **verify_remote_cert**: (**on** or **off**. Default is **on**) This flag determines whether or not to verify SSL/TLS certificate when Harbor communicates with a remote registry instance. Setting this attribute to **off** bypasses the SSL/TLS verification, which is often used when the remote instance has a self-signed or untrusted certificate.
* **customize_crt**: (**on** or **off**. Default is **on**) When this attribute is **on**, the prepare script creates private key and root certificate for the generation/verification of the regitry's token.
* The following attributes:**crt_country**, **crt_state**, **crt_location**, **crt_organization**, **crt_organizationalunit**, **crt_commonname**, **crt_email** are used as parameters for generating the keys. Set this attribute to **off** when the key and root certificate are supplied by external sources. Refer to [Customize Key and Certificate of Harbor Token Service](customize_token_service.md) for more info.
#### Configuring storage backend (optional) #### Configuring storage backend (optional)
By default, Harbor stores images on your local filesystem. In a production environment, you may consider By default, Harbor stores images on your local filesystem. In a production environment, you may consider
using other storage backend instead of the local filesystem, like S3, Openstack Swift, Ceph, etc. using other storage backend instead of the local filesystem, like S3, Openstack Swift, Ceph, etc.
What you need to update is the section of `storage` in the file `Deploy/templates/registry/config.yml`. What you need to update is the section of `storage` in the file `templates/registry/config.yml`.
For example, if you use Openstack Swift as your storage backend, the section may look like this: For example, if you use Openstack Swift as your storage backend, the section may look like this:
``` ```
@ -68,7 +84,7 @@ storage:
swift: swift:
username: admin username: admin
password: ADMIN_PASS password: ADMIN_PASS
authurl: http://keystone_addr:35357/v3 authurl: http://keystone_addr:35357/v3/auth
tenant: admin tenant: admin
domain: default domain: default
region: regionOne region: regionOne
@ -78,35 +94,21 @@ storage:
_NOTE: For detailed information on storage backend of a registry, refer to [Registry Configuration Reference](https://docs.docker.com/registry/configuration/) ._ _NOTE: For detailed information on storage backend of a registry, refer to [Registry Configuration Reference](https://docs.docker.com/registry/configuration/) ._
#### Building and starting Harbor #### Installing and starting Harbor
Once **harbord.cfg** and storage backend (optional) are configured, build and start Harbor as follows. Note that the docker-compose process can take a while. Once **harbord.cfg** and storage backend (optional) are configured, install and start Harbor using the ```install.sh script```. Note that it may take some time for the online installer to download Harbor images from Docker hub.
```sh ```sh
$ cd Deploy $ sudo ./install.sh
$ ./prepare
Generated configuration file: ./config/ui/env
Generated configuration file: ./config/ui/app.conf
Generated configuration file: ./config/registry/config.yml
Generated configuration file: ./config/db/env
Generated configuration file: ./config/jobservice/env
Clearing the configuration file: ./config/ui/private_key.pem
Clearing the configuration file: ./config/registry/root.crt
Generated configuration file: ./config/ui/private_key.pem
Generated configuration file: ./config/registry/root.crt
The configuration files are ready, please use docker-compose to start the service.
$ sudo docker-compose up -d
``` ```
_If everything worked properly, you should be able to open a browser to visit the admin portal at http://reg.yourdomain.com . Note that the default administrator username/password are admin/Harbor12345 ._ If everything worked properly, you should be able to open a browser to visit the admin portal at **http://reg.yourdomain.com** (change *reg.yourdomain.com* to the hostname configured in your harbor.cfg). Note that the default administrator username/password are admin/Harbor12345 .
Log in to the admin portal and create a new project, e.g. `myproject`. You can then use docker commands to login and push images (By default, the registry server listens on port 80): Log in to the admin portal and create a new project, e.g. `myproject`. You can then use docker commands to login and push images (By default, the registry server listens on port 80):
```sh ```sh
$ docker login reg.yourdomain.com $ docker login reg.yourdomain.com
$ docker push reg.yourdomain.com/myproject/myrepo $ docker push reg.yourdomain.com/myproject/myrepo:mytag
``` ```
**NOTE:** The default installation of Harbor uses _HTTP_ - as such, you will need to add the option `--insecure-registry` to your client's Docker daemon and restart the Docker service. **IMPORTANT:** The default installation of Harbor uses _HTTP_ - as such, you will need to add the option `--insecure-registry` to your client's Docker daemon and restart the Docker service.
For information on how to use Harbor, please refer to [User Guide of Harbor](user_guide.md) . For information on how to use Harbor, please refer to [User Guide of Harbor](user_guide.md) .
@ -114,125 +116,10 @@ For information on how to use Harbor, please refer to [User Guide of Harbor](use
Harbor does not ship with any certificates, and, by default, uses HTTP to serve requests. While this makes it relatively simple to set up and run - especially for a development or testing environment - it is **not** recommended for a production environment. To enable HTTPS, please refer to [Configuring Harbor with HTTPS Access](configure_https.md). Harbor does not ship with any certificates, and, by default, uses HTTP to serve requests. While this makes it relatively simple to set up and run - especially for a development or testing environment - it is **not** recommended for a production environment. To enable HTTPS, please refer to [Configuring Harbor with HTTPS Access](configure_https.md).
## Installation from a pre-built package
Pre-built installation packages of each release are available at [release page](https://github.com/vmware/harbor/releases).
Download the package file **harbor-&lt;version&gt;.tgz** , and then extract the files.
```
$ tar -xzvf harbor-0.3.0.tgz
$ cd harbor
```
Next, configure Harbor as described earlier in [Configuring Harbor](#configuring-harbor).
Finally, run the **prepare** script to generate config files, and use docker compose to build and start Harbor.
```
$ ./prepare
Generated configuration file: ./config/ui/env
Generated configuration file: ./config/ui/app.conf
Generated configuration file: ./config/registry/config.yml
Generated configuration file: ./config/db/env
Generated configuration file: ./config/jobservice/env
Clearing the configuration file: ./config/ui/private_key.pem
Clearing the configuration file: ./config/registry/root.crt
Generated configuration file: ./config/ui/private_key.pem
Generated configuration file: ./config/registry/root.crt
The configuration files are ready, please use docker-compose to start the service.
$ sudo docker-compose up -d
......
```
### Deploying Harbor on a host which does not have Internet access
*docker-compose up* pulls the base images from Docker Hub and builds new images for the containers, which, necessarily, requires Internet access. To deploy Harbor on a host that is not connected to the Internet:
1. Prepare Harbor on a machine that has access to the Internet.
2. Export the images as tgz files
3. Transfer them to the target host.
4. Load the tgz file into Docker's local image repo on the host.
These steps are detailed below:
#### Building and saving images for offline installation
On a machine that is connected to the Internet,
1. Extract the files from the pre-built installation package.
2. Then, run `docker-compose build` to build the images.
3. Use the script `save_image.sh` to export these images as tar files. Note that the tar files will be stored in the `images/` directory.
4. Package everything in the directory `harbor/` into a tgz file
5. Transfer this tgz file to the target machine.
The commands, in detail, are as follows:
```
$ cd harbor
$ sudo docker-compose build
......
$ sudo ./save_image.sh
saving the image of harbor_ui
finished saving the image of harbor_ui
saving the image of harbor_log
finished saving the image of harbor_log
saving the image of harbor_mysql
finished saving the image of harbor_mysql
saving the image of nginx
finished saving the image of nginx
saving the image of registry
finished saving the image of registry
saving the image of harbor_jobservice
finished saving the image of harbor_jobservice
$ cd ../
$ tar -cvzf harbor_offline-0.3.0.tgz harbor
```
The file `harbor_offline-0.3.0.tgz` contains the images and other files required to start Harbor. You can use tools such as `rsync` or `scp` to transfer this file to the target host.
On the target host, execute the following commands to start Harbor. _Note that before running the **prepare** script, you **must** update **harbor.cfg** to reflect the right configuration of the target machine!_ (Refer to Section [Configuring Harbor](#configuring-harbor)).
```
$ tar -xzvf harbor_offline-0.3.0.tgz
$ cd harbor
# load images save by excute ./save_image.sh
$ ./load_image.sh
loading the image of harbor_ui
finish loaded the image of harbor_ui
loading the image of harbor_mysql
finished loading the image of harbor_mysql
loading the image of nginx
finished loading the image of nginx
loading the image of registry
finished loading the image of registry
loading the image of harbor_jobservice
finished loading the image of harbor_jobservice
# Make update to the parameters in ./harbor.cfg
$ ./prepare
Generated configuration file: ./config/ui/env
Generated configuration file: ./config/ui/app.conf
Generated configuration file: ./config/registry/config.yml
Generated configuration file: ./config/db/env
The configuration files are ready, please use docker-compose to start the service.
# Build the images and then start the services
$ sudo docker-compose up -d
```
### Managing Harbor's lifecycle ### Managing Harbor's lifecycle
You can use docker-compose to manage the lifecycle of the containers. A few useful commands are listed below: You can use docker-compose to manage the lifecycle of Harbor. Some useful commands are listed as follows (must run in the same directory as *docker-compose.yml*).
*Build and start Harbor:* Stop Harbor:
```
$ sudo docker-compose up -d
Creating harbor_log_1
Creating harbor_mysql_1
Creating harbor_registry_1
Creating harbor_ui_1
Creating harbor_proxy_1
Creating harbor_jobservice_1
```
*Stop Harbor:*
``` ```
$ sudo docker-compose stop $ sudo docker-compose stop
Stopping harbor_proxy_1 ... done Stopping harbor_proxy_1 ... done
@ -242,7 +129,7 @@ Stopping harbor_mysql_1 ... done
Stopping harbor_log_1 ... done Stopping harbor_log_1 ... done
Stopping harbor_jobservice_1 ... done Stopping harbor_jobservice_1 ... done
``` ```
*Restart Harbor after stopping:* Restart Harbor after stopping:
``` ```
$ sudo docker-compose start $ sudo docker-compose start
Starting harbor_log_1 Starting harbor_log_1
@ -252,7 +139,17 @@ Starting harbor_ui_1
Starting harbor_proxy_1 Starting harbor_proxy_1
Starting harbor_jobservice_1 Starting harbor_jobservice_1
``` ```
*Remove Harbor's containers while keeping the image data and Harbor's database files on the file system:*
To change Harbor's confiugration, first stop existing Harbor instance, update harbor.cfg, and then run install.sh again:
```
$ sudo docker-compose down
$ vim harbor.cfg
$ sudo install.sh
```
Remove Harbor's containers while keeping the image data and Harbor's database files on the file system:
``` ```
$ sudo docker-compose rm $ sudo docker-compose rm
Going to remove harbor_proxy_1, harbor_ui_1, harbor_registry_1, harbor_mysql_1, harbor_log_1, harbor_jobservice_1 Going to remove harbor_proxy_1, harbor_ui_1, harbor_registry_1, harbor_mysql_1, harbor_log_1, harbor_jobservice_1
@ -265,7 +162,7 @@ Removing harbor_log_1 ... done
Removing harbor_jobservice_1 ... done Removing harbor_jobservice_1 ... done
``` ```
*Remove Harbor's database and image data (for a clean re-installation):* Remove Harbor's database and image data (for a clean re-installation):
```sh ```sh
$ rm -r /data/database $ rm -r /data/database
$ rm -r /data/registry $ rm -r /data/registry
@ -274,15 +171,16 @@ $ rm -r /data/registry
Please check the [Docker Compose command-line reference](https://docs.docker.com/compose/reference/) for more on docker-compose. Please check the [Docker Compose command-line reference](https://docs.docker.com/compose/reference/) for more on docker-compose.
### Persistent data and log files ### Persistent data and log files
By default, registry data is persisted in the target host's `/data/` directory. This data remains unchanged even when Harbor's containers are removed and/or recreated. By default, registry data is persisted in the target host's `/data/` directory. This data remains unchanged even when Harbor's containers are removed and/or recreated.
In addition, Harbor uses `rsyslog` to collect the logs of each container. By default, these log files are stored in the directory `/var/log/harbor/` on the target host.
In addition, Harbor uses *rsyslog* to collect the logs of each container. By default, these log files are stored in the directory `/var/log/harbor/` on the target host for troubleshooting.
## Configuring Harbor listening on a customized port ## Configuring Harbor listening on a customized port
By default, Harbor listens on port 80(HTTP) and 443(HTTPS, if configured) for both admin portal and docker commands, you can configure it with a customized one. By default, Harbor listens on port 80(HTTP) and 443(HTTPS, if configured) for both admin portal and docker commands, you can configure it with a customized one.
### For HTTP protocol ### For HTTP protocol
1.Modify Deploy/docker-compose.yml 1.Modify docker-compose.yml
Replace the first "80" to a customized port, e.g. 8888:80. Replace the first "80" to a customized port, e.g. 8888:80.
``` ```
@ -306,7 +204,7 @@ proxy:
tag: "proxy" tag: "proxy"
``` ```
2.Modify Deploy/templates/registry/config.yml 2.Modify templates/registry/config.yml
Add the customized port, e.g. ":8888", after "$ui_url". Add the customized port, e.g. ":8888", after "$ui_url".
``` ```
@ -318,17 +216,14 @@ auth:
service: token-service service: token-service
``` ```
3.Execute Deploy/prepare script and start/restart Harbor. 3.Run install.sh to update and start Harbor.
```sh ```sh
$ cd Deploy $ sudo docker-compose down
$ ./prepare $ sudo install.sh
# If Harbor has already been installed, shutdown it first:
$ docker-compose down
$ docker-compose up -d
``` ```
### For HTTPS protocol ### For HTTPS protocol
1.Enable HTTPS in Harbor by following this [guide](https://github.com/vmware/harbor/blob/master/docs/configure_https.md). 1.Enable HTTPS in Harbor by following this [guide](https://github.com/vmware/harbor/blob/master/docs/configure_https.md).
2.Modify Deploy/docker-compose.yml 2.Modify docker-compose.yml
Replace the first "443" to a customized port, e.g. 4443:443. Replace the first "443" to a customized port, e.g. 4443:443.
``` ```
@ -352,7 +247,7 @@ proxy:
tag: "proxy" tag: "proxy"
``` ```
3.Modify Deploy/templates/registry/config.yml 3.Modify templates/registry/config.yml
Add the customized port, e.g. ":4443", after "$ui_url". Add the customized port, e.g. ":4443", after "$ui_url".
``` ```
@ -364,17 +259,29 @@ auth:
service: token-service service: token-service
``` ```
4.Execute Deploy/prepare script and start/restart Harbor. 4.Run install.sh to update and start Harbor.
```sh ```sh
$ cd Deploy $ sudo docker-compose down
$ ./prepare $ sudo install.sh
# If Harbor has already been installed, shutdown it first:
$ docker-compose down
$ docker-compose up -d
``` ```
## Troubleshooting ## Troubleshooting
1.When setting up Harbor behind an nginx proxy or elastic load balancing, look for the line below, in `Deploy/config/nginx/nginx.conf` and remove it from the sections if the proxy already has similar settings: `location /`, `location /v2/` and `location /service/`. 1. When Harbor does not work properly, run the below commands to find out if all containers of Harbor are in **UP** status:
```
$ sudo docker-compose ps
Name Command State Ports
-----------------------------------------------------------------------------------------------------
harbor_jobservice_1 /harbor/harbor_jobservice Up
harbor_log_1 /bin/sh -c crond && rsyslo ... Up 0.0.0.0:1514->514/tcp
harbor_mysql_1 /entrypoint.sh mysqld Up 3306/tcp
harbor_proxy_1 nginx -g daemon off; Up 0.0.0.0:443->443/tcp, 0.0.0.0:80->80/tcp
harbor_registry_1 /entrypoint.sh serve /etc/ ... Up 5000/tcp
harbor_ui_1 /harbor/harbor_ui Up
```
If a container is not in **UP** state, check the log file of that container in directory ```/var/log/harbor```. For example, if the container ```harbor_ui_1``` is not running, you should look at the log file ```docker_ui.log```.
2.When setting up Harbor behind an nginx proxy or elastic load balancing, look for the line below, in `Deploy/config/nginx/nginx.conf` and remove it from the sections if the proxy already has similar settings: `location /`, `location /v2/` and `location /service/`.
``` ```
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
``` ```

View File

@ -1,6 +1,6 @@
# Harbor upgrade and database migration guide # Harbor upgrade and database migration guide
When upgrading your existing Habor instance to a newer version, you may need to migrate the data in your database. Refer to [change log](changelog.md) to find out whether there is any change in the database. If there is, you should go through the database migration process. Since the migration may alter the database schema, you should **always** back up your data before any migration. When upgrading your existing Habor instance to a newer version, you may need to migrate the data in your database. Refer to [change log](../migration/changelog.md) to find out whether there is any change in the database. If there is, you should go through the database migration process. Since the migration may alter the database schema, you should **always** back up your data before any migration.
*If your install Harbor for the first time, or the database version is the same as that of the lastest version, you do not need any database migration.* *If your install Harbor for the first time, or the database version is the same as that of the lastest version, you do not need any database migration.*

View File

@ -60,6 +60,18 @@ paths:
required: false required: false
type: integer type: integer
format: int32 format: int32
- name: page
in: query
type: integer
format: int32
required: false
description: The page nubmer, default is 1.
- name: page_size
in: query
type: integer
format: int32
required: false
description: The size of per page, default is 10, maximum is 100.
tags: tags:
- Products - Products
responses: responses:
@ -69,6 +81,13 @@ paths:
type: array type: array
items: items:
$ref: '#/definitions/Project' $ref: '#/definitions/Project'
headers:
X-Total-Count:
description: The total count of projects
type: integer
Link:
description: Link refers to the previous page and next page
type: string
401: 401:
description: User need to log in first. description: User need to log in first.
500: 500:
@ -104,7 +123,7 @@ paths:
description: New created project. description: New created project.
required: true required: true
schema: schema:
$ref: '#/definitions/Project' $ref: '#/definitions/ProjectReq'
tags: tags:
- Products - Products
responses: responses:
@ -136,13 +155,37 @@ paths:
200: 200:
description: Return matched project information. description: Return matched project information.
schema: schema:
type: array $ref: '#/definitions/Project'
items:
$ref: '#/definitions/Project'
401: 401:
description: User need to log in first. description: User need to log in first.
500: 500:
description: Internal errors. description: Internal errors.
delete:
summary: Delete project by projectID
description: |
This endpoint is aimed to delete project by project ID.
parameters:
- name: project_id
in: path
description: Project ID of project which will be deleted.
required: true
type: integer
format: int64
tags:
- Products
responses:
200:
description: Project is deleted successfully.
400:
description: Invalid project id.
403:
description: User need to log in first.
404:
description: Project does not exist.
412:
description: Project contains policies, can not be deleted.
500:
description: Internal errors.
/projects/{project_id}/publicity: /projects/{project_id}/publicity:
put: put:
summary: Update properties for a selected project. summary: Update properties for a selected project.
@ -193,6 +236,18 @@ paths:
schema: schema:
$ref: '#/definitions/AccessLogFilter' $ref: '#/definitions/AccessLogFilter'
description: Search results of access logs. description: Search results of access logs.
- name: page
in: query
type: integer
format: int32
required: false
description: The page nubmer, default is 1.
- name: page_size
in: query
type: integer
format: int32
required: false
description: The size of per page, default is 10, maximum is 100.
tags: tags:
- Products - Products
responses: responses:
@ -202,6 +257,13 @@ paths:
type: array type: array
items: items:
$ref: '#/definitions/AccessLog' $ref: '#/definitions/AccessLog'
headers:
X-Total-Count:
description: The total count of access logs
type: integer
Link:
description: Link refers to the previous page and next page
type: string
400: 400:
description: Illegal format of provided ID value. description: Illegal format of provided ID value.
401: 401:
@ -252,7 +314,7 @@ paths:
description: Relevant project ID. description: Relevant project ID.
- name: roles - name: roles
in: body in: body
description: Role members for adding to relevant project. description: Role members for adding to relevant project. Only one role is supported in the role list.
schema: schema:
$ref: '#/definitions/RoleParam' $ref: '#/definitions/RoleParam'
tags: tags:
@ -462,7 +524,7 @@ paths:
description: Only email, realname and comment can be modified. description: Only email, realname and comment can be modified.
required: true required: true
schema: schema:
$ref: '#/definitions/User' $ref: '#/definitions/UserProfile'
tags: tags:
- Products - Products
responses: responses:
@ -549,6 +611,12 @@ paths:
format: int format: int
required: true required: true
description: Registered user ID description: Registered user ID
- name: has_admin_role
in: body
description: Toggle a user to admin or not.
required: true
schema:
$ref: '#/definitions/HasAdminRole'
tags: tags:
- Products - Products
responses: responses:
@ -581,6 +649,18 @@ paths:
type: string type: string
required: false required: false
description: Repo name for filtering results. description: Repo name for filtering results.
- name: page
in: query
type: integer
format: int32
required: false
description: The page nubmer, default is 1.
- name: page_size
in: query
type: integer
format: int32
required: false
description: The size of per page, default is 10, maximum is 100.
tags: tags:
- Products - Products
responses: responses:
@ -590,6 +670,13 @@ paths:
type: array type: array
items: items:
type: string type: string
headers:
X-Total-Count:
description: The total count of repositories
type: integer
Link:
description: Link refers to the previous page and next page
type: string
400: 400:
description: Invalid project ID. description: Invalid project ID.
403: 403:
@ -625,7 +712,7 @@ paths:
404: 404:
description: Repository or tag not found. description: Repository or tag not found.
403: 403:
description: Forbidden. description: Forbidden.
/repositories/tags: /repositories/tags:
get: get:
summary: Get tags of a relevant repository. summary: Get tags of a relevant repository.
@ -660,6 +747,11 @@ paths:
type: string type: string
required: true required: true
description: Tag name description: Tag name
- name: version
in: query
type: string
required: false
description: The version of manifest, valid value are "v1" and "v2", default is "v2"
tags: tags:
- Products - Products
responses: responses:
@ -777,13 +869,32 @@ paths:
type: string type: string
required: false required: false
description: The respond jobs list filter by status. description: The respond jobs list filter by status.
- name: page
in: query
type: integer
format: int32
required: false
description: The page nubmer, default is 1.
- name: page_size
in: query
type: integer
format: int32
required: false
description: The size of per page, default is 10, maximum is 100.
responses: responses:
200: 200:
description: Get the required logs successfully. description: Get the required logs successfully.
schema: schema:
type: array type: array
items: items:
$ref: '#/definitions/JobStatus' $ref: '#/definitions/RepPolicy'
headers:
X-Total-Count:
description: The total count of jobs
type: integer
Link:
description: Link refers to the previous page and next page
type: string
400: 400:
description: Bad request because of invalid parameters. description: Bad request because of invalid parameters.
401: 401:
@ -1106,9 +1217,7 @@ paths:
200: 200:
description: Get replication's target successfully. description: Get replication's target successfully.
schema: schema:
type: array $ref: '#/definitions/RepTarget'
items:
$ref: '#/definitions/RepTarget'
401: 401:
description: User need to log in first. description: User need to log in first.
404: 404:
@ -1197,18 +1306,28 @@ definitions:
SearchRepository: SearchRepository:
type: object type: object
properties: 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: project_id:
type: integer type: integer
description: The ID of the project that the repository belongs to description: The ID of the project that the repository belongs to
project_name:
type: string
description: The name of the project that the repository belongs to
project_public: project_public:
type: integer type: integer
description: The flag to indicate the publicity of the project that the repository belongs to (1 is public, 0 is not) description: The flag to indicate the publicity of the project that the repository belongs to (1 is public, 0 is not)
repository_name:
type: string
description: The name of the repository
ProjectReq:
type: object
properties:
project_name:
type: string
description: The name of the project.
public:
type: integer
format: int
description: The public status of the project.
Project: Project:
type: object type: object
properties: properties:
@ -1220,7 +1339,7 @@ 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:
@ -1233,18 +1352,14 @@ definitions:
type: integer type: integer
format: int32 format: int32
description: A deletion mark of the project (1 means it's deleted, 0 is not) 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: owner_name:
type: string type: string
description: The owner name of the project. description: The owner name of the project.
public: public:
type: boolean type: integer
format: boolean format: int
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 whether the project's publicity is updatable (for UI) description: Correspond to the UI about whether the project's publicity is updatable (for UI)
current_user_role_id: current_user_role_id:
@ -1256,50 +1371,48 @@ definitions:
Repository: Repository:
type: object type: object
properties: properties:
id: manifest:
type: object
description: The detail of manifest.
config:
type: string type: string
description: Repository ID description: The config of the repository.
parent:
type: string
description: Parent of the image.
created:
type: string
description: Repository create time.
duration_days:
type: string
description: Duration days of the image.
author:
type: string
description: Author of the image.
architecture:
type: string
description: Architecture of the image.
docker_version:
type: string
description: Docker version of the image.
os:
type: string
description: OS of the image.
User: User:
type: object type: object
properties: properties:
user_id: user_id:
type: integer type: integer
format: int32 format: int
description: The ID of the user. description: The ID of the user.
username: username:
type: string type: string
email: email:
type: string type: string
password: password:
type: string type: string
realname: realname:
type: string type: string
comment: comment:
type: string type: string
deleted: deleted:
type: integer type: integer
format: int32 format: int32
role_name:
type: string
role_id:
type: integer
format: int
has_admin_role:
type: integer
format: int
reset_uuid:
type: string
Salt:
type: string
creation_time:
type: string
update_time:
type: string
Password: Password:
type: object type: object
properties: properties:
@ -1308,7 +1421,7 @@ definitions:
description: The user's existing password. description: The user's existing password.
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.
AccessLogFilter: AccessLogFilter:
type: object type: object
properties: properties:
@ -1357,6 +1470,8 @@ definitions:
role_name: role_name:
type: string type: string
description: Name the the role. description: Name the the role.
role_mask:
type: string
RoleParam: RoleParam:
type: object type: object
properties: properties:
@ -1375,7 +1490,7 @@ definitions:
repo_name: repo_name:
type: string type: string
description: The name of the repo description: The name of the repo
access_count: count:
type: integer type: integer
format: int format: int
description: The access count of the repo description: The access count of the repo
@ -1461,9 +1576,6 @@ definitions:
type: integer type: integer
format: int64 format: int64
description: The target ID. description: The target ID.
target_name:
type: string
description: The target name.
name: name:
type: string type: string
description: The policy name. description: The policy name.
@ -1489,6 +1601,8 @@ definitions:
error_job_count: error_job_count:
format: int format: int
description: The error job count number for the policy. description: The error job count number for the policy.
deleted:
type: integer
RepPolicyPost: RepPolicyPost:
type: object type: object
properties: properties:
@ -1574,3 +1688,21 @@ definitions:
password: password:
type: string type: string
description: The target server password. description: The target server password.
HasAdminRole:
type: object
properties:
has_admin_role:
type: integer
description: 1-has admin, 0-not.
UserProfile:
type: object
properties:
email:
type: string
description: The new email.
realname:
type: string
description: The new realname.
comment:
type: string
description: The new comment.

View File

@ -31,6 +31,7 @@ var localUIURL string
var localRegURL string var localRegURL string
var logDir string var logDir string
var uiSecret string var uiSecret string
var secretKey string
var verifyRemoteCert string var verifyRemoteCert string
func init() { func init() {
@ -86,6 +87,11 @@ func init() {
beego.LoadAppConfig("ini", configPath) beego.LoadAppConfig("ini", configPath)
} }
secretKey = os.Getenv("SECRET_KEY")
if len(secretKey) != 16 {
panic("The length of secretkey has to be 16 characters!")
}
log.Debugf("config: maxJobWorkers: %d", maxJobWorkers) log.Debugf("config: maxJobWorkers: %d", maxJobWorkers)
log.Debugf("config: localUIURL: %s", localUIURL) log.Debugf("config: localUIURL: %s", localUIURL)
log.Debugf("config: localRegURL: %s", localRegURL) log.Debugf("config: localRegURL: %s", localRegURL)
@ -119,6 +125,11 @@ func UISecret() string {
return uiSecret return uiSecret
} }
// SecretKey will return the secret key for encryption/decryption password in target.
func SecretKey() string {
return secretKey
}
// VerifyRemoteCert return the flag to tell jobservice whether or not verify the cert of remote registry // VerifyRemoteCert return the flag to tell jobservice whether or not verify the cert of remote registry
func VerifyRemoteCert() bool { func VerifyRemoteCert() bool {
return verifyRemoteCert != "off" return verifyRemoteCert != "off"

View File

@ -0,0 +1,9 @@
package config
import (
"testing"
)
func TestMain(t *testing.T) {
}

9
job/job_test.go Normal file
View File

@ -0,0 +1,9 @@
package job
import (
"testing"
)
func TestMain(t *testing.T) {
}

View File

@ -0,0 +1,9 @@
package replication
import (
"testing"
)
func TestMain(t *testing.T) {
}

View File

@ -192,7 +192,7 @@ func (c *Checker) enter() (string, error) {
return "", err return "", err
} }
err = c.createProject(project.Public == 1) err = c.createProject(project.Public)
if err == nil { if err == nil {
c.logger.Infof("project %s is created on %s with user %s", c.project, c.dstURL, c.dstUsr) c.logger.Infof("project %s is created on %s with user %s", c.project, c.dstURL, c.dstUsr)
return StatePullManifest, nil return StatePullManifest, nil
@ -211,13 +211,13 @@ func (c *Checker) enter() (string, error) {
return "", err return "", err
} }
func (c *Checker) createProject(isPublic bool) error { func (c *Checker) createProject(public int) error {
project := struct { project := struct {
ProjectName string `json:"project_name"` ProjectName string `json:"project_name"`
Public bool `json:"public"` Public int `json:"public"`
}{ }{
ProjectName: c.project, ProjectName: c.project,
Public: isPublic, Public: public,
} }
data, err := json.Marshal(project) data, err := json.Marshal(project)

View File

@ -231,7 +231,7 @@ func (sm *SM) Reset(jid int64) error {
pwd := target.Password pwd := target.Password
if len(pwd) != 0 { if len(pwd) != 0 {
pwd, err = uti.ReversibleDecrypt(pwd) pwd, err = uti.ReversibleDecrypt(pwd, config.SecretKey())
if err != nil { if err != nil {
return fmt.Errorf("failed to decrypt password: %v", err) return fmt.Errorf("failed to decrypt password: %v", err)
} }

9
job/utils/utils_test.go Normal file
View File

@ -0,0 +1,9 @@
package utils
import (
"testing"
)
func TestMain(t *testing.T) {
}

View File

@ -0,0 +1,9 @@
package main
import (
"testing"
)
func TestMain(t *testing.T) {
}

View File

@ -17,7 +17,7 @@ Changelog for harbor database schema
- delete data `AMDRWS` from table `role` - delete data `AMDRWS` from table `role`
- delete data `A` from table `access` - delete data `A` from table `access`
## 0.2.0 ## 0.3.0
- create table `replication_policy` - create table `replication_policy`
- create table `replication_target` - create table `replication_target`
@ -25,3 +25,15 @@ Changelog for harbor database schema
- add column `repo_tag` to table `access_log` - add column `repo_tag` to table `access_log`
- alter column `repo_name` on table `access_log` - alter column `repo_name` on table `access_log`
- alter column `email` on table `user` - alter column `email` on table `user`
## 0.4.0
- add index `pid_optime (project_id, op_time)` on table `access_log`
- add index `poid_uptime (policy_id, update_time)` on table `replication_job`
- add column `deleted` to table `replication_policy`
- alter column `username` on table `user`: varchar(15)->varchar(32)
- alter column `password` on table `replication_target`: varchar(40)->varchar(128)
- alter column `email` on table `user`: varchar(128)->varchar(255)
- alter column `name` on table `project`: varchar(30)->varchar(41)
- create table `repository`
- alter column `password` on table `replication_target`: varchar(40)->varchar(128)

View File

@ -125,3 +125,18 @@ class ReplicationJob(Base):
update_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")) update_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"))
__table_args__ = (sa.Index('policy', "policy_id"),) __table_args__ = (sa.Index('policy', "policy_id"),)
class Repository(Base):
__tablename__ = "repository"
repository_id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String(255), nullable=False, unique=True)
project_id = sa.Column(sa.Integer, nullable=False)
owner_id = sa.Column(sa.Integer, nullable=False)
description = sa.Column(sa.Text)
pull_count = sa.Column(sa.Integer,server_default=sa.text("'0'"), nullable=False)
star_count = sa.Column(sa.Integer,server_default=sa.text("'0'"), nullable=False)
creation_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP"))
update_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"))

View File

@ -0,0 +1,54 @@
# Copyright (c) 2008-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.
"""0.3.0 to 0.4.0
Revision ID: 0.3.0
Revises:
"""
# revision identifiers, used by Alembic.
revision = '0.4.0'
down_revision = '0.3.0'
branch_labels = None
depends_on = None
from alembic import op
from db_meta import *
from sqlalchemy.dialects import mysql
def upgrade():
"""
update schema&data
"""
bind = op.get_bind()
#alter column user.username, alter column user.email, project.name and add column replication_policy.deleted
op.alter_column('user', 'username', type_=sa.String(32), existing_type=sa.String(15))
op.alter_column('user', 'email', type_=sa.String(255), existing_type=sa.String(128))
op.alter_column('project', 'name', type_=sa.String(41), existing_type=sa.String(30), nullable=False)
op.alter_column('replication_target', 'password', type_=sa.String(128), existing_type=sa.String(40))
op.add_column('replication_policy', sa.Column('deleted', mysql.TINYINT(1), nullable=False, server_default=sa.text("'0'")))
#create index pid_optime (project_id, op_time) on table access_log, poid_uptime (policy_id, update_time) on table replication_job
op.create_index('pid_optime', 'access_log', ['project_id', 'op_time'])
op.create_index('poid_uptime', 'replication_job', ['policy_id', 'update_time'])
#create tables: repository
Repository.__table__.create(bind)
def downgrade():
"""
Downgrade has been disabled.
"""
pass

View File

@ -1,16 +1,16 @@
/* /*
Copyright (c) 2016 VMware, Inc. All Rights Reserved. Copyright (c) 2016 VMware, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
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.
*/ */
package models package models
@ -23,8 +23,9 @@ func init() {
orm.RegisterModel(new(RepTarget), orm.RegisterModel(new(RepTarget),
new(RepPolicy), new(RepPolicy),
new(RepJob), new(RepJob),
new(User), new(User),
new(Project), new(Project),
new(Role), new(Role),
new(AccessLog)) new(AccessLog),
new(RepoRecord))
} }

9
models/models_test.go Normal file
View File

@ -0,0 +1,9 @@
package models
import (
"testing"
)
func TestMain(t *testing.T) {
}

View File

@ -25,17 +25,17 @@ type Project struct {
OwnerID int `orm:"column(owner_id)" json:"owner_id"` OwnerID int `orm:"column(owner_id)" json:"owner_id"`
Name string `orm:"column(name)" json:"name"` Name string `orm:"column(name)" json:"name"`
CreationTime time.Time `orm:"column(creation_time)" json:"creation_time"` CreationTime time.Time `orm:"column(creation_time)" json:"creation_time"`
CreationTimeStr string `json:"creation_time_str"` CreationTimeStr string `orm:"-" json:"creation_time_str"`
Deleted int `orm:"column(deleted)" json:"deleted"` Deleted int `orm:"column(deleted)" json:"deleted"`
//UserID int `json:"UserId"` //UserID int `json:"UserId"`
OwnerName string `json:"owner_name"` OwnerName string `orm:"-" json:"owner_name"`
Public int `orm:"column(public)" json:"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 `orm:"-"`
UpdateTime time.Time `orm:"update_time" json:"update_time"` UpdateTime time.Time `orm:"update_time" json:"update_time"`
Role int `json:"current_user_role_id"` Role int `orm:"-" json:"current_user_role_id"`
RepoCount int `json:"repo_count"` RepoCount int `orm:"-" json:"repo_count"`
} }
// ProjectSorter holds an array of projects // ProjectSorter holds an array of projects

View File

@ -63,6 +63,7 @@ type RepPolicy struct {
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"` CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"` UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
ErrorJobCount int `json:"error_job_count"` ErrorJobCount int `json:"error_job_count"`
Deleted int `orm:"column(deleted)" json:"deleted"`
} }
// Valid ... // Valid ...

View File

@ -3,9 +3,7 @@
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -19,44 +17,22 @@ import (
"time" "time"
) )
// Repo holds information about repositories. // RepoRecord holds the record of an repository in DB, all the infors are from the registry notification event.
type Repo struct { type RepoRecord struct {
Repositories []string `json:"repositories"` RepositoryID string `orm:"column(repository_id);pk" json:"repository_id"`
Name string `orm:"column(name)" json:"name"`
OwnerName string `orm:"-"`
OwnerID int64 `orm:"column(owner_id)" json:"owner_id"`
ProjectName string `orm:"-"`
ProjectID int64 `orm:"column(project_id)" json:"project_id"`
Description string `orm:"column(description)" json:"description"`
PullCount int64 `orm:"column(pull_count)" json:"pull_count"`
StarCount int64 `orm:"column(star_count)" json:"star_count"`
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
} }
// RepoItem holds manifest of an image. //TableName is required by by beego orm to map RepoRecord to table repository
type RepoItem struct { func (rp *RepoRecord) TableName() string {
ID string `json:"Id"` return "repository"
Parent string `json:"Parent"`
Created time.Time `json:"Created"`
DurationDays string `json:"Duration Days"`
Author string `json:"Author"`
Architecture string `json:"Architecture"`
DockerVersion string `json:"Docker Version"`
Os string `json:"OS"`
//Size int `json:"Size"`
}
// Tag holds information about a tag.
type Tag struct {
Version string `json:"version"`
ImageID string `json:"image_id"`
}
// Manifest ...
type Manifest struct {
SchemaVersion int `json:"schemaVersion"`
Name string `json:"name"`
Tag string `json:"tag"`
Architecture string `json:"architecture"`
FsLayers []blobSumItem `json:"fsLayers"`
History []histroyItem `json:"history"`
}
type histroyItem struct {
V1Compatibility string `json:"v1Compatibility"`
}
type blobSumItem struct {
BlobSum string `json:"blobSum"`
} }

View File

@ -16,9 +16,9 @@
package cache package cache
import ( import (
"os"
"time" "time"
"github.com/vmware/harbor/dao"
"github.com/vmware/harbor/utils/log" "github.com/vmware/harbor/utils/log"
"github.com/vmware/harbor/utils/registry" "github.com/vmware/harbor/utils/registry"
"github.com/vmware/harbor/utils/registry/auth" "github.com/vmware/harbor/utils/registry/auth"
@ -28,9 +28,7 @@ import (
var ( var (
// Cache is the global cache in system. // Cache is the global cache in system.
Cache cache.Cache Cache cache.Cache
endpoint string
username string
) )
const catalogKey string = "catalog" const catalogKey string = "catalog"
@ -41,52 +39,17 @@ func init() {
if err != nil { if err != nil {
log.Errorf("Failed to initialize cache, error:%v", err) log.Errorf("Failed to initialize cache, error:%v", err)
} }
endpoint = os.Getenv("REGISTRY_URL")
username = "admin"
} }
// RefreshCatalogCache calls registry's API to get repository list and write it to cache. // RefreshCatalogCache calls registry's API to get repository list and write it to cache.
func RefreshCatalogCache() error { func RefreshCatalogCache() error {
log.Debug("refreshing catalog cache...") log.Debug("refreshing catalog cache...")
registryClient, err := NewRegistryClient(endpoint, true, username, repos, err := getAllRepositories()
"registry", "catalog", "*")
if err != nil { if err != nil {
return err return err
} }
Cache.Put(catalogKey, repos, 600*time.Second)
rs, err := registryClient.Catalog()
if err != nil {
return err
}
/*
repos := []string{}
for _, repo := range rs {
rc, ok := repositoryClients[repo]
if !ok {
rc, err = registry.NewRepositoryWithUsername(repo, endpoint, username)
if err != nil {
log.Errorf("error occurred while initializing repository client used by cache: %s %v", repo, err)
continue
}
repositoryClients[repo] = rc
}
tags, err := rc.ListTag()
if err != nil {
log.Errorf("error occurred while list tag for %s: %v", repo, err)
continue
}
if len(tags) != 0 {
repos = append(repos, repo)
log.Debugf("add %s to catalog cache", repo)
}
}
*/
Cache.Put(catalogKey, rs, 600*time.Second)
return nil return nil
} }
@ -108,6 +71,18 @@ func GetRepoFromCache() ([]string, error) {
return result.([]string), nil return result.([]string), nil
} }
func getAllRepositories() ([]string, error) {
var repos []string
rs, err := dao.GetAllRepositories()
if err != nil {
return repos, err
}
for _, e := range rs {
repos = append(repos, e.Name)
}
return repos, nil
}
// NewRegistryClient ... // NewRegistryClient ...
func NewRegistryClient(endpoint string, insecure bool, username, scopeType, scopeName string, func NewRegistryClient(endpoint string, insecure bool, username, scopeType, scopeName string,
scopeActions ...string) (*registry.Registry, error) { scopeActions ...string) (*registry.Registry, error) {

9
service/cache/cache_test.go vendored Normal file
View File

@ -0,0 +1,9 @@
package cache
import (
"testing"
)
func TestMain(t *testing.T) {
}

View File

@ -24,6 +24,7 @@ import (
"github.com/vmware/harbor/dao" "github.com/vmware/harbor/dao"
"github.com/vmware/harbor/models" "github.com/vmware/harbor/models"
"github.com/vmware/harbor/service/cache" "github.com/vmware/harbor/service/cache"
"github.com/vmware/harbor/utils"
"github.com/vmware/harbor/utils/log" "github.com/vmware/harbor/utils/log"
"github.com/astaxie/beego" "github.com/astaxie/beego"
@ -55,11 +56,7 @@ func (n *NotificationHandler) Post() {
for _, event := range events { for _, event := range events {
repository := event.Target.Repository repository := event.Target.Repository
project := "" project, _ := utils.ParseRepository(repository)
if strings.Contains(repository, "/") {
project = repository[0:strings.LastIndex(repository, "/")]
}
tag := event.Target.Tag tag := event.Target.Tag
action := event.Action action := event.Action
@ -80,6 +77,18 @@ func (n *NotificationHandler) Post() {
} }
}() }()
go func() {
exist := dao.RepositoryExists(repository)
if exist {
return
}
log.Debugf("Add repository %s into DB.", repository)
repoRecord := models.RepoRecord{Name: repository, OwnerName: user, ProjectName: project}
if err := dao.AddRepository(repoRecord); err != nil {
log.Errorf("Error happens when adding repository: %v", err)
}
}()
operation := "" operation := ""
if action == "push" { if action == "push" {
operation = models.RepOpTransfer operation = models.RepOpTransfer
@ -87,6 +96,14 @@ func (n *NotificationHandler) Post() {
go api.TriggerReplicationByRepository(repository, []string{tag}, operation) go api.TriggerReplicationByRepository(repository, []string{tag}, operation)
} }
if action == "pull" {
go func() {
log.Debugf("Increase the repository %s pull count.", repository)
if err := dao.IncreasePullCount(repository); err != nil {
log.Errorf("Error happens when increasing pull count: %v", repository)
}
}()
}
} }
} }

9
service/service_test.go Normal file
View File

@ -0,0 +1,9 @@
package service
import (
"testing"
)
func TestMain(t *testing.T) {
}

View File

@ -0,0 +1,9 @@
package token
import (
"testing"
)
func TestMain(t *testing.T) {
}

View File

@ -17,9 +17,10 @@
package utils package utils
import ( import (
"github.com/vmware/harbor/utils/log"
"net/http" "net/http"
"os" "os"
"github.com/vmware/harbor/utils/log"
) )
// VerifySecret verifies the UI_SECRET cookie in a http request. // VerifySecret verifies the UI_SECRET cookie in a http request.
@ -27,7 +28,7 @@ func VerifySecret(r *http.Request) bool {
secret := os.Getenv("UI_SECRET") secret := os.Getenv("UI_SECRET")
c, err := r.Cookie("uisecret") c, err := r.Cookie("uisecret")
if err != nil { if err != nil {
log.Errorf("Failed to get secret cookie, error: %v", err) log.Warningf("Failed to get secret cookie, error: %v", err)
} }
return c != nil && c.Value == secret return c != nil && c.Value == secret
} }

View File

@ -0,0 +1,9 @@
package utils
import (
"testing"
)
func TestMain(t *testing.T) {
}

View File

@ -73,13 +73,11 @@
} }
.sub-pane { .sub-pane {
margin: 15px;
min-height: 380px; min-height: 380px;
overflow-y: auto; overflow-y: auto;
} }
.well-custom { .well-custom {
width: 100%; width: 100%;
background-color: #f5f5f5; background-color: #f5f5f5;
background-image: none; background-image: none;

View File

@ -27,64 +27,55 @@
vm.projectName = ''; vm.projectName = '';
vm.isOpen = false; vm.isOpen = false;
vm.isProjectMember = false;
vm.target = $location.path().substr(1) || 'repositories';
if(getParameterByName('is_public', $location.absUrl())) { vm.isPublic = Number(getParameterByName('is_public', $location.absUrl()));
vm.isPublic = getParameterByName('is_public', $location.absUrl()) === 'true' ? 1 : 0;
vm.publicity = (vm.isPublic === 1) ? true : false;
}
vm.retrieve = retrieve; vm.retrieve = retrieve;
vm.filterInput = ''; vm.filterInput = '';
vm.selectItem = selectItem; vm.selectItem = selectItem;
vm.checkProjectMember = checkProjectMember; vm.checkProjectMember = checkProjectMember;
$scope.$watch('vm.selectedProject', function(current, origin) {
if(current) {
vm.selectedId = current.project_id;
}
});
$scope.$watch('vm.publicity', function(current, origin) {
vm.publicity = current ? true : false;
vm.isPublic = vm.publicity ? 1 : 0;
vm.projectType = (vm.isPublic === 1) ? 'public_projects' : 'my_projects';
vm.retrieve();
});
function retrieve() { function retrieve() {
ListProjectService(vm.projectName, vm.isPublic) ListProjectService(vm.projectName, vm.isPublic)
.success(getProjectSuccess) .success(getProjectSuccess)
.error(getProjectFailed); .error(getProjectFailed);
} }
vm.retrieve();
$scope.$watch('vm.isPublic', function(current) {
vm.projectType = vm.isPublic === 0 ? 'my_project_count' : 'public_project_count';
});
$scope.$watch('vm.selectedProject', function(current) {
if(current) {
vm.selectedId = current.project_id;
}
});
function getProjectSuccess(data, status) { function getProjectSuccess(data, status) {
vm.projects = data; vm.projects = data || [];
if(vm.projects == null) { if(vm.projects.length == 0 && vm.isPublic === 0){
vm.isPublic = 1; $window.location.href = '/project';
vm.publicity = true;
vm.projectType = 'public_projects';
console.log('vm.projects is null, load public projects.');
return;
} }
if(angular.isArray(vm.projects) && vm.projects.length > 0) {
vm.selectedProject = vm.projects[0];
}else{
$window.location.href = '/project';
}
if(getParameterByName('project_id', $location.absUrl())){ if(getParameterByName('project_id', $location.absUrl())){
angular.forEach(vm.projects, function(value, index) { for(var i in vm.projects) {
if(value['project_id'] === Number(getParameterByName('project_id', $location.absUrl()))) { var project = vm.projects[i];
vm.selectedProject = value; if(project['project_id'] == getParameterByName('project_id', $location.absUrl())) {
vm.selectedProject = project;
break;
} }
}); }
} }
$location.search('project_id', vm.selectedProject.project_id); $location.search('project_id', vm.selectedProject.project_id);
vm.checkProjectMember(vm.selectedProject.project_id);
vm.checkProjectMember(vm.selectedProject.project_id);
vm.resultCount = vm.projects.length; vm.resultCount = vm.projects.length;
$scope.$watch('vm.filterInput', function(current, origin) { $scope.$watch('vm.filterInput', function(current, origin) {
@ -102,11 +93,13 @@
function selectItem(item) { function selectItem(item) {
vm.selectedProject = item; vm.selectedProject = item;
$location.search('project_id', vm.selectedProject.project_id); $location.search('project_id', vm.selectedProject.project_id);
$scope.$emit('projectChanged', true);
} }
$scope.$on('$locationChangeSuccess', function(e) { $scope.$on('$locationChangeSuccess', function(e) {
var projectId = getParameterByName('project_id', $location.absUrl()); vm.projectId = getParameterByName('project_id', $location.absUrl());
vm.isOpen = false; vm.isOpen = false;
vm.checkProjectMember(vm.selectedProject.project_id);
}); });
function checkProjectMember(projectId) { function checkProjectMember(projectId) {
@ -121,8 +114,9 @@
} }
function getCurrentProjectMemberFailed(data, status) { function getCurrentProjectMemberFailed(data, status) {
vm.isProjectMember = false; vm.isProjectMember = false;
console.log('Current user has no member for the project:' + status + ', location.url:' + $location.url()); console.log('Current user has no member for the project:' + status + ', location.url:' + $location.url());
vm.target = 'repositories';
} }
} }
@ -132,9 +126,10 @@
restrict: 'E', restrict: 'E',
templateUrl: '/static/resources/js/components/details/retrieve-projects.directive.html', templateUrl: '/static/resources/js/components/details/retrieve-projects.directive.html',
scope: { scope: {
'target': '=',
'isOpen': '=', 'isOpen': '=',
'selectedProject': '=', 'selectedProject': '=',
'publicity': '=', 'isPublic': '=',
'isProjectMember': '=' 'isProjectMember': '='
}, },
link: link, link: link,
@ -147,7 +142,7 @@
function link(scope, element, attrs, ctrl) { function link(scope, element, attrs, ctrl) {
$(document).on('click', clickHandler); $(document).on('click', clickHandler);
function clickHandler(e) { function clickHandler(e) {
$('[data-toggle="popover"]').each(function () { $('[data-toggle="popover"]').each(function () {
if (!$(this).is(e.target) && if (!$(this).is(e.target) &&

View File

@ -20,6 +20,8 @@
<span class="input-group-btn"> <span class="input-group-btn">
<button class="btn btn-primary" type="button" ng-click="vm.search({op: vm.op, username: vm.username})"><span class="glyphicon glyphicon-search"></span></button> <button class="btn btn-primary" type="button" ng-click="vm.search({op: vm.op, username: vm.username})"><span class="glyphicon glyphicon-search"></span></button>
</span> </span>
</div>
<div class="input-group">
<span class="input-group-btn"> <span class="input-group-btn">
<button class="btn btn-link" type="button" ng-click="vm.showAdvancedSearch()">// 'advanced_search' | tr //</button> <button class="btn btn-link" type="button" ng-click="vm.showAdvancedSearch()">// 'advanced_search' | tr //</button>
</span> </span>
@ -48,7 +50,8 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
<paginator ng-if="vm.totalCount > 0" total-count="//vm.totalCount//" page-size="//vm.pageSize//" page="vm.page" display-count="5"></paginator>
</div> </div>
</div> </div>
</div> </div>

View File

@ -35,7 +35,8 @@
vm.beginTimestamp = 0; vm.beginTimestamp = 0;
vm.endTimestamp = 0; vm.endTimestamp = 0;
vm.keywords = ''; vm.keywords = '';
vm.username = '';
vm.username = $location.hash() || '';
vm.op = []; vm.op = [];
vm.opOthers = true; vm.opOthers = true;
@ -51,29 +52,36 @@
'projectId': vm.projectId, 'projectId': vm.projectId,
'username' : vm.username 'username' : vm.username
}; };
retrieve(vm.queryParams); vm.page = 1;
vm.pageSize = 15;
$scope.$on('$locationChangeSuccess', function() {
$scope.$watch('vm.page', function(current, origin) {
if(vm.publicity) { if(current) {
vm.target = 'repositories'; vm.page = current;
retrieve(vm.queryParams, vm.page, vm.pageSize);
} }
});
vm.projectId = getParameterByName('project_id', $location.absUrl()); $scope.$on('retrieveData', function(e, val) {
vm.queryParams = { if(val) {
'beginTimestamp' : vm.beginTimestamp, vm.projectId = getParameterByName('project_id', $location.absUrl());
'endTimestamp' : vm.endTimestamp, vm.queryParams = {
'keywords' : vm.keywords, 'beginTimestamp' : vm.beginTimestamp,
'projectId': vm.projectId, 'endTimestamp' : vm.endTimestamp,
'username' : vm.username 'keywords' : vm.keywords,
}; 'projectId': vm.projectId,
vm.username = ''; 'username' : vm.username
retrieve(vm.queryParams); };
vm.username = '';
retrieve(vm.queryParams, vm.page, vm.pageSize);
}
}); });
function search(e) { function search(e) {
vm.page = 1;
if(e.op[0] === 'all') { if(e.op[0] === 'all') {
e.op = ['create', 'pull', 'push', 'delete']; e.op = ['create', 'pull', 'push', 'delete'];
} }
@ -83,11 +91,12 @@
vm.queryParams.keywords = e.op.join('/'); vm.queryParams.keywords = e.op.join('/');
vm.queryParams.username = e.username; vm.queryParams.username = e.username;
vm.queryParams.beginTimestamp = toUTCSeconds(vm.fromDate, 0, 0, 0); vm.queryParams.beginTimestamp = toUTCSeconds(vm.fromDate, 0, 0, 0);
vm.queryParams.endTimestamp = toUTCSeconds(vm.toDate, 23, 59, 59); vm.queryParams.endTimestamp = toUTCSeconds(vm.toDate, 23, 59, 59);
retrieve(vm.queryParams); retrieve(vm.queryParams, vm.page, vm.pageSize);
} }
function showAdvancedSearch() { function showAdvancedSearch() {
@ -98,27 +107,18 @@
} }
} }
function retrieve(queryParams) { function retrieve(queryParams, page, pageSize) {
ListLogService(queryParams) ListLogService(queryParams, page, pageSize)
.then(listLogComplete) .then(listLogComplete)
.catch(listLogFailed); .catch(listLogFailed);
} }
function listLogComplete(response) { function listLogComplete(response) {
vm.logs = response.data; vm.logs = response.data;
vm.totalCount = response.headers('X-Total-Count');
console.log('Total Count in logs:' + vm.totalCount + ', page:' + vm.page);
vm.queryParams = {
'beginTimestamp' : 0,
'endTimestamp' : 0,
'keywords' : '',
'projectId': vm.projectId,
'username' : ''
};
vm.op = ['all'];
vm.fromDate = '';
vm.toDate = '';
vm.others = '';
vm.opOthers = true;
vm.isOpen = false; vm.isOpen = false;
} }
function listLogFailed(response){ function listLogFailed(response){
@ -148,9 +148,7 @@
'restrict': 'E', 'restrict': 'E',
'templateUrl': '/static/resources/js/components/log/list-log.directive.html', 'templateUrl': '/static/resources/js/components/log/list-log.directive.html',
'scope': { 'scope': {
'sectionHeight': '=', 'sectionHeight': '='
'target': '=',
'publicity': '='
}, },
'link': link, 'link': link,
'controller': ListLogController, 'controller': ListLogController,

View File

@ -20,9 +20,9 @@
.module('harbor.optional.menu') .module('harbor.optional.menu')
.directive('optionalMenu', optionalMenu); .directive('optionalMenu', optionalMenu);
OptionalMenuController.$inject = ['$window', 'I18nService', 'LogOutService', 'currentUser', '$timeout']; OptionalMenuController.$inject = ['$scope', '$window', 'I18nService', 'LogOutService', 'currentUser', '$timeout', 'trFilter', '$filter'];
function OptionalMenuController($window, I18nService, LogOutService, currentUser, $timeout) { function OptionalMenuController($scope, $window, I18nService, LogOutService, currentUser, $timeoutm, trFilter, $filter) {
var vm = this; var vm = this;
vm.currentLanguage = I18nService().getCurrentLanguage(); vm.currentLanguage = I18nService().getCurrentLanguage();
@ -36,6 +36,7 @@
vm.user = currentUser.get(); vm.user = currentUser.get();
vm.setLanguage = setLanguage; vm.setLanguage = setLanguage;
vm.logOut = logOut; vm.logOut = logOut;
vm.about = about;
function setLanguage(language) { function setLanguage(language) {
I18nService().setCurrentLanguage(language); I18nService().setCurrentLanguage(language);
@ -54,13 +55,25 @@
function logOutFailed(data, status) { function logOutFailed(data, status) {
console.log('Failed to log out:' + data); console.log('Failed to log out:' + data);
} }
function about() {
$scope.$emit('modalTitle', $filter('tr')('about_harbor'));
$scope.$emit('modalMessage', $filter('tr')('current_version', [vm.version || 'Unknown']));
var raiseInfo = {
'confirmOnly': true,
'contentType': 'text/html',
'action': function() {}
};
$scope.$emit('raiseInfo', raiseInfo);
}
} }
function optionalMenu() { function optionalMenu() {
var directive = { var directive = {
'restrict': 'E', 'restrict': 'E',
'templateUrl': '/optional_menu?timestamp=' + new Date().getTime(), 'templateUrl': '/optional_menu?timestamp=' + new Date().getTime(),
'scope': true, 'scope': {
'version': '@'
},
'controller': OptionalMenuController, 'controller': OptionalMenuController,
'controllerAs': 'vm', 'controllerAs': 'vm',
'bindToController': true 'bindToController': true

View File

@ -0,0 +1,14 @@
<nav aria-label="Page navigation" class="pull-left">
<ul class="pagination" style="margin: 0;">
<li>
<a href="javascript:void(0);" ng-click="vm.previous()" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
<li>
<a href="javascript:void(0);" ng-click="vm.next()" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
</ul>
</nav>

View File

@ -0,0 +1,196 @@
(function() {
'use strict';
angular
.module('harbor.paginator')
.directive('paginator', paginator);
PaginatorController.$inject = [];
function PaginatorController() {
var vm = this;
}
paginator.$inject = [];
function paginator() {
var directive = {
'restrict': 'E',
'templateUrl': '/static/resources/js/components/paginator/paginator.directive.html',
'scope': {
'totalCount': '@',
'pageSize': '@',
'page': '=',
'displayCount': '@'
},
'link': link,
'controller': PaginatorController,
'controllerAs': 'vm',
'bindToController': true
};
return directive;
function link(scope, element, attrs, ctrl) {
scope.$watch('vm.page', function(current) {
if(current) {
ctrl.page = current;
togglePageButton();
}
});
var tc;
scope.$watch('vm.totalCount', function(current) {
if(current) {
var totalCount = current;
element.find('ul li:first a').off('click');
element.find('ul li:last a').off('click');
tc = new TimeCounter();
console.log('Total Count:' + totalCount + ', Page Size:' + ctrl.pageSize + ', Display Count:' + ctrl.displayCount + ', Page:' + ctrl.page);
ctrl.buttonCount = Math.ceil(totalCount / ctrl.pageSize);
if(ctrl.buttonCount <= ctrl.displayCount) {
tc.setMaximum(1);
}else{
tc.setMaximum(Math.ceil(ctrl.buttonCount / ctrl.displayCount));
}
element.find('ul li:first a').on('click', previous);
element.find('ul li:last a').on('click', next);
drawButtons(tc.getTime());
togglePrevious(tc.canDecrement());
toggleNext(tc.canIncrement());
togglePageButton();
}
});
var TimeCounter = function() {
this.time = 0;
this.minimum = 0;
this.maximum = 0;
};
TimeCounter.prototype.setMaximum = function(maximum) {
this.maximum = maximum;
};
TimeCounter.prototype.increment = function() {
if(this.time < this.maximum) {
++this.time;
if((ctrl.page % ctrl.displayCount) != 0) {
ctrl.page = this.time * ctrl.displayCount;
}
++ctrl.page;
}
scope.$apply();
};
TimeCounter.prototype.canIncrement = function() {
if(this.time + 1 < this.maximum) {
return true;
}
return false;
};
TimeCounter.prototype.decrement = function() {
if(this.time > this.minimum) {
if(this.time === 0) {
ctrl.page = ctrl.displayCount;
}else if((ctrl.page % ctrl.displayCount) != 0) {
ctrl.page = this.time * ctrl.displayCount;
}
--this.time;
--ctrl.page;
}
scope.$apply();
};
TimeCounter.prototype.canDecrement = function() {
if(this.time > this.minimum) {
return true;
}
return false;
};
TimeCounter.prototype.getTime = function() {
return this.time;
};
function drawButtons(time) {
element.find('li[tag="pagination-button"]').remove();
var buttons = [];
for(var i = 1; i <= ctrl.displayCount; i++) {
var displayNumber = ctrl.displayCount * time + i;
if(displayNumber <= ctrl.buttonCount) {
buttons.push('<li tag="pagination-button"><a href="javascript:void(0)" page="' + displayNumber + '">' + displayNumber + '<span class="sr-only"></span></a></li>');
}
}
$(buttons.join(''))
.insertAfter(element.find('ul li:eq(0)')).end()
.on('click', buttonClickHandler);
}
function togglePrevious(status) {
if(status){
element.find('ul li:first').removeClass('disabled');
}else{
element.find('ul li:first').addClass('disabled');
}
}
function toggleNext(status) {
if(status) {
element.find('ul li:last').removeClass('disabled');
}else{
element.find('ul li:last').addClass('disabled');
}
}
function buttonClickHandler(e) {
ctrl.page = $(e.target).attr('page');
togglePageButton();
togglePrevious(tc.canDecrement());
toggleNext(tc.canIncrement());
scope.$apply();
}
function togglePageButton() {
element.find('li[tag="pagination-button"]').removeClass('active');
element.find('li[tag="pagination-button"] a[page="' + ctrl.page + '"]').parent().addClass('active');
}
function previous() {
if(tc.canDecrement()) {
tc.decrement();
drawButtons(tc.getTime());
togglePageButton();
togglePrevious(tc.canDecrement());
toggleNext(tc.canIncrement());
}
scope.$apply();
}
function next() {
if(tc.canIncrement()) {
tc.increment();
drawButtons(tc.getTime());
togglePageButton();
togglePrevious(tc.canDecrement());
toggleNext(tc.canIncrement());
}
scope.$apply();
}
}
}
})();

View File

@ -0,0 +1,8 @@
(function() {
'use strict';
angular
.module('harbor.paginator', []);
})();

View File

@ -40,10 +40,13 @@
vm.projectId = getParameterByName('project_id', $location.absUrl()); vm.projectId = getParameterByName('project_id', $location.absUrl());
vm.retrieve(); vm.retrieve();
$scope.$on('$locationChangeSuccess', function() { $scope.$on('retrieveData', function(e, val) {
vm.projectId = getParameterByName('project_id', $location.absUrl()); if(val) {
vm.username = ''; console.log('received retrieve data:' + val);
vm.retrieve(); vm.projectId = getParameterByName('project_id', $location.absUrl());
vm.username = '';
vm.retrieve();
}
}); });
function search(e) { function search(e) {
@ -91,8 +94,7 @@
function getProjectMemberFailed(response) { function getProjectMemberFailed(response) {
console.log('Failed to get project members:' + response); console.log('Failed to get project members:' + response);
vm.projectMembers = []; vm.projectMembers = [];
vm.target = 'repositories';
$location.url('repositories').search('project_id', vm.projectId); $location.url('repositories').search('project_id', vm.projectId);
} }
@ -103,8 +105,7 @@
'restrict': 'E', 'restrict': 'E',
'templateUrl': '/static/resources/js/components/project-member/list-project-member.directive.html', 'templateUrl': '/static/resources/js/components/project-member/list-project-member.directive.html',
'scope': { 'scope': {
'sectionHeight': '=', 'sectionHeight': '='
'target': '='
}, },
'link': link, 'link': link,
'controller': ListProjectMemberController, 'controller': ListProjectMemberController,

View File

@ -28,7 +28,7 @@
$scope.p = {}; $scope.p = {};
var vm0 = $scope.p; var vm0 = $scope.p;
vm0.projectName = ''; vm0.projectName = '';
vm.isPublic = false; vm.isPublic = 0;
vm.addProject = addProject; vm.addProject = addProject;
vm.cancel = cancel; vm.cancel = cancel;
@ -37,9 +37,20 @@
vm.hasError = false; vm.hasError = false;
vm.errorMessage = ''; vm.errorMessage = '';
$scope.$watch('vm.isOpen', function(current) {
if(current) {
$scope.form.$setPristine();
$scope.form.$setUntouched();
vm0.projectName = '';
vm.isPublic = 0;
}
});
function addProject(p) { function addProject(p) {
if(p && angular.isDefined(p.projectName)) { if(p && angular.isDefined(p.projectName)) {
vm.isPublic = vm.isPublic ? 1 : 0;
AddProjectService(p.projectName, vm.isPublic) AddProjectService(p.projectName, vm.isPublic)
.success(addProjectSuccess) .success(addProjectSuccess)
.error(addProjectFailed); .error(addProjectFailed);
@ -74,9 +85,9 @@
} }
vm.isOpen = false; vm.isOpen = false;
vm0.projectName = ''; vm0.projectName = '';
vm.isPublic = false; vm.isPublic = 0;
vm.hasError = false; vm.close = close; vm.hasError = false;
vm.errorMessage = ''; vm.errorMessage = '';
} }
@ -94,16 +105,10 @@
'scope' : { 'scope' : {
'isOpen': '=' 'isOpen': '='
}, },
'link': link,
'controllerAs': 'vm', 'controllerAs': 'vm',
'bindToController': true 'bindToController': true
}; };
return directive; return directive;
function link(scope, element, attrs, ctrl) {
scope.form.$setPristine();
scope.form.$setUntouched();
}
} }
})(); })();

Some files were not shown because too many files have changed in this diff Show More