From 318d10186e89bea6964737f212028f31d496a78d Mon Sep 17 00:00:00 2001 From: Tan Jiang Date: Wed, 3 Aug 2016 17:25:24 +0800 Subject: [PATCH 01/21] Use AES to encrypt password for target --- .travis.yml | 2 ++ Deploy/db/registry.sql | 2 +- Deploy/harbor.cfg | 4 +++ Deploy/prepare | 9 ++++++ Deploy/templates/jobservice/env | 1 + Deploy/templates/ui/env | 1 + api/target.go | 23 +++++++++++--- job/config/config.go | 11 +++++++ job/statemachine.go | 2 +- utils/encrypt.go | 54 ++++++++++++++++++++++++++++----- utils/utils_test.go | 20 ++++++++++++ 11 files changed, 115 insertions(+), 14 deletions(-) diff --git a/.travis.yml b/.travis.yml index 35c01aff1..3b3b77435 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,6 +28,8 @@ env: HARBOR_ADMIN: admin HARBOR_ADMIN_PASSWD: Harbor12345 UI_SECRET: tempString + MAX_JOB_WORKERS: 3 + SECRET_KEY: 1234567890123456 before_install: - sudo ./tests/hostcfg.sh diff --git a/Deploy/db/registry.sql b/Deploy/db/registry.sql index e5f13f7d4..121e27aae 100644 --- a/Deploy/db/registry.sql +++ b/Deploy/db/registry.sql @@ -122,7 +122,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, diff --git a/Deploy/harbor.cfg b/Deploy/harbor.cfg index bf8a721e7..186c711fe 100644 --- a/Deploy/harbor.cfg +++ b/Deploy/harbor.cfg @@ -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 + #Determine whether the job service should verify the ssl cert when it connects to a remote registry. #Set this flag to off when the remote registry uses a self-signed or untrusted certificate. verify_remote_cert = on diff --git a/Deploy/prepare b/Deploy/prepare index cdea56550..16fadf374 100755 --- a/Deploy/prepare +++ b/Deploy/prepare @@ -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") @@ -49,6 +55,7 @@ crt_commonname = rcp.get("configuration", "crt_commonname") crt_email = rcp.get("configuration", "crt_email") max_job_workers = rcp.get("configuration", "max_job_workers") 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)) @@ -101,6 +108,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) render(os.path.join(templates_dir, "ui", "app.conf"), @@ -126,6 +134,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) diff --git a/Deploy/templates/jobservice/env b/Deploy/templates/jobservice/env index 5359e8512..7cefb7fa4 100644 --- a/Deploy/templates/jobservice/env +++ b/Deploy/templates/jobservice/env @@ -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 diff --git a/Deploy/templates/ui/env b/Deploy/templates/ui/env index 972d94df7..dde28fc5e 100644 --- a/Deploy/templates/ui/env +++ b/Deploy/templates/ui/env @@ -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 diff --git a/api/target.go b/api/target.go index 1c78b915d..c3cb205ea 100644 --- a/api/target.go +++ b/api/target.go @@ -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 { diff --git a/job/config/config.go b/job/config/config.go index b5477e2f3..1403c30b4 100644 --- a/job/config/config.go +++ b/job/config/config.go @@ -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" diff --git a/job/statemachine.go b/job/statemachine.go index 95b242740..c4f245c7c 100644 --- a/job/statemachine.go +++ b/job/statemachine.go @@ -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) } diff --git a/utils/encrypt.go b/utils/encrypt.go index e5ce44792..f326353e0 100644 --- a/utils/encrypt.go +++ b/utils/encrypt.go @@ -16,9 +16,14 @@ package utils import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" "crypto/sha1" "encoding/base64" + "errors" "fmt" + "io" "golang.org/x/crypto/pbkdf2" ) @@ -28,13 +33,48 @@ 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)) +// 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 := 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 +func ReversibleDecrypt(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 } diff --git a/utils/utils_test.go b/utils/utils_test.go index f2b23c1da..4187c872a 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -61,3 +61,23 @@ func TestParseRepository(t *testing.T) { t.Errorf("unexpected rest: [%s] != [%s]", rest, "") } } + +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") + } + 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) + } +} From f2ea6038551fcc3ac928a2a2f20e79bf029e801e Mon Sep 17 00:00:00 2001 From: yhua Date: Thu, 4 Aug 2016 12:09:40 +0800 Subject: [PATCH 02/21] add pkg for support coveralls --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 35c01aff1..0d5271f73 100644 --- a/.travis.yml +++ b/.travis.yml @@ -63,6 +63,8 @@ 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 @@ -72,6 +74,7 @@ script: - go list ./... | grep -v -E 'vendor|tests' | xargs -L1 fgt golint - go list ./... | grep -v -E 'vendor|tests' | xargs -L1 go vet - ./Deploy/coverage4gotest.sh + - goveralls -coverprofile=profile.cov -service=travis-ci - docker-compose -f Deploy/docker-compose.yml up -d From c4081f9aa34e7a38b8a9b33aed7e284a4f4dca9e Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Mon, 1 Aug 2016 16:38:36 +0800 Subject: [PATCH 03/21] add security check on job service update --- api/jobs/replication.go | 21 +++++++++++++++++++++ api/replication_job.go | 9 ++++++++- api/utils.go | 30 ++++++++++++++++++++++++++++-- 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/api/jobs/replication.go b/api/jobs/replication.go index 2cf6b05db..98b8d6320 100644 --- a/api/jobs/replication.go +++ b/api/jobs/replication.go @@ -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 diff --git a/api/replication_job.go b/api/replication_job.go index ae3d080a3..3853b7bda 100644 --- a/api/replication_job.go +++ b/api/replication_job.go @@ -150,7 +150,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)) diff --git a/api/utils.go b/api/utils.go index 0eabc5793..4fa661932 100644 --- a/api/utils.go +++ b/api/utils.go @@ -115,7 +115,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 +195,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 +223,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) From 2074af56b377c47444351827b6c85df5a7c611c0 Mon Sep 17 00:00:00 2001 From: Penghao Cen Date: Fri, 5 Aug 2016 13:10:48 +0800 Subject: [PATCH 04/21] add comments to fix sed compatibility issue between BSD and GNU versions --- Deploy/jsminify.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Deploy/jsminify.sh b/Deploy/jsminify.sh index f89bdfee1..fde2e8274 100755 --- a/Deploy/jsminify.sh +++ b/Deploy/jsminify.sh @@ -20,6 +20,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 From fde17725d59f2cf06ce1c738cb1134e2d3f66239 Mon Sep 17 00:00:00 2001 From: Tan Jiang Date: Fri, 5 Aug 2016 13:22:24 +0800 Subject: [PATCH 05/21] Update decrypt for backward compatibility --- utils/encrypt.go | 24 ++++++++++++++++++++++-- utils/utils_test.go | 14 ++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/utils/encrypt.go b/utils/encrypt.go index f326353e0..2cd996283 100644 --- a/utils/encrypt.go +++ b/utils/encrypt.go @@ -24,6 +24,7 @@ import ( "errors" "fmt" "io" + "strings" "golang.org/x/crypto/pbkdf2" ) @@ -33,6 +34,11 @@ func Encrypt(content string, salt string) string { return fmt.Sprintf("%x", pbkdf2.Key([]byte(content), []byte(salt), 4096, 16, sha1.New)) } +const ( + // EncryptHeaderV1 ... + EncryptHeaderV1 = "" +) + // ReversibleEncrypt encrypts the str with aes/base64 func ReversibleEncrypt(str, key string) (string, error) { keyBytes := []byte(key) @@ -50,12 +56,26 @@ func ReversibleEncrypt(str, key string) (string, error) { cfb := cipher.NewCFBEncrypter(block, iv) cfb.XORKeyStream(cipherText[aes.BlockSize:], []byte(str)) - encrypted := base64.StdEncoding.EncodeToString(cipherText) + encrypted := EncryptHeaderV1 + base64.StdEncoding.EncodeToString(cipherText) return encrypted, nil } -// ReversibleDecrypt decrypts the str with aes/base64 +// 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 diff --git a/utils/utils_test.go b/utils/utils_test.go index 4187c872a..d2ab63112 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -16,6 +16,8 @@ package utils import ( + "encoding/base64" + "strings" "testing" ) @@ -73,6 +75,9 @@ func TestReversibleEncrypt(t *testing.T) { 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) @@ -80,4 +85,13 @@ func TestReversibleEncrypt(t *testing.T) { 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) + } } From 64be247565df6a9ede8e0d5ae0f29bb856cbb458 Mon Sep 17 00:00:00 2001 From: Tan Jiang Date: Fri, 5 Aug 2016 16:23:38 +0800 Subject: [PATCH 06/21] improve test coverage for dao --- dao/dao_test.go | 70 ++++++++++++++++++++++++++++++++++++++++++++++++- dao/user.go | 21 +++------------ 2 files changed, 73 insertions(+), 18 deletions(-) diff --git a/dao/dao_test.go b/dao/dao_test.go index 5dccc51ea..2a8f5e47a 100644 --- a/dao/dao_test.go +++ b/dao/dao_test.go @@ -112,6 +112,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 +158,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 +185,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 @@ -672,6 +708,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 { @@ -688,6 +739,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 { diff --git a/dao/user.go b/dao/user.go index 673f684b0..3022b55a8 100644 --- a/dao/user.go +++ b/dao/user.go @@ -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 From f44e6f5991dc125152eefc9387564ef8802f94c7 Mon Sep 17 00:00:00 2001 From: kunw Date: Fri, 5 Aug 2016 20:37:58 +0800 Subject: [PATCH 07/21] add paginator, add pagination to the logs. --- .../js/components/log/list-log.directive.html | 1 + .../js/components/log/list-log.directive.js | 30 ++- .../paginator/paginator.directive.html | 14 ++ .../paginator/paginator.directive.js | 183 ++++++++++++++++++ .../components/paginator/paginator.module.js | 8 + static/resources/js/harbor.module.js | 3 +- .../js/services/log/services.list-log.js | 4 +- views/sections/script-include.htm | 5 +- 8 files changed, 238 insertions(+), 10 deletions(-) create mode 100644 static/resources/js/components/paginator/paginator.directive.html create mode 100644 static/resources/js/components/paginator/paginator.directive.js create mode 100644 static/resources/js/components/paginator/paginator.module.js diff --git a/static/resources/js/components/log/list-log.directive.html b/static/resources/js/components/log/list-log.directive.html index 4fc3ba5a6..d0804e8b7 100644 --- a/static/resources/js/components/log/list-log.directive.html +++ b/static/resources/js/components/log/list-log.directive.html @@ -47,6 +47,7 @@ + diff --git a/static/resources/js/components/log/list-log.directive.js b/static/resources/js/components/log/list-log.directive.js index 06c99c6ba..a345cdb93 100644 --- a/static/resources/js/components/log/list-log.directive.js +++ b/static/resources/js/components/log/list-log.directive.js @@ -51,8 +51,23 @@ 'projectId': vm.projectId, 'username' : vm.username }; - - retrieve(vm.queryParams); + + vm.page = 1; + vm.pageSize = 1; + + $scope.$watch('vm.totalCount', function(current) { + if(current) { + vm.totalCount = current; + } + }); + $scope.$watch('vm.page', function(current) { + if(current) { + vm.page = current; + retrieve(vm.queryParams, vm.page, vm.pageSize); + } + }); + + retrieve(vm.queryParams, vm.page, vm.pageSize); $scope.$on('$locationChangeSuccess', function() { @@ -69,7 +84,7 @@ 'username' : vm.username }; vm.username = ''; - retrieve(vm.queryParams); + retrieve(vm.queryParams, vm.page, vm.pageSize); }); function search(e) { @@ -87,7 +102,7 @@ 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,14 +113,17 @@ } } - 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'); + + console.log('Total Count in logs:' + vm.totalCount + ', page:' + vm.page); vm.queryParams = { 'beginTimestamp' : 0, diff --git a/static/resources/js/components/paginator/paginator.directive.html b/static/resources/js/components/paginator/paginator.directive.html new file mode 100644 index 000000000..2018061f9 --- /dev/null +++ b/static/resources/js/components/paginator/paginator.directive.html @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/static/resources/js/components/paginator/paginator.directive.js b/static/resources/js/components/paginator/paginator.directive.js new file mode 100644 index 000000000..c71bba9e6 --- /dev/null +++ b/static/resources/js/components/paginator/paginator.directive.js @@ -0,0 +1,183 @@ +(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': '@', + 'retrieve': '&' + }, + 'link': link, + 'controller': PaginatorController, + 'controllerAs': 'vm', + 'bindToController': true + }; + return directive; + + function link(scope, element, attrs, ctrl) { + + scope.$watch('vm.totalCount', function(current) { + if(current) { + var totalCount = current; + var pageSize = parseInt(ctrl.pageSize); + var displayCount = parseInt(ctrl.displayCount); + + console.log('Total Count:' + totalCount + ', Page Size:' + pageSize + ', Display Count:' + displayCount); + + 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; + ++ctrl.page; + } + } + + TimeCounter.prototype.canIncrement = function() { + if(this.time < this.maximum) { + return true; + } + return false; + } + + TimeCounter.prototype.decrement = function() { + if(this.time > this.minimum) { + --this.time; + --ctrl.page; + } + } + + TimeCounter.prototype.canDecrement = function() { + if(this.time > this.minimum) { + return true; + } + return false; + } + + TimeCounter.prototype.getTime = function() { + return this.time; + } + + var buttonCount = Math.ceil(totalCount / pageSize); + var tc = new TimeCounter(); + + if(buttonCount <= displayCount) { + tc.setMaximum(0); + }else{ + tc.setMaximum(Math.floor(buttonCount / displayCount)); + } + + element.find('ul li:first a').on('click', previous); + ctrl.showPrevious = false; + + element.find('ul li:last a').on('click', next); + ctrl.showNext = (buttonCount > displayCount); + + var drawButtons = function(time) { + element.find('li[tag="pagination-button"]').remove(); + var buttons = []; + for(var i = 1; i <= displayCount; i++) { + var displayNumber = displayCount * time + i; + if(displayNumber <= buttonCount) { + buttons.push('
  • ' + displayNumber + '
  • '); + } + } + $(buttons.join('')) + .insertAfter(element.find('ul li:eq(0)')).end() + .on('click', buttonClickHandler); + } + + drawButtons(tc.getTime()); + togglePrevious(false); + toggleNext((buttonCount > displayCount)); + + togglePageButton(); + + + 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(); + ctrl.retrieve(); + + if(tc.canIncrement()) { + toggleNext(true); + }else { + toggleNext(false); + } + + if(tc.canDecrement()) { + togglePrevious(true); + }else{ + togglePrevious(false); + } + } + + 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()); + element.find('li[tag="pagination-button"] a[page="' + ctrl.page + '"]').trigger('click'); + } + } + + function next() { + if(tc.canIncrement()) { + tc.increment(); + drawButtons(tc.getTime()); + element.find('li[tag="pagination-button"] a[page="' + ctrl.page + '"]').trigger('click'); + + } + } + } + }); + } + } + +})(); \ No newline at end of file diff --git a/static/resources/js/components/paginator/paginator.module.js b/static/resources/js/components/paginator/paginator.module.js new file mode 100644 index 000000000..cc2fa17b5 --- /dev/null +++ b/static/resources/js/components/paginator/paginator.module.js @@ -0,0 +1,8 @@ +(function() { + + 'use strict'; + + angular + .module('harbor.paginator', []); + +})(); \ No newline at end of file diff --git a/static/resources/js/harbor.module.js b/static/resources/js/harbor.module.js index 500ae0f58..64dec5ea4 100644 --- a/static/resources/js/harbor.module.js +++ b/static/resources/js/harbor.module.js @@ -60,6 +60,7 @@ 'harbor.system.management', 'harbor.loading.progress', 'harbor.inline.help', - 'harbor.dismissable.alerts' + 'harbor.dismissable.alerts', + 'harbor.paginator' ]); })(); diff --git a/static/resources/js/services/log/services.list-log.js b/static/resources/js/services/log/services.list-log.js index 732c36c50..b2c14f13e 100644 --- a/static/resources/js/services/log/services.list-log.js +++ b/static/resources/js/services/log/services.list-log.js @@ -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, diff --git a/views/sections/script-include.htm b/views/sections/script-include.htm index 77ad5c2f6..9c2776b77 100644 --- a/views/sections/script-include.htm +++ b/views/sections/script-include.htm @@ -191,4 +191,7 @@ - \ No newline at end of file + + + + \ No newline at end of file From d4035e0ec336d61ff3e1e17ff2c6c3851916d083 Mon Sep 17 00:00:00 2001 From: kunw Date: Sat, 6 Aug 2016 15:11:20 +0800 Subject: [PATCH 08/21] updates for paginator and pagination in logs. --- .../js/components/log/list-log.directive.html | 2 +- .../js/components/log/list-log.directive.js | 2 +- .../paginator/paginator.directive.js | 45 +++++++------------ 3 files changed, 19 insertions(+), 30 deletions(-) diff --git a/static/resources/js/components/log/list-log.directive.html b/static/resources/js/components/log/list-log.directive.html index d0804e8b7..498129f2b 100644 --- a/static/resources/js/components/log/list-log.directive.html +++ b/static/resources/js/components/log/list-log.directive.html @@ -47,8 +47,8 @@ - + diff --git a/static/resources/js/components/log/list-log.directive.js b/static/resources/js/components/log/list-log.directive.js index a345cdb93..9daad971c 100644 --- a/static/resources/js/components/log/list-log.directive.js +++ b/static/resources/js/components/log/list-log.directive.js @@ -53,7 +53,7 @@ }; vm.page = 1; - vm.pageSize = 1; + vm.pageSize = 2; $scope.$watch('vm.totalCount', function(current) { if(current) { diff --git a/static/resources/js/components/paginator/paginator.directive.js b/static/resources/js/components/paginator/paginator.directive.js index c71bba9e6..a224730a6 100644 --- a/static/resources/js/components/paginator/paginator.directive.js +++ b/static/resources/js/components/paginator/paginator.directive.js @@ -22,8 +22,7 @@ 'totalCount': '@', 'pageSize': '@', 'page': '=', - 'displayCount': '@', - 'retrieve': '&' + 'displayCount': '@' }, 'link': link, 'controller': PaginatorController, @@ -57,6 +56,7 @@ ++this.time; ++ctrl.page; } + scope.$apply(); } TimeCounter.prototype.canIncrement = function() { @@ -71,6 +71,7 @@ --this.time; --ctrl.page; } + scope.$apply(); } TimeCounter.prototype.canDecrement = function() { @@ -93,11 +94,8 @@ tc.setMaximum(Math.floor(buttonCount / displayCount)); } - element.find('ul li:first a').on('click', previous); - ctrl.showPrevious = false; - + element.find('ul li:first a').on('click', previous); element.find('ul li:last a').on('click', next); - ctrl.showNext = (buttonCount > displayCount); var drawButtons = function(time) { element.find('li[tag="pagination-button"]').remove(); @@ -114,12 +112,12 @@ } drawButtons(tc.getTime()); - togglePrevious(false); - toggleNext((buttonCount > displayCount)); + + togglePrevious(tc.canDecrement()); + toggleNext(tc.canIncrement()); togglePageButton(); - function togglePrevious(status) { if(status){ element.find('ul li:first').removeClass('disabled'); @@ -133,38 +131,29 @@ 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(); - ctrl.retrieve(); - - if(tc.canIncrement()) { - toggleNext(true); - }else { - toggleNext(false); - } - - if(tc.canDecrement()) { - togglePrevious(true); - }else{ - togglePrevious(false); - } + 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'); + 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()); - element.find('li[tag="pagination-button"] a[page="' + ctrl.page + '"]').trigger('click'); + element.find('li[tag="pagination-button"] a[page="' + ctrl.page + '"]').trigger('click'); } + scope.$apply(); } function next() { @@ -172,8 +161,8 @@ tc.increment(); drawButtons(tc.getTime()); element.find('li[tag="pagination-button"] a[page="' + ctrl.page + '"]').trigger('click'); - } + scope.$apply(); } } }); From 066d108012d3a4a7e14b5580ea1c65bfcd4caa01 Mon Sep 17 00:00:00 2001 From: kunw Date: Sat, 6 Aug 2016 16:12:48 +0800 Subject: [PATCH 09/21] updates paginator with following the restriction mode --- .../js/components/log/list-log.directive.html | 2 +- .../js/components/log/list-log.directive.js | 2 +- .../paginator/paginator.directive.js | 219 +++++++++--------- 3 files changed, 113 insertions(+), 110 deletions(-) diff --git a/static/resources/js/components/log/list-log.directive.html b/static/resources/js/components/log/list-log.directive.html index 498129f2b..8da7229d5 100644 --- a/static/resources/js/components/log/list-log.directive.html +++ b/static/resources/js/components/log/list-log.directive.html @@ -48,7 +48,7 @@ - + diff --git a/static/resources/js/components/log/list-log.directive.js b/static/resources/js/components/log/list-log.directive.js index 9daad971c..a345cdb93 100644 --- a/static/resources/js/components/log/list-log.directive.js +++ b/static/resources/js/components/log/list-log.directive.js @@ -53,7 +53,7 @@ }; vm.page = 1; - vm.pageSize = 2; + vm.pageSize = 1; $scope.$watch('vm.totalCount', function(current) { if(current) { diff --git a/static/resources/js/components/paginator/paginator.directive.js b/static/resources/js/components/paginator/paginator.directive.js index a224730a6..3ef88974f 100644 --- a/static/resources/js/components/paginator/paginator.directive.js +++ b/static/resources/js/components/paginator/paginator.directive.js @@ -32,7 +32,7 @@ return directive; function link(scope, element, attrs, ctrl) { - + scope.$watch('vm.totalCount', function(current) { if(current) { var totalCount = current; @@ -40,54 +40,10 @@ var displayCount = parseInt(ctrl.displayCount); console.log('Total Count:' + totalCount + ', Page Size:' + pageSize + ', Display Count:' + displayCount); - - 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; - ++ctrl.page; - } - scope.$apply(); - } - - TimeCounter.prototype.canIncrement = function() { - if(this.time < this.maximum) { - return true; - } - return false; - } - - TimeCounter.prototype.decrement = function() { - if(this.time > this.minimum) { - --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; - } - + var buttonCount = Math.ceil(totalCount / pageSize); - var tc = new TimeCounter(); - + ctrl.buttonCount = buttonCount; + if(buttonCount <= displayCount) { tc.setMaximum(0); }else{ @@ -97,20 +53,6 @@ element.find('ul li:first a').on('click', previous); element.find('ul li:last a').on('click', next); - var drawButtons = function(time) { - element.find('li[tag="pagination-button"]').remove(); - var buttons = []; - for(var i = 1; i <= displayCount; i++) { - var displayNumber = displayCount * time + i; - if(displayNumber <= buttonCount) { - buttons.push('
  • ' + displayNumber + '
  • '); - } - } - $(buttons.join('')) - .insertAfter(element.find('ul li:eq(0)')).end() - .on('click', buttonClickHandler); - } - drawButtons(tc.getTime()); togglePrevious(tc.canDecrement()); @@ -118,54 +60,115 @@ togglePageButton(); - 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()); - element.find('li[tag="pagination-button"] a[page="' + ctrl.page + '"]').trigger('click'); - } - scope.$apply(); - } - - function next() { - if(tc.canIncrement()) { - tc.increment(); - drawButtons(tc.getTime()); - element.find('li[tag="pagination-button"] a[page="' + ctrl.page + '"]').trigger('click'); - } - scope.$apply(); - } } }); + + 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; + ++ctrl.page; + } + scope.$apply(); + } + + TimeCounter.prototype.canIncrement = function() { + if(this.time < this.maximum) { + return true; + } + return false; + } + + TimeCounter.prototype.decrement = function() { + if(this.time > this.minimum) { + --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; + } + + var tc = new TimeCounter(); + + function drawButtons(time, displayCount, buttonCount) { + 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('
  • ' + displayNumber + '
  • '); + } + } + $(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()); + element.find('li[tag="pagination-button"] a[page="' + ctrl.page + '"]').trigger('click'); + } + scope.$apply(); + } + + function next() { + if(tc.canIncrement()) { + tc.increment(); + drawButtons(tc.getTime()); + element.find('li[tag="pagination-button"] a[page="' + ctrl.page + '"]').trigger('click'); + } + scope.$apply(); + } } } From 8245eeaec969fecf88f8387bce10fa69f7941fb2 Mon Sep 17 00:00:00 2001 From: kunw Date: Sat, 6 Aug 2016 17:19:29 +0800 Subject: [PATCH 10/21] updates for paginator toggling previous and next. --- .../js/components/log/list-log.directive.js | 2 +- .../paginator/paginator.directive.js | 57 ++++++++++++------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/static/resources/js/components/log/list-log.directive.js b/static/resources/js/components/log/list-log.directive.js index a345cdb93..7ab65e0f6 100644 --- a/static/resources/js/components/log/list-log.directive.js +++ b/static/resources/js/components/log/list-log.directive.js @@ -101,7 +101,7 @@ vm.queryParams.beginTimestamp = toUTCSeconds(vm.fromDate, 0, 0, 0); vm.queryParams.endTimestamp = toUTCSeconds(vm.toDate, 23, 59, 59); - + vm.page = 1; retrieve(vm.queryParams, vm.page, vm.pageSize); } diff --git a/static/resources/js/components/paginator/paginator.directive.js b/static/resources/js/components/paginator/paginator.directive.js index 3ef88974f..ffbbfa5e3 100644 --- a/static/resources/js/components/paginator/paginator.directive.js +++ b/static/resources/js/components/paginator/paginator.directive.js @@ -33,25 +33,29 @@ function link(scope, element, attrs, ctrl) { + var tc; + scope.$watch('vm.totalCount', function(current) { if(current) { - var totalCount = current; - var pageSize = parseInt(ctrl.pageSize); - var displayCount = parseInt(ctrl.displayCount); + var totalCount = current; + + element.find('ul li:first a').off('click'); + element.find('ul li:last a').off('click'); - console.log('Total Count:' + totalCount + ', Page Size:' + pageSize + ', Display Count:' + displayCount); + tc = new TimeCounter(); + + console.log('Total Count:' + totalCount + ', Page Size:' + ctrl.pageSize + ', Display Count:' + ctrl.displayCount + ', Page:' + ctrl.page); - var buttonCount = Math.ceil(totalCount / pageSize); - ctrl.buttonCount = buttonCount; + ctrl.buttonCount = Math.ceil(totalCount / ctrl.pageSize); - if(buttonCount <= displayCount) { + if(ctrl.buttonCount <= ctrl.displayCount) { tc.setMaximum(0); }else{ - tc.setMaximum(Math.floor(buttonCount / displayCount)); + tc.setMaximum(Math.floor(ctrl.buttonCount / ctrl.displayCount)); } - + element.find('ul li:first a').on('click', previous); - element.find('ul li:last a').on('click', next); + element.find('ul li:last a').on('click', next); drawButtons(tc.getTime()); @@ -62,8 +66,8 @@ } }); - - var TimeCounter = function() { + + var TimeCounter = function() { this.time = 0; this.minimum = 0; this.maximum = 0; @@ -71,45 +75,54 @@ 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; + console.log('Increment Page:' + ctrl.page + ', DisplayCount:' + ctrl.displayCount + ',Time:' + this.time); } scope.$apply(); - } + }; TimeCounter.prototype.canIncrement = function() { if(this.time < this.maximum) { return true; } return false; - } + }; TimeCounter.prototype.decrement = function() { if(this.time > this.minimum) { --this.time; --ctrl.page; + + if(this.time === 0) { + ctrl.page = ctrl.displayCount; + }else if((ctrl.page % ctrl.displayCount) != 0) { + ctrl.page = this.time * ctrl.displayCount; + } + console.log('Decrement Page:' + ctrl.page + ', DisplayCount:' + ctrl.displayCount + ',Time:' + this.time); } scope.$apply(); - } + }; TimeCounter.prototype.canDecrement = function() { if(this.time > this.minimum) { return true; } return false; - } + }; TimeCounter.prototype.getTime = function() { return this.time; - } - - var tc = new TimeCounter(); - - function drawButtons(time, displayCount, buttonCount) { + }; + + function drawButtons(time) { element.find('li[tag="pagination-button"]').remove(); var buttons = []; for(var i = 1; i <= ctrl.displayCount; i++) { From e44d445ee3899477bb2bf13ada94badd54f8760f Mon Sep 17 00:00:00 2001 From: kunw Date: Sat, 6 Aug 2016 17:35:57 +0800 Subject: [PATCH 11/21] update for toggling page button. --- .../resources/js/components/paginator/paginator.directive.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/resources/js/components/paginator/paginator.directive.js b/static/resources/js/components/paginator/paginator.directive.js index ffbbfa5e3..ebabba51b 100644 --- a/static/resources/js/components/paginator/paginator.directive.js +++ b/static/resources/js/components/paginator/paginator.directive.js @@ -169,7 +169,7 @@ if(tc.canDecrement()) { tc.decrement(); drawButtons(tc.getTime()); - element.find('li[tag="pagination-button"] a[page="' + ctrl.page + '"]').trigger('click'); + togglePageButton(); } scope.$apply(); } @@ -178,7 +178,7 @@ if(tc.canIncrement()) { tc.increment(); drawButtons(tc.getTime()); - element.find('li[tag="pagination-button"] a[page="' + ctrl.page + '"]').trigger('click'); + togglePageButton(); } scope.$apply(); } From 5cf6f99025378fbf5efcb6f647a9bfa433eb2c11 Mon Sep 17 00:00:00 2001 From: kunw Date: Mon, 8 Aug 2016 17:29:42 +0800 Subject: [PATCH 12/21] update for pagination in repository, replication and logs. --- static/resources/css/project.css | 2 - .../js/components/log/list-log.directive.html | 4 +- .../js/components/log/list-log.directive.js | 50 +++++++++---------- .../paginator/paginator.directive.html | 6 +-- .../paginator/paginator.directive.js | 36 +++++++------ .../list-replication.directive.html | 6 ++- .../replication/list-replication.directive.js | 37 +++++++++----- .../repository/list-repository.directive.html | 1 + .../repository/list-repository.directive.js | 25 +++++++--- .../services.list-replication-job.js | 4 +- .../repository/services.list-repository.js | 4 +- 11 files changed, 102 insertions(+), 73 deletions(-) diff --git a/static/resources/css/project.css b/static/resources/css/project.css index 667283bba..3f3917232 100644 --- a/static/resources/css/project.css +++ b/static/resources/css/project.css @@ -73,13 +73,11 @@ } .sub-pane { - margin: 15px; min-height: 380px; overflow-y: auto; } .well-custom { - width: 100%; background-color: #f5f5f5; background-image: none; diff --git a/static/resources/js/components/log/list-log.directive.html b/static/resources/js/components/log/list-log.directive.html index 8da7229d5..86b253a53 100644 --- a/static/resources/js/components/log/list-log.directive.html +++ b/static/resources/js/components/log/list-log.directive.html @@ -48,8 +48,8 @@ - - + + diff --git a/static/resources/js/components/log/list-log.directive.js b/static/resources/js/components/log/list-log.directive.js index 7ab65e0f6..369bf5c5b 100644 --- a/static/resources/js/components/log/list-log.directive.js +++ b/static/resources/js/components/log/list-log.directive.js @@ -53,15 +53,10 @@ }; vm.page = 1; - vm.pageSize = 1; - - $scope.$watch('vm.totalCount', function(current) { - if(current) { - vm.totalCount = current; - } - }); - $scope.$watch('vm.page', function(current) { - if(current) { + vm.pageSize = 20; + + $scope.$watch('vm.page', function(current, origin) { + if(current !== 1) { vm.page = current; retrieve(vm.queryParams, vm.page, vm.pageSize); } @@ -88,7 +83,9 @@ }); function search(e) { - + + vm.page = 1; + if(e.op[0] === 'all') { e.op = ['create', 'pull', 'push', 'delete']; } @@ -98,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); - vm.page = 1; + retrieve(vm.queryParams, vm.page, vm.pageSize); + } function showAdvancedSearch() { @@ -113,7 +111,7 @@ } } - function retrieve(queryParams, page, pageSize) { + function retrieve(queryParams, page, pageSize) { ListLogService(queryParams, page, pageSize) .then(listLogComplete) .catch(listLogFailed); @@ -122,21 +120,21 @@ function listLogComplete(response) { vm.logs = response.data; vm.totalCount = response.headers('X-Total-Count'); - + console.log('Total Count in logs:' + vm.totalCount + ', page:' + vm.page); - vm.queryParams = { - 'beginTimestamp' : 0, - 'endTimestamp' : 0, - 'keywords' : '', - 'projectId': vm.projectId, - 'username' : '' - }; - vm.op = ['all']; - vm.fromDate = ''; - vm.toDate = ''; - vm.others = ''; - vm.opOthers = true; +// vm.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){ diff --git a/static/resources/js/components/paginator/paginator.directive.html b/static/resources/js/components/paginator/paginator.directive.html index 2018061f9..73cc77c4b 100644 --- a/static/resources/js/components/paginator/paginator.directive.html +++ b/static/resources/js/components/paginator/paginator.directive.html @@ -1,5 +1,5 @@ - diff --git a/static/resources/js/components/paginator/paginator.directive.js b/static/resources/js/components/paginator/paginator.directive.js index ebabba51b..7a091978a 100644 --- a/static/resources/js/components/paginator/paginator.directive.js +++ b/static/resources/js/components/paginator/paginator.directive.js @@ -33,15 +33,22 @@ function link(scope, element, attrs, ctrl) { - var tc; + 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); @@ -49,9 +56,9 @@ ctrl.buttonCount = Math.ceil(totalCount / ctrl.pageSize); if(ctrl.buttonCount <= ctrl.displayCount) { - tc.setMaximum(0); + tc.setMaximum(1); }else{ - tc.setMaximum(Math.floor(ctrl.buttonCount / ctrl.displayCount)); + tc.setMaximum(Math.ceil(ctrl.buttonCount / ctrl.displayCount)); } element.find('ul li:first a').on('click', previous); @@ -67,7 +74,7 @@ } }); - var TimeCounter = function() { + var TimeCounter = function() { this.time = 0; this.minimum = 0; this.maximum = 0; @@ -84,29 +91,26 @@ ctrl.page = this.time * ctrl.displayCount; } ++ctrl.page; - console.log('Increment Page:' + ctrl.page + ', DisplayCount:' + ctrl.displayCount + ',Time:' + this.time); } scope.$apply(); }; TimeCounter.prototype.canIncrement = function() { - if(this.time < this.maximum) { + if(this.time + 1 < this.maximum) { return true; } return false; }; TimeCounter.prototype.decrement = function() { - if(this.time > this.minimum) { - --this.time; - --ctrl.page; - + if(this.time > this.minimum) { if(this.time === 0) { - ctrl.page = ctrl.displayCount; + ctrl.page = ctrl.displayCount; }else if((ctrl.page % ctrl.displayCount) != 0) { ctrl.page = this.time * ctrl.displayCount; } - console.log('Decrement Page:' + ctrl.page + ', DisplayCount:' + ctrl.displayCount + ',Time:' + this.time); + --this.time; + --ctrl.page; } scope.$apply(); }; @@ -170,6 +174,8 @@ tc.decrement(); drawButtons(tc.getTime()); togglePageButton(); + togglePrevious(tc.canDecrement()); + toggleNext(tc.canIncrement()); } scope.$apply(); } @@ -179,6 +185,8 @@ tc.increment(); drawButtons(tc.getTime()); togglePageButton(); + togglePrevious(tc.canDecrement()); + toggleNext(tc.canIncrement()); } scope.$apply(); } diff --git a/static/resources/js/components/replication/list-replication.directive.html b/static/resources/js/components/replication/list-replication.directive.html index e5762a57f..a7d7036aa 100644 --- a/static/resources/js/components/replication/list-replication.directive.html +++ b/static/resources/js/components/replication/list-replication.directive.html @@ -73,7 +73,9 @@ -
    //vm.replicationPolicies ? vm.replicationPolicies.length : 0// // 'items' | tr //
    +
    +
    //vm.replicationPolicies ? vm.replicationPolicies.length : 0// // 'items' | tr //
    +

    // 'replication_jobs' | tr //


    @@ -147,7 +149,7 @@ -
    //vm.replicationJobs ? vm.replicationJobs.length : 0// // 'items' | tr //
    + diff --git a/static/resources/js/components/replication/list-replication.directive.js b/static/resources/js/components/replication/list-replication.directive.js index 0208c5ff1..731666258 100644 --- a/static/resources/js/components/replication/list-replication.directive.js +++ b/static/resources/js/components/replication/list-replication.directive.js @@ -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'); } diff --git a/static/resources/js/components/repository/list-repository.directive.html b/static/resources/js/components/repository/list-repository.directive.html index 23ad69a99..19098c57a 100644 --- a/static/resources/js/components/repository/list-repository.directive.html +++ b/static/resources/js/components/repository/list-repository.directive.html @@ -30,6 +30,7 @@ + \ No newline at end of file diff --git a/static/resources/js/components/repository/list-repository.directive.js b/static/resources/js/components/repository/list-repository.directive.js index 7f12a7891..da6fecfe2 100644 --- a/static/resources/js/components/repository/list-repository.directive.js +++ b/static/resources/js/components/repository/list-repository.directive.js @@ -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); } diff --git a/static/resources/js/services/replication-job/services.list-replication-job.js b/static/resources/js/services/replication-job/services.list-replication-job.js index 231da6608..493c887a3 100644 --- a/static/resources/js/services/replication-job/services.list-replication-job.js +++ b/static/resources/js/services/replication-job/services.list-replication-job.js @@ -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, diff --git a/static/resources/js/services/repository/services.list-repository.js b/static/resources/js/services/repository/services.list-repository.js index 5cdb5cc6b..fa911a85c 100644 --- a/static/resources/js/services/repository/services.list-repository.js +++ b/static/resources/js/services/repository/services.list-repository.js @@ -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 From a7f0037156dba3f57aade60a6037413e1766cceb Mon Sep 17 00:00:00 2001 From: kunw Date: Mon, 8 Aug 2016 17:31:57 +0800 Subject: [PATCH 13/21] updates for toggling project publicity and block user remove admin role with himself. --- .../js/components/project/publicity-button.directive.html | 4 ++-- .../resources/js/components/user/list-user.directive.html | 4 ++-- static/resources/js/components/user/list-user.directive.js | 6 ++++-- .../js/components/user/toggle-admin.directive.html | 2 +- .../resources/js/components/user/toggle-admin.directive.js | 4 +++- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/static/resources/js/components/project/publicity-button.directive.html b/static/resources/js/components/project/publicity-button.directive.html index 62460d3db..05a4688b0 100644 --- a/static/resources/js/components/project/publicity-button.directive.html +++ b/static/resources/js/components/project/publicity-button.directive.html @@ -12,5 +12,5 @@ See the License for the specific language governing permissions and limitations under the License. --> - - \ No newline at end of file + + \ No newline at end of file diff --git a/static/resources/js/components/user/list-user.directive.html b/static/resources/js/components/user/list-user.directive.html index 705ca4441..31e540295 100644 --- a/static/resources/js/components/user/list-user.directive.html +++ b/static/resources/js/components/user/list-user.directive.html @@ -44,10 +44,10 @@ //u.email// //u.creation_time | dateL : 'YYYY-MM-DD HH:mm:ss'// - + -    +    diff --git a/static/resources/js/components/user/list-user.directive.js b/static/resources/js/components/user/list-user.directive.js index 1e9d42ec2..18cd60a2c 100644 --- a/static/resources/js/components/user/list-user.directive.js +++ b/static/resources/js/components/user/list-user.directive.js @@ -20,9 +20,9 @@ .module('harbor.user') .directive('listUser', listUser); - ListUserController.$inject = ['$scope', 'ListUserService', 'DeleteUserService', '$filter', 'trFilter']; + ListUserController.$inject = ['$scope', 'ListUserService', 'DeleteUserService', 'currentUser', '$filter', 'trFilter']; - function ListUserController($scope, ListUserService, DeleteUserService, $filter, $trFilter) { + function ListUserController($scope, ListUserService, DeleteUserService, currentUser, $filter, $trFilter) { $scope.subsSubPane = 226; @@ -33,6 +33,8 @@ vm.deleteUser = deleteUser; vm.confirmToDelete = confirmToDelete; vm.retrieve = retrieve; + + vm.currentUser = currentUser.get(); vm.retrieve(); diff --git a/static/resources/js/components/user/toggle-admin.directive.html b/static/resources/js/components/user/toggle-admin.directive.html index 5c43a9d19..dfaf86cf7 100644 --- a/static/resources/js/components/user/toggle-admin.directive.html +++ b/static/resources/js/components/user/toggle-admin.directive.html @@ -12,5 +12,5 @@ See the License for the specific language governing permissions and limitations under the License. --> - + \ No newline at end of file diff --git a/static/resources/js/components/user/toggle-admin.directive.js b/static/resources/js/components/user/toggle-admin.directive.js index 718a6888e..c4c90c257 100644 --- a/static/resources/js/components/user/toggle-admin.directive.js +++ b/static/resources/js/components/user/toggle-admin.directive.js @@ -28,6 +28,7 @@ vm.isAdmin = (vm.hasAdminRole === 1); vm.enabled = vm.isAdmin ? 0 : 1; vm.toggle = toggle; + vm.editable = (vm.currentUser.user_id != vm.userId); function toggle() { ToggleAdminService(vm.userId, vm.enabled) @@ -63,7 +64,8 @@ 'templateUrl': '/static/resources/js/components/user/toggle-admin.directive.html', 'scope': { 'hasAdminRole': '=', - 'userId': '@' + 'userId': '@', + 'currentUser': '=' }, 'link': link, 'controller': ToggleAdminController, From 74702a0be759d2b54b3250c5195725e23bd671d3 Mon Sep 17 00:00:00 2001 From: yhua Date: Mon, 8 Aug 2016 16:40:14 +0800 Subject: [PATCH 14/21] add api testing framework --- .travis.yml | 42 +-- api/harborapi_test.go | 312 ++++++++++++++++++ api/project_test.go | 65 ++++ api/search_test.go | 32 ++ tests/apitests/apilib/accesslog.go | 3 +- tests/apitests/apilib/harborapi.go | 4 +- tests/apitests/apilib/harborlogout.go | 2 +- tests/apitests/apilib/harlogin.go | 2 +- tests/apitests/apilib/password.go | 13 + tests/apitests/apilib/project.go | 9 +- tests/apitests/apilib/projecttemp4search.go | 7 +- tests/apitests/apilib/repository.go | 5 +- .../apitests/apilib/repositorytemp4search.go | 7 +- tests/apitests/apilib/role.go | 5 +- tests/apitests/apilib/roleparam.go | 3 +- tests/apitests/apilib/search.go | 3 +- tests/apitests/apilib/user.go | 5 +- tests/apitests/hbapiaddprj_test.go | 95 ------ tests/apitests/hbapidelrpo_test.go | 130 -------- tests/apitests/hbapisearch_test.go | 31 -- tests/docker-compose.test.yml | 23 ++ tests/testprepare.sh | 13 +- 22 files changed, 506 insertions(+), 305 deletions(-) create mode 100644 api/harborapi_test.go create mode 100644 api/project_test.go create mode 100644 api/search_test.go create mode 100644 tests/apitests/apilib/password.go delete mode 100644 tests/apitests/hbapiaddprj_test.go delete mode 100644 tests/apitests/hbapidelrpo_test.go delete mode 100644 tests/apitests/hbapisearch_test.go create mode 100644 tests/docker-compose.test.yml diff --git a/.travis.yml b/.travis.yml index a8148c361..dfcc04c3e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,27 +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 + SECRET_KEY: 1234567890123456 + AUTH_MODE: db_auth before_install: - sudo ./tests/hostcfg.sh @@ -56,8 +54,8 @@ 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 +# - sudo apt-get install -y --force-yes -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" docker-engine=1.11.1-0~trusty +# - sudo rm /usr/local/bin/docker-compose - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose - chmod +x docker-compose - sudo mv docker-compose /usr/local/bin @@ -70,21 +68,27 @@ install: 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 diff --git a/api/harborapi_test.go b/api/harborapi_test.go new file mode 100644 index 000000000..ae41796a5 --- /dev/null +++ b/api/harborapi_test.go @@ -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'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 +} diff --git a/api/project_test.go b/api/project_test.go new file mode 100644 index 000000000..43a9f77dd --- /dev/null +++ b/api/project_test.go @@ -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) + } + +} diff --git a/api/search_test.go b/api/search_test.go new file mode 100644 index 000000000..529a87025 --- /dev/null +++ b/api/search_test.go @@ -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].ProjectID, int32(1), "Project id should be equal") + assert.Equal(result.Projects[0].ProjectName, "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) + //} + +} diff --git a/tests/apitests/apilib/accesslog.go b/tests/apitests/apilib/accesslog.go index 0d6318102..4c25b33a7 100644 --- a/tests/apitests/apilib/accesslog.go +++ b/tests/apitests/apilib/accesslog.go @@ -13,8 +13,9 @@ limitations under the License. */ -package HarborAPI +package apilib +//AccessLog for go test type AccessLog struct { Username string `json:"username,omitempty"` Keywords string `json:"keywords,omitempty"` diff --git a/tests/apitests/apilib/harborapi.go b/tests/apitests/apilib/harborapi.go index edf3b6ff1..659e47875 100644 --- a/tests/apitests/apilib/harborapi.go +++ b/tests/apitests/apilib/harborapi.go @@ -1,6 +1,6 @@ -//Package HarborAPI +//Package apilib //These APIs provide services for manipulating Harbor project. -package HarborAPI +package apilib import ( "encoding/json" diff --git a/tests/apitests/apilib/harborlogout.go b/tests/apitests/apilib/harborlogout.go index fa59ee2bb..9a9da0be9 100644 --- a/tests/apitests/apilib/harborlogout.go +++ b/tests/apitests/apilib/harborlogout.go @@ -1,5 +1,5 @@ // HarborLogout.go -package HarborAPI +package apilib import ( "net/http" diff --git a/tests/apitests/apilib/harlogin.go b/tests/apitests/apilib/harlogin.go index d711103bb..1b9117ecd 100644 --- a/tests/apitests/apilib/harlogin.go +++ b/tests/apitests/apilib/harlogin.go @@ -1,5 +1,5 @@ // HarborLogon.go -package HarborAPI +package apilib import ( "io/ioutil" diff --git a/tests/apitests/apilib/password.go b/tests/apitests/apilib/password.go new file mode 100644 index 000000000..dca6a48a1 --- /dev/null +++ b/tests/apitests/apilib/password.go @@ -0,0 +1,13 @@ +package apilib + +import () + +//Password for go test +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"` +} diff --git a/tests/apitests/apilib/project.go b/tests/apitests/apilib/project.go index 19444957b..dc0ffe523 100644 --- a/tests/apitests/apilib/project.go +++ b/tests/apitests/apilib/project.go @@ -1,14 +1,15 @@ -package HarborAPI +package apilib import () +//Project for go test type Project struct { - ProjectId int32 `json:"id,omitempty"` - OwnerId int32 `json:"owner_id,omitempty"` + ProjectID int32 `json:"id,omitempty"` + OwnerID int32 `json:"owner_id,omitempty"` ProjectName string `json:"project_name,omitempty"` CreationTime string `json:"creation_time,omitempty"` Deleted int32 `json:"deleted,omitempty"` - UserId int32 `json:"user_id,omitempty"` + UserID int32 `json:"user_id,omitempty"` OwnerName string `json:"owner_name,omitempty"` Public bool `json:"public,omitempty"` Togglable bool `json:"togglable,omitempty"` diff --git a/tests/apitests/apilib/projecttemp4search.go b/tests/apitests/apilib/projecttemp4search.go index c7219cc97..4b80cc045 100644 --- a/tests/apitests/apilib/projecttemp4search.go +++ b/tests/apitests/apilib/projecttemp4search.go @@ -1,9 +1,8 @@ -package HarborAPI - -import () +package apilib +//Project4Search ... type Project4Search struct { - ProjectId int32 `json:"id,omitempty"` + ProjectID int32 `json:"id,omitempty"` ProjectName string `json:"name,omitempty"` Public int32 `json:"public,omitempty"` } diff --git a/tests/apitests/apilib/repository.go b/tests/apitests/apilib/repository.go index 5de9302df..1673d83e4 100644 --- a/tests/apitests/apilib/repository.go +++ b/tests/apitests/apilib/repository.go @@ -1,11 +1,12 @@ -package HarborAPI +package apilib import ( "time" ) +//Repository ... type Repository struct { - Id string `json:"id,omitempty"` + ID string `json:"id,omitempty"` Parent string `json:"parent,omitempty"` Created time.Time `json:"created,omitempty"` DurationDays string `json:"duration_days,omitempty"` diff --git a/tests/apitests/apilib/repositorytemp4search.go b/tests/apitests/apilib/repositorytemp4search.go index f0dead98b..e438fcef6 100644 --- a/tests/apitests/apilib/repositorytemp4search.go +++ b/tests/apitests/apilib/repositorytemp4search.go @@ -1,7 +1,10 @@ -package HarborAPI +package apilib +import () + +//Repository4Search ... type Repository4Search struct { - ProjectId int32 `json:"project_id,omitempty"` + ProjectID int32 `json:"project_id,omitempty"` ProjectName string `json:"project_name,omitempty"` ProjectPublic int32 `json:"project_public,omitempty"` RepoName string `json:"repository_name,omitempty"` diff --git a/tests/apitests/apilib/role.go b/tests/apitests/apilib/role.go index c6e53b044..1b18eca2e 100644 --- a/tests/apitests/apilib/role.go +++ b/tests/apitests/apilib/role.go @@ -1,7 +1,8 @@ -package HarborAPI +package apilib +//Role ... type Role struct { - RoleId int32 `json:"role_id,omitempty"` + RoleID int32 `json:"role_id,omitempty"` RoleCode string `json:"role_code,omitempty"` RoleName string `json:"role_name,omitempty"` } diff --git a/tests/apitests/apilib/roleparam.go b/tests/apitests/apilib/roleparam.go index 12cf755a5..b91ac05e4 100644 --- a/tests/apitests/apilib/roleparam.go +++ b/tests/apitests/apilib/roleparam.go @@ -1,5 +1,6 @@ -package HarborAPI +package apilib +//RoleParam ... type RoleParam struct { Roles []int32 `json:"roles,omitempty"` UserName string `json:"user_name,omitempty"` diff --git a/tests/apitests/apilib/search.go b/tests/apitests/apilib/search.go index 81ca0fb20..ae7610f58 100644 --- a/tests/apitests/apilib/search.go +++ b/tests/apitests/apilib/search.go @@ -1,7 +1,8 @@ -package HarborAPI +package apilib import () +//Search ... type Search struct { Projects []Project4Search `json:"project,omitempty"` Repositories []Repository4Search `json:"repository,omitempty"` diff --git a/tests/apitests/apilib/user.go b/tests/apitests/apilib/user.go index 10a07933d..a514c1650 100644 --- a/tests/apitests/apilib/user.go +++ b/tests/apitests/apilib/user.go @@ -1,7 +1,8 @@ -package HarborAPI +package apilib +//User ... type User struct { - UserId int32 `json:"user_id,omitempty"` + UserID int32 `json:"user_id,omitempty"` Username string `json:"username,omitempty"` Email string `json:"email,omitempty"` Password string `json:"password,omitempty"` diff --git a/tests/apitests/hbapiaddprj_test.go b/tests/apitests/hbapiaddprj_test.go deleted file mode 100644 index 3ac82e1e8..000000000 --- a/tests/apitests/hbapiaddprj_test.go +++ /dev/null @@ -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) - //} - -} diff --git a/tests/apitests/hbapidelrpo_test.go b/tests/apitests/hbapidelrpo_test.go deleted file mode 100644 index 0777d79f4..000000000 --- a/tests/apitests/hbapidelrpo_test.go +++ /dev/null @@ -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) - //} - -} diff --git a/tests/apitests/hbapisearch_test.go b/tests/apitests/hbapisearch_test.go deleted file mode 100644 index b6a5544ae..000000000 --- a/tests/apitests/hbapisearch_test.go +++ /dev/null @@ -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) - //} - -} diff --git a/tests/docker-compose.test.yml b/tests/docker-compose.test.yml new file mode 100644 index 000000000..f804b085f --- /dev/null +++ b/tests/docker-compose.test.yml @@ -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 diff --git a/tests/testprepare.sh b/tests/testprepare.sh index 747ab3c93..671256451 100755 --- a/tests/testprepare.sh +++ b/tests/testprepare.sh @@ -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/. From 3d909bd0bdb32803c5828e2cd5b01060dc02c76b Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Wed, 10 Aug 2016 10:48:58 +0800 Subject: [PATCH 15/21] add UT for package utils --- api/search.go | 9 +- utils/registry/auth/auth_test.go | 9 -- utils/registry/auth/authorizer_test.go | 91 +++++++++++++++++++++ utils/registry/auth/credential_test.go | 67 +++++++++++++++ utils/registry/auth/tokenauthorizer.go | 4 +- utils/registry/auth/tokenauthorizer_test.go | 81 ++++++++++++++++++ utils/registry/error/error_test.go | 12 ++- utils/utils.go | 11 --- utils/utils_test.go | 41 +++++++++- 9 files changed, 297 insertions(+), 28 deletions(-) delete mode 100644 utils/registry/auth/auth_test.go create mode 100644 utils/registry/auth/authorizer_test.go create mode 100644 utils/registry/auth/credential_test.go create mode 100644 utils/registry/auth/tokenauthorizer_test.go diff --git a/api/search.go b/api/search.go index 4da0068c0..9b98caafa 100644 --- a/api/search.go +++ b/api/search.go @@ -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 diff --git a/utils/registry/auth/auth_test.go b/utils/registry/auth/auth_test.go deleted file mode 100644 index a5890bd69..000000000 --- a/utils/registry/auth/auth_test.go +++ /dev/null @@ -1,9 +0,0 @@ -package auth - -import ( - "testing" -) - -func TestMain(t *testing.T) { -} - diff --git a/utils/registry/auth/authorizer_test.go b/utils/registry/auth/authorizer_test.go new file mode 100644 index 000000000..cfebac3ea --- /dev/null +++ b/utils/registry/auth/authorizer_test.go @@ -0,0 +1,91 @@ +/* + 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" + "net/http/httptest" + "strings" + "testing" + + "github.com/docker/distribution/registry/client/auth" +) + +func TestNewAuthorizerStore(t *testing.T) { + server := newRegistryServer() + 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\"") + } +} + +func newRegistryServer() *httptest.Server { + mux := http.NewServeMux() + mux.HandleFunc("/v2/", handlePing) + + return httptest.NewServer(mux) +} + +func handlePing(w http.ResponseWriter, r *http.Request) { + challenge := "Bearer realm=\"https://auth.docker.io/token\",service=\"registry.docker.io\"" + w.Header().Set("Www-Authenticate", challenge) + w.WriteHeader(http.StatusUnauthorized) +} diff --git a/utils/registry/auth/credential_test.go b/utils/registry/auth/credential_test.go new file mode 100644 index 000000000..9c36691da --- /dev/null +++ b/utils/registry/auth/credential_test.go @@ -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) + } +} diff --git a/utils/registry/auth/tokenauthorizer.go b/utils/registry/auth/tokenauthorizer.go index 0abb72fb7..318ea6129 100644 --- a/utils/registry/auth/tokenauthorizer.go +++ b/utils/registry/auth/tokenauthorizer.go @@ -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 { diff --git a/utils/registry/auth/tokenauthorizer_test.go b/utils/registry/auth/tokenauthorizer_test.go new file mode 100644 index 000000000..6bf8cf34f --- /dev/null +++ b/utils/registry/auth/tokenauthorizer_test.go @@ -0,0 +1,81 @@ +/* + 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 ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +var ( + token = "token" +) + +func TestAuthorizeOfStandardTokenAuthorizer(t *testing.T) { + tokenServer := newTokenServer() + defer tokenServer.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": tokenServer.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") + } + +} + +func newTokenServer() *httptest.Server { + mux := http.NewServeMux() + mux.HandleFunc("/token", handleToken) + + return httptest.NewServer(mux) +} + +func handleToken(w http.ResponseWriter, r *http.Request) { + result := map[string]interface{}{} + result["token"] = token + result["expires_in"] = 300 + result["issued_at"] = time.Now().Format(time.RFC3339) + + encoder := json.NewEncoder(w) + if err := encoder.Encode(result); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } +} diff --git a/utils/registry/error/error_test.go b/utils/registry/error/error_test.go index 609e0ec3d..cfae15311 100644 --- a/utils/registry/error/error_test.go +++ b/utils/registry/error/error_test.go @@ -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") + } +} diff --git a/utils/utils.go b/utils/utils.go index 7fa1215e4..8b8dd7d8b 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -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) diff --git a/utils/utils_test.go b/utils/utils_test.go index d2ab63112..d151b8452 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -21,7 +21,36 @@ import ( "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) { @@ -64,6 +93,16 @@ func TestParseRepository(t *testing.T) { } } +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" From ff348be04852ed6afdb7f501e6ea9e43f226d675 Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Thu, 11 Aug 2016 14:02:53 +0800 Subject: [PATCH 16/21] 1. support policy deletion 2. bug fix: missing semicolon in js --- Deploy/db/registry.sql | 1 + api/replication_policy.go | 38 +++++++++++++++++++ dao/replication_job.go | 23 ++++++----- migration/changelog.md | 8 +++- models/replication_job.go | 1 + .../paginator/paginator.directive.js | 2 +- 6 files changed, 62 insertions(+), 11 deletions(-) diff --git a/Deploy/db/registry.sql b/Deploy/db/registry.sql index 121e27aae..aa7494155 100644 --- a/Deploy/db/registry.sql +++ b/Deploy/db/registry.sql @@ -110,6 +110,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, diff --git a/api/replication_policy.go b/api/replication_policy.go index be24aa0e9..633cb9356 100644 --- a/api/replication_policy.go +++ b/api/replication_policy.go @@ -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, "") + } +} diff --git a/dao/replication_job.go b/dao/replication_job.go index 5e9f8324e..3d0f46c40 100644 --- a/dao/replication_job.go +++ b/dao/replication_job.go @@ -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.delete = 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 } diff --git a/migration/changelog.md b/migration/changelog.md index 888aeb5a6..b1f86f04f 100644 --- a/migration/changelog.md +++ b/migration/changelog.md @@ -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` diff --git a/models/replication_job.go b/models/replication_job.go index 8d847f9b0..6a76dcd03 100644 --- a/models/replication_job.go +++ b/models/replication_job.go @@ -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 ... diff --git a/static/resources/js/components/paginator/paginator.directive.js b/static/resources/js/components/paginator/paginator.directive.js index 7a091978a..d9e08be0b 100644 --- a/static/resources/js/components/paginator/paginator.directive.js +++ b/static/resources/js/components/paginator/paginator.directive.js @@ -78,7 +78,7 @@ this.time = 0; this.minimum = 0; this.maximum = 0; - } + }; TimeCounter.prototype.setMaximum = function(maximum) { this.maximum = maximum; From 60cae7357fecab45c15ccf28a6bf27a1c2f9bc8f Mon Sep 17 00:00:00 2001 From: yhua Date: Mon, 15 Aug 2016 10:21:29 +0800 Subject: [PATCH 17/21] add new structs from swagger auto gen code --- api/search_test.go | 4 +- tests/apitests/apilib/access_log.go | 41 +++++++++++ tests/apitests/apilib/access_log_filter.go | 38 +++++++++++ tests/apitests/apilib/accesslog.go | 24 ------- tests/apitests/apilib/harborlogout.bak | 15 ++++ tests/apitests/apilib/harlogin.bak | 28 ++++++++ tests/apitests/apilib/job_status.go | 50 ++++++++++++++ tests/apitests/apilib/password.go | 25 ++++++- tests/apitests/apilib/project.go | 68 ++++++++++++++++--- tests/apitests/apilib/projecttemp4search.go | 8 --- tests/apitests/apilib/rep_policy.go | 62 +++++++++++++++++ .../apilib/rep_policy_enablement_req.go | 29 ++++++++ tests/apitests/apilib/rep_policy_post.go | 35 ++++++++++ tests/apitests/apilib/rep_policy_update.go | 41 +++++++++++ tests/apitests/apilib/rep_target.go | 50 ++++++++++++++ tests/apitests/apilib/rep_target_post.go | 38 +++++++++++ tests/apitests/apilib/repository.go | 59 ++++++++++++---- .../apitests/apilib/repositorytemp4search.go | 12 ---- tests/apitests/apilib/role.go | 31 ++++++++- tests/apitests/apilib/role_param.go | 32 +++++++++ tests/apitests/apilib/roleparam.go | 7 -- tests/apitests/apilib/search.go | 33 +++++++-- tests/apitests/apilib/search_project.go | 35 ++++++++++ tests/apitests/apilib/search_repository.go | 38 +++++++++++ tests/apitests/apilib/statistic_map.go | 44 ++++++++++++ tests/apitests/apilib/tags.go | 29 ++++++++ tests/apitests/apilib/top_repo.go | 32 +++++++++ tests/apitests/apilib/user.go | 39 +++++++++-- 28 files changed, 856 insertions(+), 91 deletions(-) create mode 100644 tests/apitests/apilib/access_log.go create mode 100644 tests/apitests/apilib/access_log_filter.go delete mode 100644 tests/apitests/apilib/accesslog.go create mode 100644 tests/apitests/apilib/harborlogout.bak create mode 100644 tests/apitests/apilib/harlogin.bak create mode 100644 tests/apitests/apilib/job_status.go delete mode 100644 tests/apitests/apilib/projecttemp4search.go create mode 100644 tests/apitests/apilib/rep_policy.go create mode 100644 tests/apitests/apilib/rep_policy_enablement_req.go create mode 100644 tests/apitests/apilib/rep_policy_post.go create mode 100644 tests/apitests/apilib/rep_policy_update.go create mode 100644 tests/apitests/apilib/rep_target.go create mode 100644 tests/apitests/apilib/rep_target_post.go delete mode 100644 tests/apitests/apilib/repositorytemp4search.go create mode 100644 tests/apitests/apilib/role_param.go delete mode 100644 tests/apitests/apilib/roleparam.go create mode 100644 tests/apitests/apilib/search_project.go create mode 100644 tests/apitests/apilib/search_repository.go create mode 100644 tests/apitests/apilib/statistic_map.go create mode 100644 tests/apitests/apilib/tags.go create mode 100644 tests/apitests/apilib/top_repo.go diff --git a/api/search_test.go b/api/search_test.go index 529a87025..1eecd1009 100644 --- a/api/search_test.go +++ b/api/search_test.go @@ -20,8 +20,8 @@ func TestSearch(t *testing.T) { t.Error("Error while search project or repository", err.Error()) t.Log(err) } else { - assert.Equal(result.Projects[0].ProjectID, int32(1), "Project id should be equal") - assert.Equal(result.Projects[0].ProjectName, "library", "Project name should be library") + 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) } diff --git a/tests/apitests/apilib/access_log.go b/tests/apitests/apilib/access_log.go new file mode 100644 index 000000000..aea708ff1 --- /dev/null +++ b/tests/apitests/apilib/access_log.go @@ -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"` +} diff --git a/tests/apitests/apilib/access_log_filter.go b/tests/apitests/apilib/access_log_filter.go new file mode 100644 index 000000000..9c6b87296 --- /dev/null +++ b/tests/apitests/apilib/access_log_filter.go @@ -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"` +} diff --git a/tests/apitests/apilib/accesslog.go b/tests/apitests/apilib/accesslog.go deleted file mode 100644 index 4c25b33a7..000000000 --- a/tests/apitests/apilib/accesslog.go +++ /dev/null @@ -1,24 +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 apilib - -//AccessLog for go test -type AccessLog struct { - Username string `json:"username,omitempty"` - Keywords string `json:"keywords,omitempty"` - BeginTimestamp int32 `json:"beginTimestamp,omitempty"` - EndTimestamp int32 `json:"endTimestamp,omitempty"` -} diff --git a/tests/apitests/apilib/harborlogout.bak b/tests/apitests/apilib/harborlogout.bak new file mode 100644 index 000000000..fa59ee2bb --- /dev/null +++ b/tests/apitests/apilib/harborlogout.bak @@ -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 +} diff --git a/tests/apitests/apilib/harlogin.bak b/tests/apitests/apilib/harlogin.bak new file mode 100644 index 000000000..d711103bb --- /dev/null +++ b/tests/apitests/apilib/harlogin.bak @@ -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 +} diff --git a/tests/apitests/apilib/job_status.go b/tests/apitests/apilib/job_status.go new file mode 100644 index 000000000..62826636f --- /dev/null +++ b/tests/apitests/apilib/job_status.go @@ -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"` +} diff --git a/tests/apitests/apilib/password.go b/tests/apitests/apilib/password.go index dca6a48a1..f791cd056 100644 --- a/tests/apitests/apilib/password.go +++ b/tests/apitests/apilib/password.go @@ -1,8 +1,27 @@ +/* + * 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 -import () - -//Password for go test type Password struct { // The user's existing password. diff --git a/tests/apitests/apilib/project.go b/tests/apitests/apilib/project.go index dc0ffe523..0026ca0f9 100644 --- a/tests/apitests/apilib/project.go +++ b/tests/apitests/apilib/project.go @@ -1,16 +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 -import () - -//Project for go test 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"` } diff --git a/tests/apitests/apilib/projecttemp4search.go b/tests/apitests/apilib/projecttemp4search.go deleted file mode 100644 index 4b80cc045..000000000 --- a/tests/apitests/apilib/projecttemp4search.go +++ /dev/null @@ -1,8 +0,0 @@ -package apilib - -//Project4Search ... -type Project4Search struct { - ProjectID int32 `json:"id,omitempty"` - ProjectName string `json:"name,omitempty"` - Public int32 `json:"public,omitempty"` -} diff --git a/tests/apitests/apilib/rep_policy.go b/tests/apitests/apilib/rep_policy.go new file mode 100644 index 000000000..16e7edd82 --- /dev/null +++ b/tests/apitests/apilib/rep_policy.go @@ -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"` +} diff --git a/tests/apitests/apilib/rep_policy_enablement_req.go b/tests/apitests/apilib/rep_policy_enablement_req.go new file mode 100644 index 000000000..11574f645 --- /dev/null +++ b/tests/apitests/apilib/rep_policy_enablement_req.go @@ -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"` +} diff --git a/tests/apitests/apilib/rep_policy_post.go b/tests/apitests/apilib/rep_policy_post.go new file mode 100644 index 000000000..6b8d42731 --- /dev/null +++ b/tests/apitests/apilib/rep_policy_post.go @@ -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"` +} diff --git a/tests/apitests/apilib/rep_policy_update.go b/tests/apitests/apilib/rep_policy_update.go new file mode 100644 index 000000000..fe57bf20e --- /dev/null +++ b/tests/apitests/apilib/rep_policy_update.go @@ -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"` +} diff --git a/tests/apitests/apilib/rep_target.go b/tests/apitests/apilib/rep_target.go new file mode 100644 index 000000000..a7bf4ac29 --- /dev/null +++ b/tests/apitests/apilib/rep_target.go @@ -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"` +} diff --git a/tests/apitests/apilib/rep_target_post.go b/tests/apitests/apilib/rep_target_post.go new file mode 100644 index 000000000..f2932667c --- /dev/null +++ b/tests/apitests/apilib/rep_target_post.go @@ -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"` +} diff --git a/tests/apitests/apilib/repository.go b/tests/apitests/apilib/repository.go index 1673d83e4..d6cbe8d44 100644 --- a/tests/apitests/apilib/repository.go +++ b/tests/apitests/apilib/repository.go @@ -1,17 +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 -import ( - "time" -) - -//Repository ... 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"` } diff --git a/tests/apitests/apilib/repositorytemp4search.go b/tests/apitests/apilib/repositorytemp4search.go deleted file mode 100644 index e438fcef6..000000000 --- a/tests/apitests/apilib/repositorytemp4search.go +++ /dev/null @@ -1,12 +0,0 @@ -package apilib - -import () - -//Repository4Search ... -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"` -} - diff --git a/tests/apitests/apilib/role.go b/tests/apitests/apilib/role.go index 1b18eca2e..9025fd65b 100644 --- a/tests/apitests/apilib/role.go +++ b/tests/apitests/apilib/role.go @@ -1,8 +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 -//Role ... 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"` } diff --git a/tests/apitests/apilib/role_param.go b/tests/apitests/apilib/role_param.go new file mode 100644 index 000000000..aa022792c --- /dev/null +++ b/tests/apitests/apilib/role_param.go @@ -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"` +} diff --git a/tests/apitests/apilib/roleparam.go b/tests/apitests/apilib/roleparam.go deleted file mode 100644 index b91ac05e4..000000000 --- a/tests/apitests/apilib/roleparam.go +++ /dev/null @@ -1,7 +0,0 @@ -package apilib - -//RoleParam ... -type RoleParam struct { - Roles []int32 `json:"roles,omitempty"` - UserName string `json:"user_name,omitempty"` -} diff --git a/tests/apitests/apilib/search.go b/tests/apitests/apilib/search.go index ae7610f58..d12388469 100644 --- a/tests/apitests/apilib/search.go +++ b/tests/apitests/apilib/search.go @@ -1,9 +1,34 @@ +/* + * 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 -import () +import() -//Search ... 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"` } diff --git a/tests/apitests/apilib/search_project.go b/tests/apitests/apilib/search_project.go new file mode 100644 index 000000000..4f803253c --- /dev/null +++ b/tests/apitests/apilib/search_project.go @@ -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"` +} diff --git a/tests/apitests/apilib/search_repository.go b/tests/apitests/apilib/search_repository.go new file mode 100644 index 000000000..7961422a4 --- /dev/null +++ b/tests/apitests/apilib/search_repository.go @@ -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"` +} diff --git a/tests/apitests/apilib/statistic_map.go b/tests/apitests/apilib/statistic_map.go new file mode 100644 index 000000000..ec89e79a1 --- /dev/null +++ b/tests/apitests/apilib/statistic_map.go @@ -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"` +} diff --git a/tests/apitests/apilib/tags.go b/tests/apitests/apilib/tags.go new file mode 100644 index 000000000..53dcfc31a --- /dev/null +++ b/tests/apitests/apilib/tags.go @@ -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"` +} diff --git a/tests/apitests/apilib/top_repo.go b/tests/apitests/apilib/top_repo.go new file mode 100644 index 000000000..9cea0726e --- /dev/null +++ b/tests/apitests/apilib/top_repo.go @@ -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"` +} diff --git a/tests/apitests/apilib/user.go b/tests/apitests/apilib/user.go index a514c1650..ccd6522a3 100644 --- a/tests/apitests/apilib/user.go +++ b/tests/apitests/apilib/user.go @@ -1,12 +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 -//User ... 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"` } From 8302892bfb379e4ccc5cd50343bb067b0d24b9f7 Mon Sep 17 00:00:00 2001 From: Tan Jiang Date: Wed, 17 Aug 2016 16:17:17 +0800 Subject: [PATCH 18/21] jsminify's basedir should be configurable --- Deploy/jsminify.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Deploy/jsminify.sh b/Deploy/jsminify.sh index fde2e8274..37808d98c 100755 --- a/Deploy/jsminify.sh +++ b/Deploy/jsminify.sh @@ -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..." From 2d91b8c3fdb44afac139299bc61458b9cdc92ac7 Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Thu, 18 Aug 2016 11:43:45 +0800 Subject: [PATCH 19/21] bug fix for policy deletion: wrong column name --- dao/dao_test.go | 2 +- dao/replication_job.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dao/dao_test.go b/dao/dao_test.go index 2a8f5e47a..fc45fe672 100644 --- a/dao/dao_test.go +++ b/dao/dao_test.go @@ -1376,7 +1376,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) } } diff --git a/dao/replication_job.go b/dao/replication_job.go index 3d0f46c40..2718eaea8 100644 --- a/dao/replication_job.go +++ b/dao/replication_job.go @@ -156,7 +156,7 @@ func FilterRepPolicies(name string, projectID int64) ([]*models.RepPolicy, error 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") - where rp.delete = 0 ` + where rp.deleted = 0 ` if len(name) != 0 && projectID != 0 { sql += `and rp.name like ? and rp.project_id = ? ` From 7538bbf0668ad04a0574adf1fe52e212e71af3ed Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Tue, 16 Aug 2016 13:45:59 +0800 Subject: [PATCH 20/21] UT for package utils --- Deploy/coverage4gotest.sh | 2 +- utils/log/level.go | 3 +- utils/log/level_test.go | 61 +++ utils/log/log_test.go | 9 - utils/log/logger_test.go | 156 +++++++ utils/registry/auth/authorizer_test.go | 28 +- utils/registry/auth/tokenauthorizer_test.go | 52 +-- utils/registry/manifest_test.go | 56 +++ utils/registry/registry.go | 14 +- utils/registry/registry_test.go | 150 ++++++ utils/registry/repository.go | 29 +- utils/registry/repository_test.go | 494 ++++++++++++++------ utils/registry/transport_test.go | 60 +++ utils/test/test.go | 88 ++++ 14 files changed, 977 insertions(+), 225 deletions(-) create mode 100644 utils/log/level_test.go delete mode 100644 utils/log/log_test.go create mode 100644 utils/log/logger_test.go create mode 100644 utils/registry/manifest_test.go create mode 100644 utils/registry/registry_test.go create mode 100644 utils/registry/transport_test.go create mode 100644 utils/test/test.go diff --git a/Deploy/coverage4gotest.sh b/Deploy/coverage4gotest.sh index dadbf256f..8fb49b38f 100755 --- a/Deploy/coverage4gotest.sh +++ b/Deploy/coverage4gotest.sh @@ -1,5 +1,5 @@ #!/bin/bash - +set -e echo "mode: set" >>profile.cov for dir in $(go list ./... | grep -v -E 'vendor|tests') do diff --git a/utils/log/level.go b/utils/log/level.go index 84bfc394f..fa39ea075 100644 --- a/utils/log/level.go +++ b/utils/log/level.go @@ -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": diff --git a/utils/log/level_test.go b/utils/log/level_test.go new file mode 100644 index 000000000..e80adff67 --- /dev/null +++ b/utils/log/level_test.go @@ -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") + } +} diff --git a/utils/log/log_test.go b/utils/log/log_test.go deleted file mode 100644 index 9f93ee776..000000000 --- a/utils/log/log_test.go +++ /dev/null @@ -1,9 +0,0 @@ -package log - -import ( - "testing" -) - -func TestMain(t *testing.T) { -} - diff --git a/utils/log/logger_test.go b/utils/log/logger_test.go new file mode 100644 index 000000000..7b2e6de18 --- /dev/null +++ b/utils/log/logger_test.go @@ -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) +} diff --git a/utils/registry/auth/authorizer_test.go b/utils/registry/auth/authorizer_test.go index cfebac3ea..ffc2944e1 100644 --- a/utils/registry/auth/authorizer_test.go +++ b/utils/registry/auth/authorizer_test.go @@ -17,15 +17,26 @@ package auth import ( "net/http" - "net/http/httptest" "strings" "testing" "github.com/docker/distribution/registry/client/auth" + "github.com/vmware/harbor/utils/test" ) func TestNewAuthorizerStore(t *testing.T) { - server := newRegistryServer() + 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) @@ -76,16 +87,3 @@ func TestModify(t *testing.T) { t.Fatal("\"Authorization\" header does not start with \"Bearer\"") } } - -func newRegistryServer() *httptest.Server { - mux := http.NewServeMux() - mux.HandleFunc("/v2/", handlePing) - - return httptest.NewServer(mux) -} - -func handlePing(w http.ResponseWriter, r *http.Request) { - challenge := "Bearer realm=\"https://auth.docker.io/token\",service=\"registry.docker.io\"" - w.Header().Set("Www-Authenticate", challenge) - w.WriteHeader(http.StatusUnauthorized) -} diff --git a/utils/registry/auth/tokenauthorizer_test.go b/utils/registry/auth/tokenauthorizer_test.go index 6bf8cf34f..da601ba48 100644 --- a/utils/registry/auth/tokenauthorizer_test.go +++ b/utils/registry/auth/tokenauthorizer_test.go @@ -16,20 +16,29 @@ package auth import ( - "encoding/json" "net/http" - "net/http/httptest" "testing" - "time" -) -var ( - token = "token" + "github.com/vmware/harbor/utils/test" ) func TestAuthorizeOfStandardTokenAuthorizer(t *testing.T) { - tokenServer := newTokenServer() - defer tokenServer.Close() + 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) @@ -38,7 +47,7 @@ func TestAuthorizeOfStandardTokenAuthorizer(t *testing.T) { } params := map[string]string{ - "realm": tokenServer.URL + "/token", + "realm": server.URL + "/token", } if err := authorizer.Authorize(req, params); err != nil { @@ -46,8 +55,8 @@ func TestAuthorizeOfStandardTokenAuthorizer(t *testing.T) { } tk := req.Header.Get("Authorization") - if tk != "Bearer "+token { - t.Errorf("unexpected token: %s != %s", tk, "Bearer "+token) + if tk != "Bearer token" { + t.Errorf("unexpected token: %s != %s", tk, "Bearer token") } } @@ -58,24 +67,3 @@ func TestSchemeOfStandardTokenAuthorizer(t *testing.T) { } } - -func newTokenServer() *httptest.Server { - mux := http.NewServeMux() - mux.HandleFunc("/token", handleToken) - - return httptest.NewServer(mux) -} - -func handleToken(w http.ResponseWriter, r *http.Request) { - result := map[string]interface{}{} - result["token"] = token - result["expires_in"] = 300 - result["issued_at"] = time.Now().Format(time.RFC3339) - - encoder := json.NewEncoder(w) - if err := encoder.Encode(result); err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return - } -} diff --git a/utils/registry/manifest_test.go b/utils/registry/manifest_test.go new file mode 100644 index 000000000..66c7a6abc --- /dev/null +++ b/utils/registry/manifest_test.go @@ -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) + } +} diff --git a/utils/registry/registry.go b/utils/registry/registry.go index 0ef6e9dc9..4934c8e8d 100644 --- a/utils/registry/registry.go +++ b/utils/registry/registry.go @@ -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 ... diff --git a/utils/registry/registry_test.go b/utils/registry/registry_test.go new file mode 100644 index 000000000..ce9853db2 --- /dev/null +++ b/utils/registry/registry_test.go @@ -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{}) +} diff --git a/utils/registry/repository.go b/utils/registry/repository.go index 5ae2eb9d7..88d0182b3 100644 --- a/utils/registry/repository.go +++ b/utils/registry/repository.go @@ -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) } diff --git a/utils/registry/repository_test.go b/utils/registry/repository_test.go index f1c4c3c2d..07c837b53 100644 --- a/utils/registry/repository_test.go +++ b/utils/registry/repository_test.go @@ -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: ®istry_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{}) } diff --git a/utils/registry/transport_test.go b/utils/registry/transport_test.go new file mode 100644 index 000000000..997ca3c84 --- /dev/null +++ b/utils/registry/transport_test.go @@ -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") + } + +} diff --git a/utils/test/test.go b/utils/test/test.go new file mode 100644 index 000000000..37a435c24 --- /dev/null +++ b/utils/test/test.go @@ -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) +} From c546c3a48b8c882a90a51e846320a2c61ca2cd2a Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Tue, 23 Aug 2016 15:56:30 +0800 Subject: [PATCH 21/21] support project deletion --- Deploy/db/registry.sql | 4 ++- api/project.go | 78 +++++++++++++++++++++++++++++++++++++++--- api/repository.go | 21 ++---------- api/utils.go | 33 ++++++++++++++++++ dao/dao_test.go | 34 ++++++++++++++++++ dao/project.go | 9 +++++ 6 files changed, 154 insertions(+), 25 deletions(-) diff --git a/Deploy/db/registry.sql b/Deploy/db/registry.sql index aa7494155..deb15603e 100644 --- a/Deploy/db/registry.sql +++ b/Deploy/db/registry.sql @@ -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, diff --git a/api/project.go b/api/project.go index 8ea705aa5..517c37157 100644 --- a/api/project.go +++ b/api/project.go @@ -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 projectList []models.Project diff --git a/api/repository.go b/api/repository.go index 646a55dfb..8bf1eece7 100644 --- a/api/repository.go +++ b/api/repository.go @@ -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 { diff --git a/api/utils.go b/api/utils.go index 4fa661932..2cd8a2212 100644 --- a/api/utils.go +++ b/api/utils.go @@ -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" ) @@ -259,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() +} diff --git a/dao/dao_test.go b/dao/dao_test.go index fc45fe672..67f436340 100644 --- a/dao/dao_test.go +++ b/dao/dao_test.go @@ -16,6 +16,7 @@ package dao import ( + "fmt" "os" "testing" "time" @@ -1458,3 +1459,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) + } + +} diff --git a/dao/project.go b/dao/project.go index b8a158692..7e582fcb6 100644 --- a/dao/project.go +++ b/dao/project.go @@ -245,3 +245,12 @@ func getProjects(public int, projectName string) ([]models.Project, error) { } return projects, nil } + +// 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 +}