Merge pull request #727 from ywk253100/m_t

Merge lost commits into branch dev
This commit is contained in:
Wenkai Yin 2016-08-29 16:01:17 +08:00 committed by GitHub
commit 4e065bba95
97 changed files with 3385 additions and 697 deletions

View File

@ -9,25 +9,25 @@ go_import_path: github.com/vmware/harbor
services:
- docker
- mysql
dist: trusty
addons:
apt:
packages:
- mysql-server-5.6
- mysql-client-core-5.6
- mysql-client-5.6
env:
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_USR: root
DB_PWD:
DB_PWD: root123
MYSQL_HOST: localhost
MYSQL_PORT: 3306
MYSQL_USR: root
MYSQL_PWD: root123
DOCKER_COMPOSE_VERSION: 1.7.1
HARBOR_ADMIN: admin
HARBOR_ADMIN_PASSWD: Harbor12345
UI_SECRET: tempString
MAX_JOB_WORKERS: 3
SECRET_KEY: 1234567890123456
AUTH_MODE: db_auth
before_install:
- sudo ./tests/hostcfg.sh
@ -54,7 +54,7 @@ install:
- go get -d github.com/go-sql-driver/mysql
- go get github.com/golang/lint/golint
- go get github.com/GeertJohan/fgt
# - sudo apt-get install -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" docker-engine=1.11.1-0~trusty
# - sudo rm /usr/local/bin/docker-compose
- curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose
- chmod +x docker-compose
@ -63,23 +63,32 @@ install:
- sudo service docker restart
- go get github.com/dghubble/sling
- go get github.com/stretchr/testify
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
before_script:
# create tables and load data
- mysql < ./Deploy/db/registry.sql -uroot --verbose
# - mysql < ./Deploy/db/registry.sql -uroot --verbose
script:
- go list ./... | grep -v -E 'vendor|tests' | xargs -L1 fgt golint
- go list ./... | grep -v -E 'vendor|tests' | xargs -L1 go vet
script:
- sudo ./tests/testprepare.sh
- docker-compose -f Deploy/docker-compose.test.yml up -d
- go list ./... | grep -v -E 'vendor|tests|api' | xargs -L1 fgt golint
- go list ./... | grep -v -E 'vendor|tests|api' | xargs -L1 go vet
- IP=`ip addr s eth0 |grep "inet "|awk '{print $2}' |awk -F "/" '{print $1}'`
- export MYSQL_HOST=$IP
- export REGISTRY_URL=http://$IP:5000
- echo $REGISTRY_URL
- ./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 ps
- go run tests/startuptest.go http://localhost/
- go run tests/userlogintest.go -name ${HARBOR_ADMIN} -passwd ${HARBOR_ADMIN_PASSWD}
# test for API
- sudo ./tests/testprepare.sh
- go test -v ./tests/apitests
# - sudo ./tests/testprepare.sh
# - go test -v ./tests/apitests

View File

@ -1,5 +1,5 @@
#!/bin/bash
set -e
echo "mode: set" >>profile.cov
for dir in $(go list ./... | grep -v -E 'vendor|tests')
do

View File

@ -61,7 +61,9 @@ insert into user (username, email, password, realname, comment, deleted, sysadmi
create table project (
project_id int NOT NULL AUTO_INCREMENT,
owner_id int NOT NULL,
name varchar (30) NOT NULL,
# The max length of name controlled by API is 30,
# and 11 bytes is reserved for marking the deleted project.
name varchar (41) NOT NULL,
creation_time timestamp,
update_time timestamp,
deleted tinyint (1) DEFAULT 0 NOT NULL,
@ -110,6 +112,7 @@ create table replication_policy (
target_id int NOT NULL,
enabled tinyint(1) NOT NULL DEFAULT 1,
description text,
deleted tinyint (1) DEFAULT 0 NOT NULL,
cron_str varchar(256),
start_time timestamp NULL,
creation_time timestamp default CURRENT_TIMESTAMP,
@ -122,7 +125,7 @@ create table replication_target (
name varchar(64),
url varchar(64),
username varchar(40),
password varchar(40),
password varchar(128),
/*
target_type indicates the type of target registry,
0 means it's a harbor instance,

View File

@ -44,6 +44,10 @@ use_compressed_js = on
#Maximum number of job workers in job service
max_job_workers = 3
#Secret key for encryption/decryption, its length has to be 16 chars
#**NOTE** if this changes, previously encrypted password will not be decrypted!
secret_key = secretkey1234567
#The expiration of token used by token service, default is 30 minutes
token_expiration = 30

View File

@ -1,12 +1,18 @@
#!/bin/bash
set -e
echo "This shell will minify the Javascript in Harbor project."
echo "Usage: #jsminify [src] [dest]"
echo "Usage: #jsminify [src] [dest] [basedir]"
#prepare workspace
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
echo "Concat js files..."
@ -20,6 +26,12 @@ do
fi
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
echo "Remove space.."
sed 's/ \+/ /g' -i /tmp/harbor.app.temp.js

View File

@ -16,6 +16,10 @@ if sys.version_info[:3][0] == 3:
import configparser as ConfigParser
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!")
#Read configurations
conf = StringIO.StringIO()
conf.write("[configuration]\n")
@ -24,6 +28,8 @@ conf.seek(0, os.SEEK_SET)
rcp = ConfigParser.RawConfigParser()
rcp.readfp(conf)
validate(rcp)
hostname = rcp.get("configuration", "hostname")
ui_url = rcp.get("configuration", "ui_url_protocol") + "://" + hostname
email_server = rcp.get("configuration", "email_server")
@ -50,6 +56,7 @@ crt_email = rcp.get("configuration", "crt_email")
max_job_workers = rcp.get("configuration", "max_job_workers")
token_expiration = rcp.get("configuration", "token_expiration")
verify_remote_cert = rcp.get("configuration", "verify_remote_cert")
secret_key = rcp.get("configuration", "secret_key")
########
ui_secret = ''.join(random.choice(string.ascii_letters+string.digits) for i in range(16))
@ -102,6 +109,7 @@ render(os.path.join(templates_dir, "ui", "env"),
self_registration=self_registration,
use_compressed_js=use_compressed_js,
ui_secret=ui_secret,
secret_key=secret_key,
verify_remote_cert=verify_remote_cert,
token_expiration=token_expiration)
@ -128,6 +136,7 @@ render(os.path.join(templates_dir, "jobservice", "env"),
db_password=db_password,
ui_secret=ui_secret,
max_job_workers=max_job_workers,
secret_key=secret_key,
ui_url=ui_url,
verify_remote_cert=verify_remote_cert)

View File

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

View File

@ -12,6 +12,7 @@ AUTH_MODE=$auth_mode
LDAP_URL=$ldap_url
LDAP_BASE_DN=$ldap_basedn
UI_SECRET=$ui_secret
SECRET_KEY=$secret_key
SELF_REGISTRATION=$self_registration
USE_COMPRESSED_JS=$use_compressed_js
LOG_LEVEL=debug

312
api/harborapi_test.go Normal file
View File

@ -0,0 +1,312 @@
//These APIs provide services for manipulating Harbor project.
package api
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http/httptest"
"path/filepath"
"runtime"
"github.com/vmware/harbor/tests/apitests/apilib"
"github.com/vmware/harbor/dao"
"github.com/vmware/harbor/models"
"github.com/astaxie/beego"
"github.com/dghubble/sling"
//for test env prepare
_ "github.com/vmware/harbor/auth/db"
_ "github.com/vmware/harbor/auth/ldap"
)
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")
beego.Router("/api/users/:id([0-9]+)/password", &UserAPI{}, "put:ChangePassword")
_ = updateInitPassword(1, "Harbor12345")
}
//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})
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()
w := httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, req)
body, err := ioutil.ReadAll(w.Body)
if err != nil {
// handle error
}
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.Project) (int, error) {
_sling := sling.New().Post(a.basePath)
// create path and map variables
path := "/api/projects/"
_sling = _sling.Path(path)
// accept header
accepts := []string{"application/json", "text/plain"}
for key := range accepts {
_sling = _sling.Set("Accept", accepts[key])
break // only use the first Accept
}
// body params
_sling = _sling.BodyJSON(project)
//fmt.Printf("project post req: %+v\n", _sling)
req, err := _sling.Request()
req.SetBasicAuth(prjUsr.Name, prjUsr.Passwd)
w := httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, req)
return w.Code, err
}
//Change password
//Implementation Notes
//Change the password on a user that already exists.
//@param userID user ID
//@param password user old and new password
//@return error
//func (a api) UsersUserIDPasswordPut (user usrInfo, userID int32, password apilib.Password) int {
func (a api) UsersUserIDPasswordPut(user usrInfo, userID int32, password apilib.Password) int {
_sling := sling.New().Put(a.basePath)
// create path and map variables
path := "/api/users/" + fmt.Sprintf("%d", userID) + "/password"
fmt.Printf("change passwd path: %s\n", path)
fmt.Printf("password %+v\n", password)
_sling = _sling.Path(path)
// accept header
accepts := []string{"application/json", "text/plain"}
for key := range accepts {
_sling = _sling.Set("Accept", accepts[key])
break // only use the first Accept
}
// body params
_sling = _sling.BodyJSON(password)
fmt.Printf("project post req: %+v\n", _sling)
req, err := _sling.Request()
req.SetBasicAuth(user.Name, user.Passwd)
fmt.Printf("project post req: %+v\n", req)
if err != nil {
// handle error
}
w := httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, req)
return w.Code
}
////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 HarborAPI) RepositoriesDelete(prjUsr UsrInfo, repoName string, tag string) (int, error) {
//func (a HarborAPI) 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
//}
//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 HarborApi) UsersGet (userName string) ([]User, error) {
//}
//Creates a new user account.
//func (a HarborApi) UsersPost (user User) (error) {
//}
//Mark a registered user as be removed.
//func (a HarborApi) UsersUserIdDelete (userId int32) (error) {
//}
//Update a registered user to change to be an administrator of Harbor.
//func (a HarborApi) UsersUserIdPut (userId int32) (error) {
//}
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
}

View File

@ -46,6 +46,27 @@ type ReplicationReq struct {
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.
func (rj *ReplicationJob) Post() {
var data ReplicationReq

View File

@ -31,8 +31,9 @@ import (
// ProjectAPI handles request to /api/projects/{} /api/projects/{}/logs
type ProjectAPI struct {
BaseAPI
userID int
projectID int64
userID int
projectID int64
projectName string
}
type projectReq struct {
@ -54,14 +55,16 @@ func (p *ProjectAPI) Prepare() {
log.Errorf("Error parsing project id: %s, error: %v", idStr, err)
p.CustomAbort(http.StatusBadRequest, "invalid project id")
}
exist, err := dao.ProjectExists(p.projectID)
project, err := dao.GetProjectByID(p.projectID)
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.")
}
if !exist {
if project == nil {
p.CustomAbort(http.StatusNotFound, fmt.Sprintf("project does not exist, id: %v", p.projectID))
}
p.projectName = project.Name
}
}
@ -152,6 +155,71 @@ func (p *ProjectAPI) Get() {
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 ...
func (p *ProjectAPI) List() {
var total int64

65
api/project_test.go Normal file
View File

@ -0,0 +1,65 @@
package api
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/harbor/tests/apitests/apilib"
)
func TestAddProject(t *testing.T) {
fmt.Println("Testing Add Project(ProjectsPost) API")
assert := assert.New(t)
apiTest := newHarborAPI()
//prepare for test
admin := &usrInfo{"admin", "Harbor12345"}
prjUsr := &usrInfo{"unknown", "unknown"}
var project apilib.Project
project.ProjectName = "test_project"
project.Public = true
//case 1: admin not login, expect project creation fail.
result, err := apiTest.ProjectsPost(*prjUsr, project)
if err != nil {
t.Error("Error while creat project", err.Error())
t.Log(err)
} else {
assert.Equal(result, int(401), "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.")
prjUsr = admin
result, err = apiTest.ProjectsPost(*prjUsr, project)
if err != nil {
t.Error("Error while creat project", err.Error())
t.Log(err)
} else {
assert.Equal(result, int(201), "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(*prjUsr, project)
if err != nil {
t.Error("Error while creat project", err.Error())
t.Log(err)
} else {
assert.Equal(result, int(409), "Case 3: Project creation status should be 409")
//t.Log(result)
}
}

View File

@ -147,7 +147,14 @@ func (ra *RepJobAPI) GetLog() {
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 {
log.Errorf("failed to get log for job %d: %v", ra.jobID, err)
ra.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))

View File

@ -349,3 +349,41 @@ func (pa *RepPolicyAPI) UpdateEnablement() {
}()
}
}
// Delete : policies which are disabled and have no running jobs
// can be deleted
func (r *RepPolicyAPI) Delete() {
id := r.GetIDFromURL()
policy, err := dao.GetRepPolicy(id)
if err != nil {
log.Errorf("failed to get policy %d: %v", id, err)
r.CustomAbort(http.StatusInternalServerError, "")
}
if policy == nil || policy.Deleted == 1 {
r.CustomAbort(http.StatusNotFound, "")
}
if policy.Enabled == 1 {
r.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)
r.CustomAbort(http.StatusInternalServerError, "")
}
for _, job := range jobs {
if job.Status == models.JobRunning ||
job.Status == models.JobRetrying ||
job.Status == models.JobPending {
r.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)
r.CustomAbort(http.StatusInternalServerError, "")
}
}

View File

@ -35,7 +35,6 @@ import (
registry_error "github.com/vmware/harbor/utils/registry/error"
"github.com/vmware/harbor/utils"
"github.com/vmware/harbor/utils/registry/auth"
)
@ -78,28 +77,12 @@ func (ra *RepositoryAPI) Get() {
}
}
repoList, err := cache.GetRepoFromCache()
repositories, err := getReposByProject(project.Name, ra.GetString("q"))
if err != nil {
log.Errorf("failed to get repository from cache: %v", err)
log.Errorf("failed to get repository: %v", err)
ra.CustomAbort(http.StatusInternalServerError, "")
}
repositories := []string{}
q := ra.GetString("q")
for _, repo := range repoList {
pn, rest := utils.ParseRepository(repo)
if project.Name != pn {
continue
}
if len(q) != 0 && !strings.Contains(rest, q) {
continue
}
repositories = append(repositories, repo)
}
total := int64(len(repositories))
if (page-1)*pageSize > total {

View File

@ -101,18 +101,19 @@ func filterRepositories(repositories []string, projects []models.Project, keywor
i, j := 0, 0
result := []map[string]interface{}{}
for i < len(repositories) && j < len(projects) {
r := &utils.Repository{Name: repositories[i]}
d := strings.Compare(r.GetProject(), projects[j].Name)
r := repositories[i]
p, _ := utils.ParseRepository(r)
d := strings.Compare(p, projects[j].Name)
if d < 0 {
i++
continue
} else if d == 0 {
i++
if len(keyword) != 0 && !strings.Contains(r.Name, keyword) {
if len(keyword) != 0 && !strings.Contains(r, keyword) {
continue
}
entry := make(map[string]interface{})
entry["repository_name"] = r.Name
entry["repository_name"] = r
entry["project_name"] = projects[j].Name
entry["project_id"] = projects[j].ProjectID
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

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

View File

@ -26,6 +26,8 @@ import (
"github.com/vmware/harbor/dao"
"github.com/vmware/harbor/models"
"github.com/vmware/harbor/service/cache"
"github.com/vmware/harbor/utils"
"github.com/vmware/harbor/utils/log"
)
@ -115,7 +117,14 @@ func TriggerReplication(policyID int64, repository string,
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 {
return err
}
@ -188,7 +197,16 @@ func postReplicationAction(policyID int64, acton string) error {
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 {
return err
}
@ -207,6 +225,16 @@ func postReplicationAction(policyID int64, acton string) error {
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"),
})
}
}
func buildReplicationURL() string {
url := getJobServiceURL()
return fmt.Sprintf("%s/api/jobs/replication", url)
@ -233,3 +261,34 @@ func getJobServiceURL() string {
return url
}
func getReposByProject(name string, keyword ...string) ([]string, error) {
repositories := []string{}
list, err := getAllRepos()
if err != nil {
return repositories, err
}
project := ""
rest := ""
for _, repository := range list {
project, rest = utils.ParseRepository(repository)
if project != name {
continue
}
if len(keyword) > 0 && len(keyword[0]) != 0 &&
!strings.Contains(rest, keyword[0]) {
continue
}
repositories = append(repositories, repository)
}
return repositories, nil
}
func getAllRepos() ([]string, error) {
return cache.GetRepoFromCache()
}

View File

@ -16,6 +16,7 @@
package dao
import (
"fmt"
"os"
"testing"
"time"
@ -112,6 +113,7 @@ func clearUp(username string) {
}
const username string = "Tester01"
const password string = "Abc12345"
const projectName string = "test_project"
const repoTag string = "test1.1"
const repoTag2 string = "test1.2"
@ -157,7 +159,7 @@ func TestRegister(t *testing.T) {
user := models.User{
Username: username,
Email: "tester01@vmware.com",
Password: "Abc12345",
Password: password,
Realname: "tester01",
Comment: "register",
}
@ -184,6 +186,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) {
var exists bool
var err error
@ -694,6 +731,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) {
err := DeleteProjectMember(currentProject.ProjectID, 1)
if err != nil {
@ -710,6 +762,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) {
err := ToggleUserAdminRole(currentUser.UserID, 1)
if err != nil {
@ -1330,7 +1399,7 @@ func TestDeleteRepPolicy(t *testing.T) {
if err != nil && err != orm.ErrNoRows {
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)
}
}
@ -1412,3 +1481,36 @@ func TestGetOrmer(t *testing.T) {
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)
}
}

View File

@ -277,3 +277,12 @@ func getProjects(userID int, name string, args ...int64) ([]models.Project, erro
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 replication_target rt on rp.target_id=rt.id
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 {
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, projectID)
} else if len(name) != 0 {
sql += `where rp.name like ? `
sql += `and rp.name like ? `
args = append(args, "%"+name+"%")
} else if projectID != 0 {
sql += `where rp.project_id = ? `
sql += `and rp.project_id = ? `
args = append(args, projectID)
}
@ -181,7 +182,7 @@ func FilterRepPolicies(name string, projectID int64) ([]*models.RepPolicy, error
// GetRepPolicyByName ...
func GetRepPolicyByName(name string) (*models.RepPolicy, error) {
o := GetOrmer()
sql := `select * from replication_policy where name = ?`
sql := `select * from replication_policy where deleted = 0 and name = ?`
var policy models.RepPolicy
@ -198,7 +199,7 @@ func GetRepPolicyByName(name string) (*models.RepPolicy, error) {
// GetRepPolicyByProject ...
func GetRepPolicyByProject(projectID int64) ([]*models.RepPolicy, error) {
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
@ -212,7 +213,7 @@ func GetRepPolicyByProject(projectID int64) ([]*models.RepPolicy, error) {
// GetRepPolicyByTarget ...
func GetRepPolicyByTarget(targetID int64) ([]*models.RepPolicy, error) {
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
@ -226,7 +227,7 @@ func GetRepPolicyByTarget(targetID int64) ([]*models.RepPolicy, error) {
// GetRepPolicyByProjectAndTarget ...
func GetRepPolicyByProjectAndTarget(projectID, targetID int64) ([]*models.RepPolicy, error) {
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
@ -247,7 +248,11 @@ func UpdateRepPolicy(policy *models.RepPolicy) error {
// DeleteRepPolicy ...
func DeleteRepPolicy(id int64) error {
o := GetOrmer()
_, err := o.Delete(&models.RepPolicy{ID: id})
policy := &models.RepPolicy{
ID: id,
Deleted: 1,
}
_, err := o.Update(policy, "Deleted")
return err
}

View File

@ -111,7 +111,7 @@ func ListUsers(query models.User) ([]models.User, error) {
// ToggleUserAdminRole gives a user admin role.
func ToggleUserAdminRole(userID, hasAdmin int) error {
o := GetOrmer()
queryParams := make([]interface{}, 1)
queryParams := make([]interface{}, 1)
sql := `update user set sysadmin_flag = ? where user_id = ?`
queryParams = append(queryParams, hasAdmin)
queryParams = append(queryParams, userID)
@ -185,37 +185,24 @@ func UpdateUserResetUUID(u models.User) error {
func CheckUserPassword(query models.User) (*models.User, error) {
currentUser, err := GetUser(query)
if err != nil {
return nil, err
}
if currentUser == 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)
if query.UserID != 0 {
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))
}
queryParam = append(queryParam, currentUser.Username)
queryParam = append(queryParam, utils.Encrypt(query.Password, currentUser.Salt))
o := GetOrmer()
var user []models.User
n, err := o.Raw(sql, queryParam).QueryRows(&user)
if err != nil {
return nil, err
}
if n == 0 {
log.Warning("User principal does not match password. Current:", currentUser)
return nil, nil

View File

@ -31,6 +31,7 @@ var localUIURL string
var localRegURL string
var logDir string
var uiSecret string
var secretKey string
var verifyRemoteCert string
func init() {
@ -86,6 +87,11 @@ func init() {
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: localUIURL: %s", localUIURL)
log.Debugf("config: localRegURL: %s", localRegURL)
@ -119,6 +125,11 @@ func UISecret() string {
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
func VerifyRemoteCert() bool {
return verifyRemoteCert != "off"

View File

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

View File

@ -17,7 +17,7 @@ Changelog for harbor database schema
- delete data `AMDRWS` from table `role`
- delete data `A` from table `access`
## 0.2.0
## 0.3.0
- create table `replication_policy`
- create table `replication_target`
@ -25,3 +25,9 @@ Changelog for harbor database schema
- add column `repo_tag` to table `access_log`
- alter column `repo_name` on table `access_log`
- alter column `email` on table `user`
## TODO
- 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`

View File

@ -63,6 +63,7 @@ type RepPolicy struct {
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"`
ErrorJobCount int `json:"error_job_count"`
Deleted int `orm:"column(deleted)" json:"deleted"`
}
// Valid ...

View File

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

View File

@ -48,7 +48,8 @@
</tbody>
</table>
</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>

View File

@ -51,8 +51,18 @@
'projectId': vm.projectId,
'username' : vm.username
};
retrieve(vm.queryParams);
vm.page = 1;
vm.pageSize = 20;
$scope.$watch('vm.page', function(current, origin) {
if(current !== 1) {
vm.page = current;
retrieve(vm.queryParams, vm.page, vm.pageSize);
}
});
retrieve(vm.queryParams, vm.page, vm.pageSize);
$scope.$on('$locationChangeSuccess', function() {
@ -69,11 +79,13 @@
'username' : vm.username
};
vm.username = '';
retrieve(vm.queryParams);
retrieve(vm.queryParams, vm.page, vm.pageSize);
});
function search(e) {
vm.page = 1;
if(e.op[0] === 'all') {
e.op = ['create', 'pull', 'push', 'delete'];
}
@ -83,11 +95,12 @@
vm.queryParams.keywords = e.op.join('/');
vm.queryParams.username = e.username;
vm.queryParams.beginTimestamp = toUTCSeconds(vm.fromDate, 0, 0, 0);
vm.queryParams.endTimestamp = toUTCSeconds(vm.toDate, 23, 59, 59);
retrieve(vm.queryParams);
retrieve(vm.queryParams, vm.page, vm.pageSize);
}
function showAdvancedSearch() {
@ -98,27 +111,30 @@
}
}
function retrieve(queryParams) {
ListLogService(queryParams)
function retrieve(queryParams, page, pageSize) {
ListLogService(queryParams, page, pageSize)
.then(listLogComplete)
.catch(listLogFailed);
}
function listLogComplete(response) {
vm.logs = response.data;
vm.totalCount = response.headers('X-Total-Count');
vm.queryParams = {
'beginTimestamp' : 0,
'endTimestamp' : 0,
'keywords' : '',
'projectId': vm.projectId,
'username' : ''
};
vm.op = ['all'];
vm.fromDate = '';
vm.toDate = '';
vm.others = '';
vm.opOthers = true;
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;
}
function listLogFailed(response){

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

@ -73,7 +73,9 @@
</div>
</div>
</div>
<div class="col-xs-4 col-md-12 well well-sm well-custom well-split"><div class="col-md-offset-10">//vm.replicationPolicies ? vm.replicationPolicies.length : 0// // 'items' | tr //</div></div>
<div class="col-xs-4 col-md-12 well well-sm well-custom well-split">
<div class="col-md-offset-10">//vm.replicationPolicies ? vm.replicationPolicies.length : 0// // 'items' | tr //</div>
</div>
<p class="split-handle"><span class="glyphicon glyphicon-align-justify"></span></p>
<h4 class="h4-custom-down">// 'replication_jobs' | tr //</h4>
<hr class="hr-line"/>
@ -147,7 +149,7 @@
</div>
</div>
</div>
<div class="col-xs-4 col-md-12 well well-sm well-custom well-split"><div class="col-md-offset-10">//vm.replicationJobs ? vm.replicationJobs.length : 0// // 'items' | tr //</div></div>
<paginator ng-if="vm.totalCount > 0" total-count="//vm.totalCount//" page-size="//vm.pageSize//" display-count="5" page="vm.page"></paginator>
</div>
</div>
</div>

View File

@ -59,6 +59,17 @@
vm.retrievePolicy = retrievePolicy;
vm.retrieveJob = retrieveJob;
vm.pageSize = 20;
vm.page = 1;
$scope.$watch('vm.page', function(current) {
if(current !== 1) {
vm.page = current;
console.log('replication job: vm.page:' + current);
vm.retrieveJob(vm.lastPolicyId, vm.page, vm.pageSize);
}
});
vm.confirmToTogglePolicy = confirmToTogglePolicy;
vm.togglePolicy = togglePolicy;
@ -84,14 +95,14 @@
function searchReplicationJob() {
if(vm.lastPolicyId !== -1) {
vm.searchJobTIP = true;
vm.retrieveJob(vm.lastPolicyId);
vm.retrieveJob(vm.lastPolicyId, vm.page, vm.pageSize);
}
}
function refreshReplicationJob() {
if(vm.lastPolicyId !== -1) {
vm.refreshJobTIP = true;
vm.retrieveJob(vm.lastPolicyId);
vm.retrieveJob(vm.lastPolicyId, vm.page, vm.pageSize);
}
}
@ -101,11 +112,10 @@
.error(listReplicationPolicyFailed);
}
function retrieveJob(policyId) {
function retrieveJob(policyId, page, pageSize) {
var status = (vm.currentStatus.key === 'all' ? '' : vm.currentStatus.key);
ListReplicationJobService(policyId, vm.replicationJobName, status, toUTCSeconds(vm.fromDate, 0, 0, 0), toUTCSeconds(vm.toDate, 23, 59, 59))
.success(listReplicationJobSuccess)
.error(listReplicationJobFailed);
ListReplicationJobService(policyId, vm.replicationJobName, status, toUTCSeconds(vm.fromDate, 0, 0, 0), toUTCSeconds(vm.toDate, 23, 59, 59), page, pageSize)
.then(listReplicationJobSuccess, listReplicationJobFailed);
}
function listReplicationPolicySuccess(data, status) {
@ -117,8 +127,9 @@
console.log('Failed to list replication policy:' + data);
}
function listReplicationJobSuccess(data, status) {
vm.replicationJobs = data || [];
function listReplicationJobSuccess(response) {
vm.replicationJobs = response.data || [];
vm.totalCount = response.headers('X-Total-Count');
var alertInfo = {
'show': false,
'message': ''
@ -146,8 +157,8 @@
vm.refreshJobTIP = false;
}
function listReplicationJobFailed(data, status) {
console.log('Failed to list replication job:' + data);
function listReplicationJobFailed(response) {
console.log('Failed to list replication job:' + response);
vm.searchJobTIP = false;
vm.refreshJobTIP = false;
}
@ -259,8 +270,8 @@
var uponTableHeight = element.find('#upon-pane .table-body-container').height();
var downTableHeight = element.find('#down-pane .table-body-container').height();
var handleHeight = element.find('.split-handle').height() + element.find('.split-handle').offset().top + element.find('.well').height() - 24;
var handleHeight = element.find('.split-handle').height() + element.find('.split-handle').offset().top + element.find('.well').height() - 32;
console.log('handleHeight:' + handleHeight);
var maxDownPaneHeight = 760;
element.find('.split-handle').on('mousedown', mousedownHandler);
@ -328,7 +339,7 @@
.css({'color': '#fff'});
$('a', this)
.css({'color': '#fff'});
ctrl.retrieveJob($(this).attr('policy_id'));
ctrl.retrieveJob($(this).attr('policy_id'), ctrl.page, ctrl.pageSize);
ctrl.lastPolicyId = $(this).attr('policy_id');
}

View File

@ -30,6 +30,7 @@
</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>

View File

@ -41,7 +41,9 @@
vm.filterInput = hashValue;
}
}
vm.page = 1;
vm.pageSize = 8;
vm.retrieve = retrieve;
vm.tagCount = {};
@ -61,6 +63,15 @@
}
});
$scope.$watch('vm.page', function(current) {
if(current !== 1) {
vm.page = current;
vm.retrieve();
}
});
$scope.$on('repoName', function(e, val) {
vm.repoName = val;
});
@ -76,19 +87,19 @@
$scope.$on('tags', function(e, val) {
vm.tags = val;
});
vm.deleteByRepo = deleteByRepo;
vm.deleteByTag = deleteByTag;
vm.deleteImage = deleteImage;
function retrieve(){
ListRepositoryService(vm.projectId, vm.filterInput)
.success(getRepositoryComplete)
.error(getRepositoryFailed);
ListRepositoryService(vm.projectId, vm.filterInput, vm.page, vm.pageSize)
.then(getRepositoryComplete, getRepositoryFailed);
}
function getRepositoryComplete(data, status) {
vm.repositories = data || [];
function getRepositoryComplete(response) {
vm.repositories = response.data || [];
vm.totalCount = response.headers('X-Total-Count');
$scope.$broadcast('refreshTags', true);
}

View File

@ -60,6 +60,7 @@
'harbor.system.management',
'harbor.loading.progress',
'harbor.inline.help',
'harbor.dismissable.alerts'
'harbor.dismissable.alerts',
'harbor.paginator'
]);
})();

View File

@ -26,7 +26,7 @@
return LogResult;
function LogResult(queryParams) {
function LogResult(queryParams, page, pageSize) {
var projectId = queryParams.projectId;
var username = queryParams.username;
var beginTimestamp = queryParams.beginTimestamp;
@ -34,7 +34,7 @@
var keywords = queryParams.keywords;
return $http
.post('/api/projects/' + projectId + '/logs/filter', {
.post('/api/projects/' + projectId + '/logs/filter?page=' + page + '&page_size=' + pageSize, {
'begin_timestamp' : beginTimestamp,
'end_timestamp' : endTimestamp,
'keywords' : keywords,

View File

@ -26,9 +26,9 @@
return listReplicationJob;
function listReplicationJob(policyId, repository, status, startTime, endTime) {
function listReplicationJob(policyId, repository, status, startTime, endTime, page, pageSize) {
return $http
.get('/api/jobs/replication/', {
.get('/api/jobs/replication/?page=' + page + '&page_size=' + pageSize, {
'params': {
'policy_id': policyId,
'repository': repository,

View File

@ -25,11 +25,11 @@
return ListRepository;
function ListRepository(projectId, q) {
function ListRepository(projectId, q, page, pageSize) {
$log.info('list repositories:' + projectId + ', q:' + q);
return $http
.get('/api/repositories', {
.get('/api/repositories?page=' + page + '&page_size=' + pageSize, {
'params':{
'project_id': projectId,
'q': q

View File

@ -0,0 +1,41 @@
/*
* Harbor API
*
* These APIs provide services for manipulating Harbor project.
*
* OpenAPI spec version: 0.3.0
*
* Generated by: https://github.com/swagger-api/swagger-codegen.git
*
* 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 apilib
type AccessLog struct {
// The ID of the log entry.
LogId int32 `json:"log_id,omitempty"`
// Name of the repository in this log entry.
RepoName string `json:"repo_name,omitempty"`
// Tag of the repository in this log entry.
RepoTag string `json:"repo_tag,omitempty"`
// The operation against the repository in this log entry.
Operation string `json:"operation,omitempty"`
// The time when this operation is triggered.
OpTime string `json:"op_time,omitempty"`
}

View File

@ -0,0 +1,38 @@
/*
* Harbor API
*
* These APIs provide services for manipulating Harbor project.
*
* OpenAPI spec version: 0.3.0
*
* Generated by: https://github.com/swagger-api/swagger-codegen.git
*
* 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 apilib
type AccessLogFilter struct {
// Relevant user's name that accessed this project.
Username string `json:"username,omitempty"`
// Operation name specified when project created.
Keywords string `json:"keywords,omitempty"`
// Begin timestamp for querying access logs.
BeginTimestamp int64 `json:"begin_timestamp,omitempty"`
// End timestamp for querying accessl logs.
EndTimestamp int64 `json:"end_timestamp,omitempty"`
}

View File

@ -1,23 +0,0 @@
/*
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package HarborAPI
type AccessLog struct {
Username string `json:"username,omitempty"`
Keywords string `json:"keywords,omitempty"`
BeginTimestamp int32 `json:"beginTimestamp,omitempty"`
EndTimestamp int32 `json:"endTimestamp,omitempty"`
}

View File

@ -1,6 +1,6 @@
//Package HarborAPI
//Package apilib
//These APIs provide services for manipulating Harbor project.
package HarborAPI
package apilib
import (
"encoding/json"

View File

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

View File

@ -1,5 +1,5 @@
// HarborLogout.go
package HarborAPI
package apilib
import (
"net/http"

View File

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

View File

@ -1,5 +1,5 @@
// HarborLogon.go
package HarborAPI
package apilib
import (
"io/ioutil"

View File

@ -0,0 +1,50 @@
/*
* Harbor API
*
* These APIs provide services for manipulating Harbor project.
*
* OpenAPI spec version: 0.3.0
*
* Generated by: https://github.com/swagger-api/swagger-codegen.git
*
* 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 apilib
type JobStatus struct {
// The job ID.
Id int64 `json:"id,omitempty"`
// The status of the job.
Status string `json:"status,omitempty"`
// The repository handled by the job.
Repository string `json:"repository,omitempty"`
// The ID of the policy that triggered this job.
PolicyId int64 `json:"policy_id,omitempty"`
// The operation of the job.
Operation string `json:"operation,omitempty"`
// The repository's used tag list.
Tags []Tags `json:"tags,omitempty"`
// The creation time of the job.
CreationTime string `json:"creation_time,omitempty"`
// The update time of the job.
UpdateTime string `json:"update_time,omitempty"`
}

View File

@ -0,0 +1,32 @@
/*
* Harbor API
*
* These APIs provide services for manipulating Harbor project.
*
* OpenAPI spec version: 0.3.0
*
* Generated by: https://github.com/swagger-api/swagger-codegen.git
*
* 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 apilib
type Password struct {
// The user's existing password.
OldPassword string `json:"old_password,omitempty"`
// New password for marking as to be updated.
NewPassword string `json:"new_password,omitempty"`
}

View File

@ -1,15 +1,62 @@
package HarborAPI
/*
* Harbor API
*
* These APIs provide services for manipulating Harbor project.
*
* OpenAPI spec version: 0.3.0
*
* Generated by: https://github.com/swagger-api/swagger-codegen.git
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import ()
package apilib
type Project struct {
ProjectId int32 `json:"id,omitempty"`
OwnerId int32 `json:"owner_id,omitempty"`
ProjectName string `json:"project_name,omitempty"`
// Project ID
ProjectId int32 `json:"project_id,omitempty"`
// The owner ID of the project always means the creator of the project.
OwnerId int32 `json:"owner_id,omitempty"`
// The name of the project.
ProjectName string `json:"project_name,omitempty"`
// The creation time of the project.
CreationTime string `json:"creation_time,omitempty"`
Deleted int32 `json:"deleted,omitempty"`
UserId int32 `json:"user_id,omitempty"`
OwnerName string `json:"owner_name,omitempty"`
Public bool `json:"public,omitempty"`
Togglable bool `json:"togglable,omitempty"`
// The update time of the project.
UpdateTime string `json:"update_time,omitempty"`
// A deletion mark of the project (1 means it's deleted, 0 is not)
Deleted int32 `json:"deleted,omitempty"`
// A relation field to the user table.
UserId int32 `json:"user_id,omitempty"`
// The owner name of the project.
OwnerName string `json:"owner_name,omitempty"`
// The public status of the project.
Public bool `json:"public,omitempty"`
// Correspond to the UI about whether the project's publicity is updatable (for UI)
Togglable bool `json:"togglable,omitempty"`
// The role ID of the current user who triggered the API (for UI)
CurrentUserRoleId int32 `json:"current_user_role_id,omitempty"`
// The number of the repositories under this project.
RepoCount int32 `json:"repo_count,omitempty"`
}

View File

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

View File

@ -0,0 +1,62 @@
/*
* Harbor API
*
* These APIs provide services for manipulating Harbor project.
*
* OpenAPI spec version: 0.3.0
*
* Generated by: https://github.com/swagger-api/swagger-codegen.git
*
* 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 apilib
type RepPolicy struct {
// The policy ID.
Id int64 `json:"id,omitempty"`
// The project ID.
ProjectId int64 `json:"project_id,omitempty"`
// The project name.
ProjectName string `json:"project_name,omitempty"`
// The target ID.
TargetId int64 `json:"target_id,omitempty"`
// The target name.
TargetName string `json:"target_name,omitempty"`
// The policy name.
Name string `json:"name,omitempty"`
// The policy's enabled status.
Enabled int32 `json:"enabled,omitempty"`
// The description of the policy.
Description string `json:"description,omitempty"`
// The cron string for schedule job.
CronStr string `json:"cron_str,omitempty"`
// The start time of the policy.
StartTime string `json:"start_time,omitempty"`
// The create time of the policy.
CreationTime string `json:"creation_time,omitempty"`
// The update time of the policy.
UpdateTime string `json:"update_time,omitempty"`
}

View File

@ -0,0 +1,29 @@
/*
* Harbor API
*
* These APIs provide services for manipulating Harbor project.
*
* OpenAPI spec version: 0.3.0
*
* Generated by: https://github.com/swagger-api/swagger-codegen.git
*
* 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 apilib
type RepPolicyEnablementReq struct {
// The policy enablement flag.
Enabled int32 `json:"enabled,omitempty"`
}

View File

@ -0,0 +1,35 @@
/*
* Harbor API
*
* These APIs provide services for manipulating Harbor project.
*
* OpenAPI spec version: 0.3.0
*
* Generated by: https://github.com/swagger-api/swagger-codegen.git
*
* 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 apilib
type RepPolicyPost struct {
// The project ID.
ProjectId int64 `json:"project_id,omitempty"`
// The target ID.
TargetId int64 `json:"target_id,omitempty"`
// The policy name.
Name string `json:"name,omitempty"`
}

View File

@ -0,0 +1,41 @@
/*
* Harbor API
*
* These APIs provide services for manipulating Harbor project.
*
* OpenAPI spec version: 0.3.0
*
* Generated by: https://github.com/swagger-api/swagger-codegen.git
*
* 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 apilib
type RepPolicyUpdate struct {
// The target ID.
TargetId int64 `json:"target_id,omitempty"`
// The policy name.
Name string `json:"name,omitempty"`
// The policy's enabled status.
Enabled int32 `json:"enabled,omitempty"`
// The description of the policy.
Description string `json:"description,omitempty"`
// The cron string for schedule job.
CronStr string `json:"cron_str,omitempty"`
}

View File

@ -0,0 +1,50 @@
/*
* Harbor API
*
* These APIs provide services for manipulating Harbor project.
*
* OpenAPI spec version: 0.3.0
*
* Generated by: https://github.com/swagger-api/swagger-codegen.git
*
* 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 apilib
type RepTarget struct {
// The target ID.
Id int64 `json:"id,omitempty"`
// The target address URL string.
Endpoint string `json:"endpoint,omitempty"`
// The target name.
Name string `json:"name,omitempty"`
// The target server username.
Username string `json:"username,omitempty"`
// The target server password.
Password string `json:"password,omitempty"`
// Reserved field.
Type_ int32 `json:"type,omitempty"`
// The create time of the policy.
CreationTime string `json:"creation_time,omitempty"`
// The update time of the policy.
UpdateTime string `json:"update_time,omitempty"`
}

View File

@ -0,0 +1,38 @@
/*
* Harbor API
*
* These APIs provide services for manipulating Harbor project.
*
* OpenAPI spec version: 0.3.0
*
* Generated by: https://github.com/swagger-api/swagger-codegen.git
*
* 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 apilib
type RepTargetPost struct {
// The target address URL string.
Endpoint string `json:"endpoint,omitempty"`
// The target name.
Name string `json:"name,omitempty"`
// The target server username.
Username string `json:"username,omitempty"`
// The target server password.
Password string `json:"password,omitempty"`
}

View File

@ -1,16 +1,50 @@
package HarborAPI
/*
* Harbor API
*
* These APIs provide services for manipulating Harbor project.
*
* OpenAPI spec version: 0.3.0
*
* Generated by: https://github.com/swagger-api/swagger-codegen.git
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import (
"time"
)
package apilib
type Repository struct {
Id string `json:"id,omitempty"`
Parent string `json:"parent,omitempty"`
Created time.Time `json:"created,omitempty"`
DurationDays string `json:"duration_days,omitempty"`
Author string `json:"author,omitempty"`
Architecture string `json:"architecture,omitempty"`
DockerVersion string `json:"docker_version,omitempty"`
Os string `json:"os,omitempty"`
// Repository ID
Id string `json:"id,omitempty"`
// Parent of the image.
Parent string `json:"parent,omitempty"`
// Repository create time.
Created string `json:"created,omitempty"`
// Duration days of the image.
DurationDays string `json:"duration_days,omitempty"`
// Author of the image.
Author string `json:"author,omitempty"`
// Architecture of the image.
Architecture string `json:"architecture,omitempty"`
// Docker version of the image.
DockerVersion string `json:"docker_version,omitempty"`
// OS of the image.
Os string `json:"os,omitempty"`
}

View File

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

View File

@ -1,7 +1,35 @@
package HarborAPI
/*
* Harbor API
*
* These APIs provide services for manipulating Harbor project.
*
* OpenAPI spec version: 0.3.0
*
* Generated by: https://github.com/swagger-api/swagger-codegen.git
*
* 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 apilib
type Role struct {
RoleId int32 `json:"role_id,omitempty"`
// ID in table.
RoleId int32 `json:"role_id,omitempty"`
// Description of permissions for the role.
RoleCode string `json:"role_code,omitempty"`
// Name the the role.
RoleName string `json:"role_name,omitempty"`
}

View File

@ -0,0 +1,32 @@
/*
* Harbor API
*
* These APIs provide services for manipulating Harbor project.
*
* OpenAPI spec version: 0.3.0
*
* Generated by: https://github.com/swagger-api/swagger-codegen.git
*
* 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 apilib
type RoleParam struct {
// Role ID for updating project role member.
Roles []int32 `json:"roles,omitempty"`
// Username relevant to a project role member.
Username string `json:"username,omitempty"`
}

View File

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

View File

@ -1,8 +1,34 @@
package HarborAPI
/*
* Harbor API
*
* These APIs provide services for manipulating Harbor project.
*
* OpenAPI spec version: 0.3.0
*
* Generated by: https://github.com/swagger-api/swagger-codegen.git
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import ()
package apilib
import()
type Search struct {
Projects []Project4Search `json:"project,omitempty"`
Repositories []Repository4Search `json:"repository,omitempty"`
// Search results of the projects that matched the filter keywords.
Projects []SearchProject `json:"project,omitempty"`
// Search results of the repositories that matched the filter keywords.
Repositories []SearchRepository `json:"repository,omitempty"`
}

View File

@ -0,0 +1,35 @@
/*
* Harbor API
*
* These APIs provide services for manipulating Harbor project.
*
* OpenAPI spec version: 0.3.0
*
* Generated by: https://github.com/swagger-api/swagger-codegen.git
*
* 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 apilib
type SearchProject struct {
// The ID of project
Id int64 `json:"id,omitempty"`
// The name of the project
Name string `json:"name,omitempty"`
// The flag to indicate the publicity of the project (1 is public, 0 is non-public)
Public int32 `json:"public,omitempty"`
}

View File

@ -0,0 +1,38 @@
/*
* Harbor API
*
* These APIs provide services for manipulating Harbor project.
*
* OpenAPI spec version: 0.3.0
*
* Generated by: https://github.com/swagger-api/swagger-codegen.git
*
* 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 apilib
type SearchRepository struct {
// The name of the repository
RepositoryName string `json:"repository_name,omitempty"`
// The name of the project that the repository belongs to
ProjectName string `json:"project_name,omitempty"`
// The ID of the project that the repository belongs to
ProjectId int32 `json:"project_id,omitempty"`
// The flag to indicate the publicity of the project that the repository belongs to (1 is public, 0 is not)
ProjectPublic int32 `json:"project_public,omitempty"`
}

View File

@ -0,0 +1,44 @@
/*
* Harbor API
*
* These APIs provide services for manipulating Harbor project.
*
* OpenAPI spec version: 0.3.0
*
* Generated by: https://github.com/swagger-api/swagger-codegen.git
*
* 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 apilib
type StatisticMap struct {
// The count of the projects which the user is a member of.
MyProjectCount int32 `json:"my_project_count,omitempty"`
// The count of the repositories belonging to the projects which the user is a member of.
MyRepoCount int32 `json:"my_repo_count,omitempty"`
// The count of the public projects.
PublicProjectCount int32 `json:"public_project_count,omitempty"`
// The count of the public repositories belonging to the public projects which the user is a member of.
PublicRepoCount int32 `json:"public_repo_count,omitempty"`
// The count of the total projects, only be seen when the is admin.
TotalProjectCount int32 `json:"total_project_count,omitempty"`
// The count of the total repositories, only be seen when the user is admin.
TotalRepoCount int32 `json:"total_repo_count,omitempty"`
}

View File

@ -0,0 +1,29 @@
/*
* Harbor API
*
* These APIs provide services for manipulating Harbor project.
*
* OpenAPI spec version: 0.3.0
*
* Generated by: https://github.com/swagger-api/swagger-codegen.git
*
* 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 apilib
type Tags struct {
// The repository's used tag.
Tag string `json:"tag,omitempty"`
}

View File

@ -0,0 +1,32 @@
/*
* Harbor API
*
* These APIs provide services for manipulating Harbor project.
*
* OpenAPI spec version: 0.3.0
*
* Generated by: https://github.com/swagger-api/swagger-codegen.git
*
* 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 apilib
type TopRepo struct {
// The name of the repo
RepoName string `json:"repo_name,omitempty"`
// The access count of the repo
AccessCount int32 `json:"access_count,omitempty"`
}

View File

@ -1,11 +1,41 @@
package HarborAPI
/*
* Harbor API
*
* These APIs provide services for manipulating Harbor project.
*
* OpenAPI spec version: 0.3.0
*
* Generated by: https://github.com/swagger-api/swagger-codegen.git
*
* 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 apilib
type User struct {
UserId int32 `json:"user_id,omitempty"`
// The ID of the user.
UserId int32 `json:"user_id,omitempty"`
Username string `json:"username,omitempty"`
Email string `json:"email,omitempty"`
Email string `json:"email,omitempty"`
Password string `json:"password,omitempty"`
Realname string `json:"realname,omitempty"`
Comment string `json:"comment,omitempty"`
Deleted int32 `json:"deleted,omitempty"`
Comment string `json:"comment,omitempty"`
Deleted int32 `json:"deleted,omitempty"`
}

View File

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

View File

@ -1,130 +0,0 @@
package HarborAPItest
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/harbor/tests/apitests/apilib"
)
func TestRepositoryDelete(t *testing.T) {
fmt.Println("Test for Project Delete (ProjectDelete) API")
assert := assert.New(t)
//prepare for test
adminEr := &HarborAPI.UsrInfo{"admin", "Harbor1234"}
admin := &HarborAPI.UsrInfo{"admin", "Harbor12345"}
prjUsr := &HarborAPI.UsrInfo{"unknown", "unknown"}
fmt.Println("Checking repository status...")
apiTest := HarborAPI.NewHarborAPI()
var searchResault HarborAPI.Search
searchResault, err := apiTest.SearchGet("library")
//fmt.Printf("%+v\n", resault)
if err != nil {
t.Error("Error while search project or repository", err.Error())
t.Log(err)
} else {
//assert.Equal(searchResault.Repositories[0].RepoName, "library/docker", "1st repo name should be")
if !assert.Equal(searchResault.Repositories[0].RepoName, "library/docker", "1st repo name should be") {
t.Error("fail to find repo 'library/docker'", err.Error())
t.Log(err)
} else {
fmt.Println("repo 'library/docker' exit")
}
//assert.Equal(searchResault.Repositories[1].RepoName, "library/hello-world", "2nd repo name should be")
if !assert.Equal(searchResault.Repositories[1].RepoName, "library/hello-world", "2nd repo name should be") {
t.Error("fail to find repo 'library/hello-world'", err.Error())
t.Log(err)
} else {
fmt.Println("repo 'library/hello-world' exit")
}
//t.Log(resault)
}
//case 1: admin login fail, expect repo delete fail.
fmt.Println("case 1: admin login fail, expect repo delete fail.")
resault, err := apiTest.HarborLogin(*adminEr)
if err != nil {
t.Error("Error while admin login", err.Error())
t.Log(err)
} else {
assert.Equal(resault, int(401), "Admin login status should be 401")
//t.Log(resault)
}
if resault != 401 {
t.Log(resault)
} else {
prjUsr = adminEr
}
resault, err = apiTest.RepositoriesDelete(*prjUsr, "library/docker", "")
if err != nil {
t.Error("Error while delete repository", err.Error())
t.Log(err)
} else {
assert.Equal(resault, int(401), "Case 1: Repository delete status should be 401")
//t.Log(resault)
}
//case 2: admin successful login, expect repository delete success.
fmt.Println("case 2: admin successful login, expect repository delete success.")
resault, err = apiTest.HarborLogin(*admin)
if err != nil {
t.Error("Error while admin login", err.Error())
t.Log(err)
} else {
assert.Equal(resault, int(200), "Admin login status should be 200")
//t.Log(resault)
}
if resault != 200 {
t.Log(resault)
} else {
prjUsr = admin
}
resault, err = apiTest.RepositoriesDelete(*prjUsr, "library/docker", "")
if err != nil {
t.Error("Error while delete repository", err.Error())
t.Log(err)
} else {
if assert.Equal(resault, int(200), "Case 2: Repository delete status should be 200") {
fmt.Println("Repository 'library/docker' delete success.")
}
//t.Log(resault)
}
resault, err = apiTest.RepositoriesDelete(*prjUsr, "library/hello-world", "")
if err != nil {
t.Error("Error while delete repository", err.Error())
t.Log(err)
} else {
if assert.Equal(resault, int(200), "Case 2: Repository delete status should be 200") {
fmt.Println("Repository 'hello-world' delete success.")
}
//t.Log(resault)
}
//case 3: delete one repo not exit, expect repo delete fail.
fmt.Println("case 3: delete one repo not exit, expect repo delete fail.")
resault, err = apiTest.RepositoriesDelete(*prjUsr, "library/hello-world", "")
if err != nil {
t.Error("Error while delete repository", err.Error())
t.Log(err)
} else {
if assert.Equal(resault, int(404), "Case 3: Repository delete status should be 404") {
fmt.Println("Repository 'hello-world' not exit.")
}
//t.Log(resault)
}
//if resault.Response.StatusCode != 200 {
// t.Log(resault.Response)
//}
}

View File

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

View File

@ -0,0 +1,23 @@
version: '2'
services:
registry:
image: library/registry:2.4.0
restart: always
volumes:
- /data/registry:/storage
- ./config/registry/:/etc/registry/
environment:
- GODEBUG=netdns=cgo
ports:
- 5000:5000
command:
["serve", "/etc/registry/config.yml"]
mysql:
build: ./db/
restart: always
volumes:
- /data/database:/var/lib/mysql
env_file:
- ./config/db/env
ports:
- 3306:3306

View File

@ -1,10 +1,9 @@
docker pull hello-world
docker pull docker
docker login -u admin -p Harbor12345 127.0.0.1
#!/bin/bash
docker tag hello-world 127.0.0.1/library/hello-world
docker push 127.0.0.1/library/hello-world
cp tests/docker-compose.test.yml Deploy/.
docker tag docker 127.0.0.1/library/docker
docker push 127.0.0.1/library/docker
mkdir /etc/ui
cp Deploy/config/ui/private_key.pem /etc/ui/.
mkdir conf
cp Deploy/config/ui/app.conf conf/.

View File

@ -16,9 +16,15 @@
package utils
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha1"
"encoding/base64"
"errors"
"fmt"
"io"
"strings"
"golang.org/x/crypto/pbkdf2"
)
@ -28,13 +34,67 @@ func Encrypt(content string, salt string) string {
return fmt.Sprintf("%x", pbkdf2.Key([]byte(content), []byte(salt), 4096, 16, sha1.New))
}
// ReversibleEncrypt encrypts the str with base64
func ReversibleEncrypt(str string) string {
return base64.StdEncoding.EncodeToString([]byte(str))
const (
// EncryptHeaderV1 ...
EncryptHeaderV1 = "<enc-v1>"
)
// ReversibleEncrypt encrypts the str with aes/base64
func ReversibleEncrypt(str, key string) (string, error) {
keyBytes := []byte(key)
var block cipher.Block
var err error
if block, err = aes.NewCipher(keyBytes); err != nil {
return "", err
}
cipherText := make([]byte, aes.BlockSize+len(str))
iv := cipherText[:aes.BlockSize]
if _, err = io.ReadFull(rand.Reader, iv); err != nil {
return "", err
}
cfb := cipher.NewCFBEncrypter(block, iv)
cfb.XORKeyStream(cipherText[aes.BlockSize:], []byte(str))
encrypted := EncryptHeaderV1 + base64.StdEncoding.EncodeToString(cipherText)
return encrypted, nil
}
// ReversibleDecrypt decrypts the str with base64
func ReversibleDecrypt(str string) (string, error) {
b, err := base64.StdEncoding.DecodeString(str)
return string(b), err
// ReversibleDecrypt decrypts the str with aes/base64 or base 64 depending on "header"
func ReversibleDecrypt(str, key string) (string, error) {
if strings.HasPrefix(str, EncryptHeaderV1) {
str = str[len(EncryptHeaderV1):]
return decryptAES(str, key)
}
//fallback to base64
return decodeB64(str)
}
func decodeB64(str string) (string, error) {
cipherText, err := base64.StdEncoding.DecodeString(str)
return string(cipherText), err
}
func decryptAES(str, key string) (string, error) {
keyBytes := []byte(key)
var block cipher.Block
var cipherText []byte
var err error
if block, err = aes.NewCipher(keyBytes); err != nil {
return "", err
}
if cipherText, err = base64.StdEncoding.DecodeString(str); err != nil {
return "", err
}
if len(cipherText) < aes.BlockSize {
err = errors.New("cipherText too short")
return "", err
}
iv := cipherText[:aes.BlockSize]
cipherText = cipherText[aes.BlockSize:]
cfb := cipher.NewCFBDecrypter(block, iv)
cfb.XORKeyStream(cipherText, cipherText)
return string(cipherText), nil
}

View File

@ -17,6 +17,7 @@ package log
import (
"fmt"
"strings"
)
// Level ...
@ -56,7 +57,7 @@ func (l Level) string() (lvl string) {
func parseLevel(lvl string) (level Level, err error) {
switch lvl {
switch strings.ToLower(lvl) {
case "debug":
level = DebugLevel
case "info":

61
utils/log/level_test.go Normal file
View File

@ -0,0 +1,61 @@
/*
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 log
import (
"testing"
)
func TestString(t *testing.T) {
m := map[Level]string{
DebugLevel: "DEBUG",
InfoLevel: "INFO",
WarningLevel: "WARNING",
ErrorLevel: "ERROR",
FatalLevel: "FATAL",
-1: "UNKNOWN",
}
for level, str := range m {
if level.string() != str {
t.Errorf("unexpected string: %s != %s", level.string(), str)
}
}
}
func TestParseLevel(t *testing.T) {
m := map[string]Level{
"DEBUG": DebugLevel,
"INFO": InfoLevel,
"WARNING": WarningLevel,
"ERROR": ErrorLevel,
"FATAL": FatalLevel,
}
for str, level := range m {
l, err := parseLevel(str)
if err != nil {
t.Errorf("failed to parse level: %v", err)
}
if l != level {
t.Errorf("unexpected level: %d != %d", l, level)
}
}
if _, err := parseLevel("UNKNOWN"); err == nil {
t.Errorf("unexpected behaviour: should be error here")
}
}

View File

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

156
utils/log/logger_test.go Normal file
View File

@ -0,0 +1,156 @@
/*
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 log
import (
"bytes"
"os"
"strings"
"testing"
)
var (
message = "message"
)
func TestSetx(t *testing.T) {
logger := New(nil, nil, WarningLevel)
logger.SetOutput(os.Stdout)
fmt := NewTextFormatter()
logger.SetFormatter(fmt)
logger.SetLevel(DebugLevel)
if logger.out != os.Stdout {
t.Errorf("unexpected outer: %v != %v", logger.out, os.Stdout)
}
if logger.fmtter != fmt {
t.Errorf("unexpected formatter: %v != %v", logger.fmtter, fmt)
}
if logger.lvl != DebugLevel {
t.Errorf("unexpected log level: %v != %v", logger.lvl, DebugLevel)
}
}
func TestDebug(t *testing.T) {
buf := enter()
defer exit()
Debug(message)
str := buf.String()
if len(str) != 0 {
t.Errorf("unexpected message: %s != %s", str, "")
}
}
func TestDebugf(t *testing.T) {
buf := enter()
defer exit()
Debugf("%s", message)
str := buf.String()
if len(str) != 0 {
t.Errorf("unexpected message: %s != %s", str, "")
}
}
func TestInfo(t *testing.T) {
buf := enter()
defer exit()
Info(message)
str := buf.String()
if strings.HasSuffix(str, "[INFO] message") {
t.Errorf("unexpected message: %s != %s", str, "")
}
}
func TestInfof(t *testing.T) {
buf := enter()
defer exit()
Infof("%s", message)
str := buf.String()
if strings.HasSuffix(str, "[INFO] message") {
t.Errorf("unexpected message: %s != %s", str, "")
}
}
func TestWarning(t *testing.T) {
buf := enter()
defer exit()
Warning(message)
str := buf.String()
if strings.HasSuffix(str, "[WARNING] message") {
t.Errorf("unexpected message: %s != %s", str, "")
}
}
func TestWarningf(t *testing.T) {
buf := enter()
defer exit()
Warningf("%s", message)
str := buf.String()
if strings.HasSuffix(str, "[WARNING] message") {
t.Errorf("unexpected message: %s != %s", str, "")
}
}
func TestError(t *testing.T) {
buf := enter()
defer exit()
Error(message)
str := buf.String()
if strings.HasSuffix(str, "[ERROR] message") {
t.Errorf("unexpected message: %s != %s", str, "")
}
}
func TestErrorf(t *testing.T) {
buf := enter()
defer exit()
Errorf("%s", message)
str := buf.String()
if strings.HasSuffix(str, "[ERROR] message") {
t.Errorf("unexpected message: %s != %s", str, "")
}
}
func enter() *bytes.Buffer {
b := make([]byte, 0, 32)
buf := bytes.NewBuffer(b)
logger.SetOutput(buf)
return buf
}
func exit() {
logger.SetOutput(os.Stdout)
}

View File

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

View File

@ -0,0 +1,89 @@
/*
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 auth
import (
"net/http"
"strings"
"testing"
"github.com/docker/distribution/registry/client/auth"
"github.com/vmware/harbor/utils/test"
)
func TestNewAuthorizerStore(t *testing.T) {
handler := test.Handler(&test.Response{
StatusCode: http.StatusUnauthorized,
Headers: map[string]string{
"Www-Authenticate": "Bearer realm=\"https://auth.docker.io/token\",service=\"registry.docker.io\"",
},
})
server := test.NewServer(&test.RequestHandlerMapping{
Method: "GET",
Pattern: "/v2/",
Handler: handler,
})
defer server.Close()
_, err := NewAuthorizerStore(server.URL, false, nil)
if err != nil {
t.Fatalf("failed to create authorizer store: %v", err)
}
}
type simpleAuthorizer struct {
}
func (s *simpleAuthorizer) Scheme() string {
return "bearer"
}
func (s *simpleAuthorizer) Authorize(req *http.Request,
params map[string]string) error {
req.Header.Set("Authorization", "Bearer token")
return nil
}
func TestModify(t *testing.T) {
authorizer := &simpleAuthorizer{}
challenge := auth.Challenge{
Scheme: "bearer",
}
as := &AuthorizerStore{
authorizers: []Authorizer{authorizer},
challenges: []auth.Challenge{challenge},
}
req, err := http.NewRequest("GET", "http://example.com", nil)
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
if err = as.Modify(req); err != nil {
t.Fatalf("failed to modify request: %v", err)
}
header := req.Header.Get("Authorization")
if len(header) == 0 {
t.Fatal("\"Authorization\" header not found")
}
if !strings.HasPrefix(header, "Bearer") {
t.Fatal("\"Authorization\" header does not start with \"Bearer\"")
}
}

View File

@ -0,0 +1,67 @@
/*
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 auth
import (
"net/http"
"testing"
)
func TestAddAuthorizationOfBasicAuthCredential(t *testing.T) {
cred := NewBasicAuthCredential("usr", "pwd")
req, err := http.NewRequest("GET", "http://example.com", nil)
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
cred.AddAuthorization(req)
usr, pwd, ok := req.BasicAuth()
if !ok {
t.Fatal("basic auth not found")
}
if usr != "usr" {
t.Errorf("unexpected username: %s != usr", usr)
}
if pwd != "pwd" {
t.Errorf("unexpected password: %s != pwd", pwd)
}
}
func TestAddAuthorizationOfCookieCredential(t *testing.T) {
cookie := &http.Cookie{
Name: "name",
Value: "value",
}
cred := NewCookieCredential(cookie)
req, err := http.NewRequest("GET", "http://example.com", nil)
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
cred.AddAuthorization(req)
ck, err := req.Cookie("name")
if err != nil {
t.Fatalf("failed to get cookie: %v", err)
}
if ck.Value != "value" {
t.Errorf("unexpected value: %s != value", ck.Value)
}
}

View File

@ -180,7 +180,9 @@ func (s *standardTokenAuthorizer) generateToken(realm, service string, scopes []
return
}
s.credential.AddAuthorization(r)
if s.credential != nil {
s.credential.AddAuthorization(r)
}
resp, err := s.client.Do(r)
if err != nil {

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 auth
import (
"net/http"
"testing"
"github.com/vmware/harbor/utils/test"
)
func TestAuthorizeOfStandardTokenAuthorizer(t *testing.T) {
handler := test.Handler(&test.Response{
Body: []byte(`
{
"token":"token",
"expires_in":300,
"issued_at":"2016-08-17T23:17:58+08:00"
}
`),
})
server := test.NewServer(&test.RequestHandlerMapping{
Method: "GET",
Pattern: "/token",
Handler: handler,
})
defer server.Close()
authorizer := NewStandardTokenAuthorizer(nil, false, "repository", "library/ubuntu", "pull")
req, err := http.NewRequest("GET", "http://registry", nil)
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
params := map[string]string{
"realm": server.URL + "/token",
}
if err := authorizer.Authorize(req, params); err != nil {
t.Fatalf("failed to authorize request: %v", err)
}
tk := req.Header.Get("Authorization")
if tk != "Bearer token" {
t.Errorf("unexpected token: %s != %s", tk, "Bearer token")
}
}
func TestSchemeOfStandardTokenAuthorizer(t *testing.T) {
authorizer := &standardTokenAuthorizer{}
if authorizer.Scheme() != "bearer" {
t.Errorf("unexpected scheme: %s != %s", authorizer.Scheme(), "bearer")
}
}

View File

@ -4,6 +4,14 @@ import (
"testing"
)
func TestMain(t *testing.T) {
}
func TestError(t *testing.T) {
err := &Error{
StatusCode: 404,
Detail: "not found",
}
if err.Error() != "404 not found" {
t.Fatalf("unexpected content: %s != %s",
err.Error(), "404 not found")
}
}

View File

@ -0,0 +1,56 @@
/*
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 registry
import (
"testing"
"github.com/docker/distribution/manifest/schema2"
)
func TestUnMarshal(t *testing.T) {
b := []byte(`{
"schemaVersion":2,
"mediaType":"application/vnd.docker.distribution.manifest.v2+json",
"config":{
"mediaType":"application/vnd.docker.container.image.v1+json",
"size":1473,
"digest":"sha256:c54a2cc56cbb2f04003c1cd4507e118af7c0d340fe7e2720f70976c4b75237dc"
},
"layers":[
{
"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip",
"size":974,
"digest":"sha256:c04b14da8d1441880ed3fe6106fb2cc6fa1c9661846ac0266b8a5ec8edf37b7c"
}
]
}`)
manifest, _, err := UnMarshal(schema2.MediaTypeManifest, b)
if err != nil {
t.Fatalf("failed to parse manifest: %v", err)
}
refs := manifest.References()
if len(refs) != 1 {
t.Fatalf("unexpected length of reference: %d != %d", len(refs), 1)
}
digest := "sha256:c04b14da8d1441880ed3fe6106fb2cc6fa1c9661846ac0266b8a5ec8edf37b7c"
if refs[0].Digest.String() != digest {
t.Errorf("unexpected digest: %s != %s", refs[0].Digest.String(), digest)
}
}

View File

@ -48,11 +48,6 @@ func NewRegistry(endpoint string, client *http.Client) (*Registry, error) {
// NewRegistryWithModifiers returns an instance of Registry according to the modifiers
func NewRegistryWithModifiers(endpoint string, insecure bool, modifiers ...Modifier) (*Registry, error) {
u, err := utils.ParseEndpoint(endpoint)
if err != nil {
return nil, err
}
t := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: insecure,
@ -61,12 +56,9 @@ func NewRegistryWithModifiers(endpoint string, insecure bool, modifiers ...Modif
transport := NewTransport(t, modifiers...)
return &Registry{
Endpoint: u,
client: &http.Client{
Transport: transport,
},
}, nil
return NewRegistry(endpoint, &http.Client{
Transport: transport,
})
}
// Catalog ...

View File

@ -0,0 +1,150 @@
/*
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 registry
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"testing"
"github.com/vmware/harbor/utils/test"
)
func TestNewRegistryWithModifiers(t *testing.T) {
_, err := NewRegistryWithModifiers("http://registry.org", false, nil)
if err != nil {
t.Errorf("fail to crearte client of registry: %v", err)
}
}
func TestPing(t *testing.T) {
server := test.NewServer(
&test.RequestHandlerMapping{
Method: "GET",
Pattern: "/v2/",
Handler: test.Handler(nil),
})
defer server.Close()
client, err := newRegistryClient(server.URL)
if err != nil {
t.Fatalf("failed to create client for registry: %v", err)
}
if err = client.Ping(); err != nil {
t.Errorf("failed to ping registry: %v", err)
}
}
func TestCatalog(t *testing.T) {
repositories := make([]string, 0, 1001)
for i := 0; i < 1001; i++ {
repositories = append(repositories, strconv.Itoa(i))
}
handler := func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
last := q.Get("last")
n, err := strconv.Atoi(q.Get("n"))
if err != nil || n <= 0 {
n = 1000
}
length := len(repositories)
begin := length
if len(last) == 0 {
begin = 0
} else {
for i, repository := range repositories {
if repository == last {
begin = i + 1
break
}
}
}
end := begin + n
if end > length {
end = length
}
w.Header().Set(http.CanonicalHeaderKey("Content-Type"), "application/json")
if end < length {
u, err := url.Parse("/v2/_catalog")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
values := u.Query()
values.Add("last", repositories[end-1])
values.Add("n", strconv.Itoa(n))
u.RawQuery = values.Encode()
link := fmt.Sprintf("<%s>; rel=\"next\"", u.String())
w.Header().Set(http.CanonicalHeaderKey("link"), link)
}
repos := struct {
Repositories []string `json:"repositories"`
}{
Repositories: []string{},
}
if begin < length {
repos.Repositories = repositories[begin:end]
}
b, err := json.Marshal(repos)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Write(b)
}
server := test.NewServer(
&test.RequestHandlerMapping{
Method: "GET",
Pattern: "/v2/_catalog",
Handler: handler,
})
defer server.Close()
client, err := newRegistryClient(server.URL)
if err != nil {
t.Fatalf("failed to create client for registry: %v", err)
}
repos, err := client.Catalog()
if err != nil {
t.Fatalf("failed to catalog repositories: %v", err)
}
if len(repos) != len(repositories) {
t.Errorf("unexpected length of repositories: %d != %d", len(repos), len(repositories))
}
}
func newRegistryClient(url string) (*Registry, error) {
return NewRegistry(url, &http.Client{})
}

View File

@ -61,13 +61,6 @@ func NewRepository(name, endpoint string, client *http.Client) (*Repository, err
// NewRepositoryWithModifiers returns an instance of Repository according to the modifiers
func NewRepositoryWithModifiers(name, endpoint string, insecure bool, modifiers ...Modifier) (*Repository, error) {
name = strings.TrimSpace(name)
u, err := utils.ParseEndpoint(endpoint)
if err != nil {
return nil, err
}
t := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: insecure,
@ -76,13 +69,9 @@ func NewRepositoryWithModifiers(name, endpoint string, insecure bool, modifiers
transport := NewTransport(t, modifiers...)
return &Repository{
Name: name,
Endpoint: u,
client: &http.Client{
Transport: transport,
},
}, nil
return NewRepository(name, endpoint, &http.Client{
Transport: transport,
})
}
func parseError(err error) error {
@ -347,7 +336,7 @@ func (r *Repository) PullBlob(digest string) (size int64, data io.ReadCloser, er
data = resp.Body
return
}
// can not close the connect if the status code is 200
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
@ -428,7 +417,6 @@ func (r *Repository) PushBlob(digest string, size int64, data io.Reader) error {
if err != nil {
return err
}
return r.monolithicBlobUpload(location, digest, size, data)
}
@ -482,5 +470,12 @@ func buildInitiateBlobUploadURL(endpoint, repoName string) string {
}
func buildMonolithicBlobUploadURL(location, digest string) string {
return fmt.Sprintf("%s&digest=%s", location, digest)
query := ""
if strings.ContainsRune(location, '?') {
query = "&"
} else {
query = "?"
}
query += fmt.Sprintf("digest=%s", digest)
return fmt.Sprintf("%s%s", location, query)
}

View File

@ -16,179 +16,395 @@
package registry
import (
"encoding/json"
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"net/url"
"strconv"
"strings"
"testing"
"time"
"github.com/vmware/harbor/utils/registry/auth"
"github.com/docker/distribution/manifest/schema2"
registry_error "github.com/vmware/harbor/utils/registry/error"
"github.com/vmware/harbor/utils/test"
)
var (
username = "user"
password = "P@ssw0rd"
repo = "samalba/my-app"
tags = tagResp{Tags: []string{"1.0", "2.0", "3.0"}}
validToken = "valid_token"
invalidToken = "invalid_token"
credential auth.Credential
registryServer *httptest.Server
tokenServer *httptest.Server
repositoryClient *Repository
repository = "library/hello-world"
tag = "latest"
mediaType = schema2.MediaTypeManifest
manifest = []byte("manifest")
blob = []byte("blob")
uuid = "0663ff44-63bb-11e6-8b77-86f30ca893d3"
digest = "sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b"
)
type tagResp struct {
Tags []string `json:"tags"`
}
func TestMain(m *testing.M) {
//log.SetLevel(log.DebugLevel)
credential = auth.NewBasicAuthCredential(username, password)
tokenServer = initTokenServer()
defer tokenServer.Close()
registryServer = initRegistryServer()
defer registryServer.Close()
os.Exit(m.Run())
}
func initRegistryServer() *httptest.Server {
mux := http.NewServeMux()
mux.HandleFunc("/v2/", servePing)
mux.HandleFunc(fmt.Sprintf("/v2/%s/tags/list", repo), serveTaglisting)
return httptest.NewServer(mux)
}
//response ping request: http://registry/v2
func servePing(w http.ResponseWriter, r *http.Request) {
if !isTokenValid(r) {
challenge(w)
return
func TestNewRepositoryWithModifiers(t *testing.T) {
_, err := NewRepositoryWithModifiers("library/ubuntu",
"http://registry.org", true, nil)
if err != nil {
t.Fatalf("failed to create client for repository: %v", err)
}
}
func serveTaglisting(w http.ResponseWriter, r *http.Request) {
if !isTokenValid(r) {
challenge(w)
return
}
if err := json.NewEncoder(w).Encode(tags); err != nil {
w.Write([]byte(err.Error()))
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func isTokenValid(r *http.Request) bool {
valid := false
auth := r.Header.Get(http.CanonicalHeaderKey("Authorization"))
if len(auth) != 0 {
auth = strings.TrimSpace(auth)
index := strings.Index(auth, "Bearer")
token := auth[index+6:]
token = strings.TrimSpace(token)
if token == validToken {
valid = true
func TestBlobExist(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
dgt := path[strings.LastIndex(path, "/")+1 : len(path)]
if dgt == digest {
w.Header().Add(http.CanonicalHeaderKey("Content-Length"), strconv.Itoa(len(blob)))
w.Header().Add(http.CanonicalHeaderKey("Docker-Content-Digest"), digest)
w.Header().Add(http.CanonicalHeaderKey("Content-Type"), "application/octet-stream")
return
}
}
return valid
}
func challenge(w http.ResponseWriter) {
challenge := "Bearer realm=\"" + tokenServer.URL + "/service/token\",service=\"token-service\""
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
return
}
func initTokenServer() *httptest.Server {
mux := http.NewServeMux()
mux.HandleFunc("/service/token", serveToken)
return httptest.NewServer(mux)
}
func serveToken(w http.ResponseWriter, r *http.Request) {
u, p, ok := r.BasicAuth()
if !ok || u != username || p != password {
w.WriteHeader(http.StatusUnauthorized)
return
w.WriteHeader(http.StatusNotFound)
}
result := make(map[string]interface{})
result["token"] = validToken
result["expires_in"] = 300
result["issued_at"] = time.Now().Format(time.RFC3339)
server := test.NewServer(
&test.RequestHandlerMapping{
Method: "HEAD",
Pattern: fmt.Sprintf("/v2/%s/blobs/", repository),
Handler: handler,
})
defer server.Close()
encoder := json.NewEncoder(w)
if err := encoder.Encode(result); err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
client, err := newRepository(server.URL)
if err != nil {
err = parseError(err)
t.Fatalf("failed to create client for repository: %v", err)
}
exist, err := client.BlobExist(digest)
if err != nil {
t.Fatalf("failed to check the existence of blob: %v", err)
}
if !exist {
t.Errorf("blob should exist on registry, but it does not exist")
}
exist, err = client.BlobExist("invalid_digest")
if err != nil {
t.Fatalf("failed to check the existence of blob: %v", err)
}
if exist {
t.Errorf("blob should not exist on registry, but it exists")
}
}
func TestPullBlob(t *testing.T) {
handler := test.Handler(&test.Response{
Headers: map[string]string{
"Content-Length": strconv.Itoa(len(blob)),
"Docker-Content-Digest": digest,
"Content-Type": "application/octet-stream",
},
Body: blob,
})
server := test.NewServer(
&test.RequestHandlerMapping{
Method: "GET",
Pattern: fmt.Sprintf("/v2/%s/blobs/%s", repository, digest),
Handler: handler,
})
defer server.Close()
client, err := newRepository(server.URL)
if err != nil {
t.Fatalf("failed to create client for repository: %v", err)
}
size, reader, err := client.PullBlob(digest)
if err != nil {
t.Fatalf("failed to pull blob: %v", err)
}
if size != int64(len(blob)) {
t.Errorf("unexpected size of blob: %d != %d", size, len(blob))
}
b, err := ioutil.ReadAll(reader)
if err != nil {
t.Fatalf("failed to read from reader: %v", err)
}
if bytes.Compare(b, blob) != 0 {
t.Errorf("unexpected blob: %s != %s", string(b), string(blob))
}
}
func TestPushBlob(t *testing.T) {
location := ""
initUploadHandler := func(w http.ResponseWriter, r *http.Request) {
w.Header().Add(http.CanonicalHeaderKey("Content-Length"), "0")
w.Header().Add(http.CanonicalHeaderKey("Location"), location)
w.Header().Add(http.CanonicalHeaderKey("Range"), "0-0")
w.Header().Add(http.CanonicalHeaderKey("Docker-Upload-UUID"), uuid)
w.WriteHeader(http.StatusAccepted)
}
monolithicUploadHandler := test.Handler(&test.Response{
StatusCode: http.StatusCreated,
Headers: map[string]string{
"Content-Length": "0",
"Location": fmt.Sprintf("/v2/%s/blobs/%s", repository, digest),
"Docker-Content-Digest": digest,
},
})
server := test.NewServer(
&test.RequestHandlerMapping{
Method: "POST",
Pattern: fmt.Sprintf("/v2/%s/blobs/uploads/", repository),
Handler: initUploadHandler,
},
&test.RequestHandlerMapping{
Method: "PUT",
Pattern: fmt.Sprintf("/v2/%s/blobs/uploads/%s", repository, uuid),
Handler: monolithicUploadHandler,
})
defer server.Close()
location = fmt.Sprintf("%s/v2/%s/blobs/uploads/%s", server.URL, repository, uuid)
client, err := newRepository(server.URL)
if err != nil {
t.Fatalf("failed to create client for repository: %v", err)
}
if err = client.PushBlob(digest, int64(len(blob)), bytes.NewReader(blob)); err != nil {
t.Fatalf("failed to push blob: %v", err)
}
}
func TestDeleteBlob(t *testing.T) {
handler := test.Handler(&test.Response{
StatusCode: http.StatusAccepted,
})
server := test.NewServer(
&test.RequestHandlerMapping{
Method: "DELETE",
Pattern: fmt.Sprintf("/v2/%s/blobs/%s", repository, digest),
Handler: handler,
})
defer server.Close()
client, err := newRepository(server.URL)
if err != nil {
t.Fatalf("failed to create client for repository: %v", err)
}
if err = client.DeleteBlob(digest); err != nil {
t.Fatalf("failed to delete blob: %v", err)
}
}
func TestManifestExist(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
tg := path[strings.LastIndex(path, "/")+1 : len(path)]
if tg == tag {
w.Header().Add(http.CanonicalHeaderKey("Docker-Content-Digest"), digest)
w.Header().Add(http.CanonicalHeaderKey("Content-Type"), mediaType)
return
}
w.WriteHeader(http.StatusNotFound)
}
server := test.NewServer(
&test.RequestHandlerMapping{
Method: "HEAD",
Pattern: fmt.Sprintf("/v2/%s/manifests/%s", repository, tag),
Handler: handler,
})
defer server.Close()
client, err := newRepository(server.URL)
if err != nil {
t.Fatalf("failed to create client for repository: %v", err)
}
d, exist, err := client.ManifestExist(tag)
if err != nil {
t.Fatalf("failed to check the existence of manifest: %v", err)
}
if !exist || d != digest {
t.Errorf("manifest should exist on registry, but it does not exist")
}
_, exist, err = client.ManifestExist("invalid_tag")
if err != nil {
t.Fatalf("failed to check the existence of manifest: %v", err)
}
if exist {
t.Errorf("manifest should not exist on registry, but it exists")
}
}
func TestPullManifest(t *testing.T) {
handler := test.Handler(&test.Response{
Headers: map[string]string{
"Docker-Content-Digest": digest,
"Content-Type": mediaType,
},
Body: manifest,
})
server := test.NewServer(
&test.RequestHandlerMapping{
Method: "GET",
Pattern: fmt.Sprintf("/v2/%s/manifests/%s", repository, tag),
Handler: handler,
})
defer server.Close()
client, err := newRepository(server.URL)
if err != nil {
t.Fatalf("failed to create client for repository: %v", err)
}
d, md, payload, err := client.PullManifest(tag, []string{mediaType})
if err != nil {
t.Fatalf("failed to pull manifest: %v", err)
}
if d != digest {
t.Errorf("unexpected digest of manifest: %s != %s", d, digest)
}
if md != mediaType {
t.Errorf("unexpected media type of manifest: %s != %s", md, mediaType)
}
if bytes.Compare(payload, manifest) != 0 {
t.Errorf("unexpected manifest: %s != %s", string(payload), string(manifest))
}
}
func TestPushManifest(t *testing.T) {
handler := test.Handler(&test.Response{
StatusCode: http.StatusCreated,
Headers: map[string]string{
"Content-Length": "0",
"Docker-Content-Digest": digest,
"Location": "",
},
})
server := test.NewServer(
&test.RequestHandlerMapping{
Method: "PUT",
Pattern: fmt.Sprintf("/v2/%s/manifests/%s", repository, tag),
Handler: handler,
})
defer server.Close()
client, err := newRepository(server.URL)
if err != nil {
t.Fatalf("failed to create client for repository: %v", err)
}
d, err := client.PushManifest(tag, mediaType, manifest)
if err != nil {
t.Fatalf("failed to pull manifest: %v", err)
}
if d != digest {
t.Errorf("unexpected digest of manifest: %s != %s", d, digest)
}
}
func TestDeleteTag(t *testing.T) {
manifestExistHandler := test.Handler(&test.Response{
Headers: map[string]string{
"Docker-Content-Digest": digest,
"Content-Type": mediaType,
},
})
deleteManifestandler := test.Handler(&test.Response{
StatusCode: http.StatusAccepted,
})
server := test.NewServer(
&test.RequestHandlerMapping{
Method: "HEAD",
Pattern: fmt.Sprintf("/v2/%s/manifests/", repository),
Handler: manifestExistHandler,
},
&test.RequestHandlerMapping{
Method: "DELETE",
Pattern: fmt.Sprintf("/v2/%s/manifests/%s", repository, digest),
Handler: deleteManifestandler,
})
defer server.Close()
client, err := newRepository(server.URL)
if err != nil {
t.Fatalf("failed to create client for repository: %v", err)
}
if err = client.DeleteTag(tag); err != nil {
t.Fatalf("failed to delete tag: %v", err)
}
}
func TestListTag(t *testing.T) {
client, err := newRepositoryClient(registryServer.URL, true, credential,
repo, "repository", repo, "pull", "push", "*")
handler := test.Handler(&test.Response{
Headers: map[string]string{
"Content-Type": "application/json",
},
Body: []byte(fmt.Sprintf("{\"name\": \"%s\",\"tags\": [\"%s\"]}", repository, tag)),
})
server := test.NewServer(
&test.RequestHandlerMapping{
Method: "GET",
Pattern: fmt.Sprintf("/v2/%s/tags/list", repository),
Handler: handler,
})
defer server.Close()
client, err := newRepository(server.URL)
if err != nil {
t.Error(err)
t.Fatalf("failed to create client for repository: %v", err)
}
list, err := client.ListTag()
tags, err := client.ListTag()
if err != nil {
t.Error(err)
return
}
if len(list) != len(tags.Tags) {
t.Errorf("expected length: %d, actual length: %d", len(tags.Tags), len(list))
return
t.Fatalf("failed to list tags: %v", err)
}
}
func TestListTagWithInvalidCredential(t *testing.T) {
credential := auth.NewBasicAuthCredential(username, "wrong_password")
client, err := newRepositoryClient(registryServer.URL, true, credential,
repo, "repository", repo, "pull", "push", "*")
if err != nil {
t.Error(err)
if len(tags) != 1 {
t.Fatalf("unexpected length of tags: %d != %d", len(tags), 1)
}
if _, err = client.ListTag(); err != nil {
e, ok := err.(*registry_error.Error)
if ok && e.StatusCode == http.StatusUnauthorized {
return
}
t.Error(err)
return
if tags[0] != tag {
t.Errorf("unexpected tag: %s != %s", tags[0], tag)
}
}
func newRepositoryClient(endpoint string, insecure bool, credential auth.Credential, repository, scopeType, scopeName string,
scopeActions ...string) (*Repository, error) {
authorizer := auth.NewStandardTokenAuthorizer(credential, insecure, scopeType, scopeName, scopeActions...)
store, err := auth.NewAuthorizerStore(endpoint, true, authorizer)
if err != nil {
return nil, err
func TestParseError(t *testing.T) {
err := &url.Error{
Err: &registry_error.Error{},
}
client, err := NewRepositoryWithModifiers(repository, endpoint, insecure, store)
if err != nil {
return nil, err
e := parseError(err)
if _, ok := e.(*registry_error.Error); !ok {
t.Errorf("error type does not match registry error")
}
return client, nil
}
func newRepository(endpoint string) (*Repository, error) {
return NewRepository(repository, endpoint, &http.Client{})
}

View File

@ -0,0 +1,60 @@
/*
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 registry
import (
"fmt"
"net/http"
"testing"
"github.com/vmware/harbor/utils/test"
)
type simpleModifier struct {
}
func (s *simpleModifier) Modify(req *http.Request) error {
req.Header.Set("Authorization", "token")
return nil
}
func TestRoundTrip(t *testing.T) {
server := test.NewServer(
&test.RequestHandlerMapping{
Method: "GET",
Pattern: "/",
Handler: test.Handler(nil),
})
transport := NewTransport(&http.Transport{}, &simpleModifier{})
client := &http.Client{
Transport: transport,
}
req, err := http.NewRequest("GET", fmt.Sprintf("%s/", server.URL), nil)
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
if _, err := client.Do(req); err != nil {
t.Fatalf("failed to send request: %s", err)
}
header := req.Header.Get("Authorization")
if header != "token" {
t.Errorf("unexpected header: %s != %s", header, "token")
}
}

88
utils/test/test.go Normal file
View File

@ -0,0 +1,88 @@
/*
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 test
import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"strings"
)
// RequestHandlerMapping is a mapping between request and its handler
type RequestHandlerMapping struct {
// Method is the method the request used
Method string
// Pattern is the pattern the request must match
Pattern string
// Handler is the handler which handles the request
Handler func(http.ResponseWriter, *http.Request)
}
// ServeHTTP ...
func (rhm *RequestHandlerMapping) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if len(rhm.Method) != 0 && r.Method != strings.ToUpper(rhm.Method) {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
rhm.Handler(w, r)
}
// Response is a response used for unit test
type Response struct {
// StatusCode is the status code of the response
StatusCode int
// Headers are the headers of the response
Headers map[string]string
// Boby is the body of the response
Body []byte
}
// Handler returns a handler function which handle requst according to
// the response provided
func Handler(resp *Response) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if resp == nil {
return
}
for k, v := range resp.Headers {
w.Header().Add(http.CanonicalHeaderKey(k), v)
}
if resp.StatusCode == 0 {
resp.StatusCode = http.StatusOK
}
w.WriteHeader(resp.StatusCode)
if len(resp.Body) != 0 {
io.Copy(w, bytes.NewReader(resp.Body))
}
}
}
// NewServer creates a HTTP server for unit test
func NewServer(mappings ...*RequestHandlerMapping) *httptest.Server {
mux := http.NewServeMux()
for _, mapping := range mappings {
mux.Handle(mapping.Pattern, mapping)
}
return httptest.NewServer(mux)
}

View File

@ -20,17 +20,6 @@ import (
"strings"
)
// Repository holds information about repository
type Repository struct {
Name string
}
// GetProject parses the repository and return the name of project.
func (r *Repository) GetProject() string {
project, _ := ParseRepository(r.Name)
return project
}
// FormatEndpoint formats endpoint
func FormatEndpoint(endpoint string) string {
endpoint = strings.TrimSpace(endpoint)

View File

@ -16,10 +16,41 @@
package utils
import (
"encoding/base64"
"strings"
"testing"
)
func TestMain(t *testing.T) {
func TestParseEndpoint(t *testing.T) {
endpoint := "example.com"
u, err := ParseEndpoint(endpoint)
if err != nil {
t.Fatalf("failed to parse endpoint %s: %v", endpoint, err)
}
if u.String() != "http://example.com" {
t.Errorf("unexpected endpoint: %s != %s", endpoint, "http://example.com")
}
endpoint = "https://example.com"
u, err = ParseEndpoint(endpoint)
if err != nil {
t.Fatalf("failed to parse endpoint %s: %v", endpoint, err)
}
if u.String() != "https://example.com" {
t.Errorf("unexpected endpoint: %s != %s", endpoint, "https://example.com")
}
endpoint = " example.com/ "
u, err = ParseEndpoint(endpoint)
if err != nil {
t.Fatalf("failed to parse endpoint %s: %v", endpoint, err)
}
if u.String() != "http://example.com" {
t.Errorf("unexpected endpoint: %s != %s", endpoint, "http://example.com")
}
}
func TestParseRepository(t *testing.T) {
@ -61,3 +92,45 @@ func TestParseRepository(t *testing.T) {
t.Errorf("unexpected rest: [%s] != [%s]", rest, "")
}
}
func TestEncrypt(t *testing.T) {
content := "content"
salt := "salt"
result := Encrypt(content, salt)
if result != "dc79e76c88415c97eb089d9cc80b4ab0" {
t.Errorf("unexpected result: %s != %s", result, "dc79e76c88415c97eb089d9cc80b4ab0")
}
}
func TestReversibleEncrypt(t *testing.T) {
password := "password"
key := "1234567890123456"
encrypted, err := ReversibleEncrypt(password, key)
if err != nil {
t.Errorf("Failed to encrypt: %v", err)
}
t.Logf("Encrypted password: %s", encrypted)
if encrypted == password {
t.Errorf("Encrypted password is identical to the original")
}
if !strings.HasPrefix(encrypted, EncryptHeaderV1) {
t.Errorf("Encrypted password does not have v1 header")
}
decrypted, err := ReversibleDecrypt(encrypted, key)
if err != nil {
t.Errorf("Failed to decrypt: %v", err)
}
if decrypted != password {
t.Errorf("decrypted password: %s, is not identical to original", decrypted)
}
//Test b64 for backward compatibility
b64password := base64.StdEncoding.EncodeToString([]byte(password))
decrypted, err = ReversibleDecrypt(b64password, key)
if err != nil {
t.Errorf("Failed to decrypt: %v", err)
}
if decrypted != password {
t.Errorf("decrypted password: %s, is not identical to original", decrypted)
}
}

View File

@ -191,4 +191,7 @@
<script src="/static/resources/js/components/inline-help/inline-help.directive.js"></script>
<script src="/static/resources/js/components/dismissable-alerts/dismissable-alerts.module.js"></script>
<script src="/static/resources/js/components/dismissable-alerts/dismissable-alerts.directive.js"></script>
<script src="/static/resources/js/components/dismissable-alerts/dismissable-alerts.directive.js"></script>
<script src="/static/resources/js/components/paginator/paginator.module.js"></script>
<script src="/static/resources/js/components/paginator/paginator.directive.js"></script>