Merge remote-tracking branch 'upstream/new-ui-with-sync-image' into author

This commit is contained in:
wemeya 2016-06-23 15:54:50 +08:00
commit c4734205e4
57 changed files with 1082 additions and 491 deletions

View File

@ -93,8 +93,8 @@ create table access_log (
log_id int NOT NULL AUTO_INCREMENT, log_id int NOT NULL AUTO_INCREMENT,
user_id int NOT NULL, user_id int NOT NULL,
project_id int NOT NULL, project_id int NOT NULL,
repo_name varchar (40), repo_name varchar (256),
repo_tag varchar (20), repo_tag varchar (128),
GUID varchar(64), GUID varchar(64),
operation varchar(20) NOT NULL, operation varchar(20) NOT NULL,
op_time timestamp, op_time timestamp,
@ -159,4 +159,4 @@ CREATE TABLE IF NOT EXISTS `alembic_version` (
`version_num` varchar(32) NOT NULL `version_num` varchar(32) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8; ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into alembic_version values ('0.1.1'); insert into alembic_version values ('0.2.0');

View File

@ -38,6 +38,10 @@ self_registration = on
#Number of job workers in job service, default is 3 #Number of job workers in job service, default is 3
max_job_workers = 3 max_job_workers = 3
#Toggle on and off to tell job service wheter or not verify the ssl cert
#when it tries to access a remote registry
verify_remote_cert = on
#Turn on or off the customize your certificate for registry's token. #Turn on or off the customize your certificate for registry's token.
#If the value is on, the prepare script will generate new root cert and private key #If the value is on, the prepare script will generate new root cert and private key
#for generating token to access the image in registry. #for generating token to access the image in registry.

View File

@ -47,6 +47,7 @@ crt_organizationalunit = rcp.get("configuration", "crt_organizationalunit")
crt_commonname = rcp.get("configuration", "crt_commonname") crt_commonname = rcp.get("configuration", "crt_commonname")
crt_email = rcp.get("configuration", "crt_email") crt_email = rcp.get("configuration", "crt_email")
max_job_workers = rcp.get("configuration", "max_job_workers") max_job_workers = rcp.get("configuration", "max_job_workers")
verify_remote_cert = rcp.get("configuration", "verify_remote_cert")
######## ########
ui_secret = ''.join(random.choice(string.ascii_letters+string.digits) for i in range(16)) ui_secret = ''.join(random.choice(string.ascii_letters+string.digits) for i in range(16))
@ -122,7 +123,8 @@ render(os.path.join(templates_dir, "jobservice", "env"),
db_password=db_password, db_password=db_password,
ui_secret=ui_secret, ui_secret=ui_secret,
max_job_workers=max_job_workers, max_job_workers=max_job_workers,
ui_url=ui_url) ui_url=ui_url,
verify_remote_cert=verify_remote_cert)
def validate_crt_subj(dirty_subj): def validate_crt_subj(dirty_subj):
subj_list = [item for item in dirty_subj.strip().split("/") \ subj_list = [item for item in dirty_subj.strip().split("/") \

View File

@ -3,7 +3,10 @@ MYSQL_PORT=3306
MYSQL_USR=root MYSQL_USR=root
MYSQL_PWD=$db_password MYSQL_PWD=$db_password
UI_SECRET=$ui_secret UI_SECRET=$ui_secret
HARBOR_URL=$ui_url REGISTRY_URL=http://registry:5000
VERIFY_REMOTE_CERT=$verify_remote_cert
MAX_JOB_WORKERS=$max_job_workers MAX_JOB_WORKERS=$max_job_workers
LOG_LEVEL=debug LOG_LEVEL=debug
GODEBUG=netdns=cgo GODEBUG=netdns=cgo
EXT_ENDPOINT=$ui_url
TOKEN_URL=http://ui

View File

@ -3,10 +3,11 @@ MYSQL_PORT=3306
MYSQL_USR=root MYSQL_USR=root
MYSQL_PWD=$db_password MYSQL_PWD=$db_password
REGISTRY_URL=http://registry:5000 REGISTRY_URL=http://registry:5000
UI_URL=http://ui
CONFIG_PATH=/etc/ui/app.conf CONFIG_PATH=/etc/ui/app.conf
HARBOR_REG_URL=$hostname HARBOR_REG_URL=$hostname
HARBOR_ADMIN_PASSWORD=$harbor_admin_password HARBOR_ADMIN_PASSWORD=$harbor_admin_password
HARBOR_URL=$hostname HARBOR_URL=$ui_url
AUTH_MODE=$auth_mode AUTH_MODE=$auth_mode
LDAP_URL=$ldap_url LDAP_URL=$ldap_url
LDAP_BASE_DN=$ldap_basedn LDAP_BASE_DN=$ldap_basedn
@ -14,3 +15,5 @@ UI_SECRET=$ui_secret
SELF_REGISTRATION=$self_registration SELF_REGISTRATION=$self_registration
LOG_LEVEL=debug LOG_LEVEL=debug
GODEBUG=netdns=cgo GODEBUG=netdns=cgo
EXT_ENDPOINT=$ui_url
TOKEN_URL=http://ui

View File

@ -165,7 +165,7 @@ func getRepoList(projectID int64) ([]string, error) {
uiPwd = "Harbor12345" uiPwd = "Harbor12345"
} }
*/ */
uiURL := config.LocalHarborURL() uiURL := config.LocalUIURL()
client := &http.Client{} client := &http.Client{}
req, err := http.NewRequest("GET", uiURL+"/api/repositories?project_id="+strconv.Itoa(int(projectID)), nil) req, err := http.NewRequest("GET", uiURL+"/api/repositories?project_id="+strconv.Itoa(int(projectID)), nil)
if err != nil { if err != nil {

View File

@ -159,7 +159,7 @@ func (p *ProjectAPI) List() {
if len(isPublic) > 0 { if len(isPublic) > 0 {
public, err = strconv.Atoi(isPublic) public, err = strconv.Atoi(isPublic)
if err != nil { if err != nil {
log.Errorf("Error parsing public property: %d, error: %v", isPublic, err) log.Errorf("Error parsing public property: %v, error: %v", isPublic, err)
p.CustomAbort(http.StatusBadRequest, "invalid project Id") p.CustomAbort(http.StatusBadRequest, "invalid project Id")
} }
} }

View File

@ -121,6 +121,16 @@ func (pa *RepPolicyAPI) Post() {
pa.CustomAbort(http.StatusBadRequest, fmt.Sprintf("target %d does not exist", policy.TargetID)) pa.CustomAbort(http.StatusBadRequest, fmt.Sprintf("target %d does not exist", policy.TargetID))
} }
policies, err := dao.GetRepPolicyByProjectAndTarget(policy.ProjectID, policy.TargetID)
if err != nil {
log.Errorf("failed to get policy [project ID: %d,targetID: %d]: %v", policy.ProjectID, policy.TargetID, err)
pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if len(policies) > 0 {
pa.CustomAbort(http.StatusConflict, "policy already exists with the same project and target")
}
pid, err := dao.AddRepPolicy(*policy) pid, err := dao.AddRepPolicy(*policy)
if err != nil { if err != nil {
log.Errorf("Failed to add policy to DB, error: %v", err) log.Errorf("Failed to add policy to DB, error: %v", err)
@ -159,6 +169,7 @@ func (pa *RepPolicyAPI) Put() {
policy.ProjectID = originalPolicy.ProjectID policy.ProjectID = originalPolicy.ProjectID
pa.Validate(policy) pa.Validate(policy)
// check duplicate name
if policy.Name != originalPolicy.Name { if policy.Name != originalPolicy.Name {
po, err := dao.GetRepPolicyByName(policy.Name) po, err := dao.GetRepPolicyByName(policy.Name)
if err != nil { if err != nil {
@ -172,6 +183,12 @@ func (pa *RepPolicyAPI) Put() {
} }
if policy.TargetID != originalPolicy.TargetID { if policy.TargetID != originalPolicy.TargetID {
//target of policy can not be modified when the policy is enabled
if originalPolicy.Enabled == 1 {
pa.CustomAbort(http.StatusBadRequest, "target of policy can not be modified when the policy is enabled")
}
// check the existance of target
target, err := dao.GetRepTarget(policy.TargetID) target, err := dao.GetRepTarget(policy.TargetID)
if err != nil { if err != nil {
log.Errorf("failed to get target %d: %v", policy.TargetID, err) log.Errorf("failed to get target %d: %v", policy.TargetID, err)
@ -181,10 +198,22 @@ func (pa *RepPolicyAPI) Put() {
if target == nil { if target == nil {
pa.CustomAbort(http.StatusBadRequest, fmt.Sprintf("target %d does not exist", policy.TargetID)) pa.CustomAbort(http.StatusBadRequest, fmt.Sprintf("target %d does not exist", policy.TargetID))
} }
// check duplicate policy with the same project and target
policies, err := dao.GetRepPolicyByProjectAndTarget(policy.ProjectID, policy.TargetID)
if err != nil {
log.Errorf("failed to get policy [project ID: %d,targetID: %d]: %v", policy.ProjectID, policy.TargetID, err)
pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if len(policies) > 0 {
pa.CustomAbort(http.StatusConflict, "policy already exists with the same project and target")
}
} }
policy.ID = id policy.ID = id
/*
isTargetChanged := !(policy.TargetID == originalPolicy.TargetID) isTargetChanged := !(policy.TargetID == originalPolicy.TargetID)
isEnablementChanged := !(policy.Enabled == policy.Enabled) isEnablementChanged := !(policy.Enabled == policy.Enabled)
@ -250,6 +279,22 @@ func (pa *RepPolicyAPI) Put() {
} }
}() }()
} }
*/
if err = dao.UpdateRepPolicy(policy); err != nil {
log.Errorf("failed to update policy %d: %v", id, err)
pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if policy.Enabled != originalPolicy.Enabled && policy.Enabled == 1 {
go func() {
if err := TriggerReplication(id, "", nil, models.RepOpTransfer); err != nil {
log.Errorf("failed to trigger replication of %d: %v", id, err)
} else {
log.Infof("replication of %d triggered", id)
}
}()
}
} }
type enablementReq struct { type enablementReq struct {

View File

@ -255,11 +255,13 @@ func (ra *RepositoryAPI) GetManifests() {
func (ra *RepositoryAPI) initRepositoryClient(repoName string) (r *registry.Repository, err error) { func (ra *RepositoryAPI) initRepositoryClient(repoName string) (r *registry.Repository, err error) {
endpoint := os.Getenv("REGISTRY_URL") endpoint := os.Getenv("REGISTRY_URL")
// TODO read variable from config file
insecure := true
username, password, ok := ra.Ctx.Request.BasicAuth() username, password, ok := ra.Ctx.Request.BasicAuth()
if ok { if ok {
credential := auth.NewBasicAuthCredential(username, password) return newRepositoryClient(endpoint, insecure, username, password,
return registry.NewRepositoryWithCredential(repoName, endpoint, credential) repoName, "repository", repoName, "pull", "push", "*")
} }
username, err = ra.getUsername() username, err = ra.getUsername()
@ -267,7 +269,8 @@ func (ra *RepositoryAPI) initRepositoryClient(repoName string) (r *registry.Repo
return nil, err return nil, err
} }
return registry.NewRepositoryWithUsername(repoName, endpoint, username) return cache.NewRepositoryClient(endpoint, insecure, username, repoName,
"repository", repoName, "pull", "push", "*")
} }
func (ra *RepositoryAPI) getUsername() (string, error) { func (ra *RepositoryAPI) getUsername() (string, error) {
@ -327,3 +330,21 @@ func (ra *RepositoryAPI) GetTopRepos() {
ra.Data["json"] = repos ra.Data["json"] = repos
ra.ServeJSON() ra.ServeJSON()
} }
func newRepositoryClient(endpoint string, insecure bool, username, password, repository, scopeType, scopeName string,
scopeActions ...string) (*registry.Repository, error) {
credential := auth.NewBasicAuthCredential(username, password)
authorizer := auth.NewStandardTokenAuthorizer(credential, insecure, scopeType, scopeName, scopeActions...)
store, err := auth.NewAuthorizerStore(endpoint, insecure, authorizer)
if err != nil {
return nil, err
}
client, err := registry.NewRepositoryWithModifiers(repository, endpoint, insecure, store)
if err != nil {
return nil, err
}
return client, nil
}

View File

@ -26,7 +26,7 @@ import (
"github.com/vmware/harbor/models" "github.com/vmware/harbor/models"
"github.com/vmware/harbor/utils" "github.com/vmware/harbor/utils"
"github.com/vmware/harbor/utils/log" "github.com/vmware/harbor/utils/log"
registry_util "github.com/vmware/harbor/utils/registry" "github.com/vmware/harbor/utils/registry"
"github.com/vmware/harbor/utils/registry/auth" "github.com/vmware/harbor/utils/registry/auth"
registry_error "github.com/vmware/harbor/utils/registry/error" registry_error "github.com/vmware/harbor/utils/registry/error"
) )
@ -92,8 +92,10 @@ func (t *TargetAPI) Ping() {
password = t.GetString("password") password = t.GetString("password")
} }
credential := auth.NewBasicAuthCredential(username, password) // TODO read variable from config file
registry, err := registry_util.NewRegistryWithCredential(endpoint, credential) insecure := true
registry, err := newRegistryClient(endpoint, insecure, username, password,
"", "", "")
if err != nil { if err != nil {
// timeout, dns resolve error, connection refused, etc. // timeout, dns resolve error, connection refused, etc.
if urlErr, ok := err.(*url.Error); ok { if urlErr, ok := err.(*url.Error); ok {
@ -190,6 +192,16 @@ func (t *TargetAPI) Post() {
t.CustomAbort(http.StatusConflict, "name is already used") t.CustomAbort(http.StatusConflict, "name is already used")
} }
ta, err = dao.GetRepTargetByConnInfo(target.URL, target.Username)
if err != nil {
log.Errorf("failed to get target [ %s %s ]: %v", target.URL, target.Username, err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if ta != nil {
t.CustomAbort(http.StatusConflict, "the connection information[ endpoint, username ] is conflict with other target")
}
if len(target.Password) != 0 { if len(target.Password) != 0 {
target.Password = utils.ReversibleEncrypt(target.Password) target.Password = utils.ReversibleEncrypt(target.Password)
} }
@ -217,6 +229,24 @@ func (t *TargetAPI) Put() {
t.CustomAbort(http.StatusNotFound, http.StatusText(http.StatusNotFound)) t.CustomAbort(http.StatusNotFound, http.StatusText(http.StatusNotFound))
} }
policies, err := dao.GetRepPolicyByTarget(id)
if err != nil {
log.Errorf("failed to get policies according target %d: %v", id, err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
hasEnabledPolicy := false
for _, policy := range policies {
if policy.Enabled == 1 {
hasEnabledPolicy = true
break
}
}
if hasEnabledPolicy {
t.CustomAbort(http.StatusBadRequest, "the target is associated with policy which is enabled")
}
target := &models.RepTarget{} target := &models.RepTarget{}
t.DecodeJSONReqAndValidate(target) t.DecodeJSONReqAndValidate(target)
@ -232,6 +262,18 @@ func (t *TargetAPI) Put() {
} }
} }
if target.URL != originalTarget.URL || target.Username != originalTarget.Username {
ta, err := dao.GetRepTargetByConnInfo(target.URL, target.Username)
if err != nil {
log.Errorf("failed to get target [ %s %s ]: %v", target.URL, target.Username, err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if ta != nil {
t.CustomAbort(http.StatusConflict, "the connection information[ endpoint, username ] is conflict with other target")
}
}
target.ID = id target.ID = id
if len(target.Password) != 0 { if len(target.Password) != 0 {
@ -273,3 +315,44 @@ func (t *TargetAPI) Delete() {
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
} }
} }
func newRegistryClient(endpoint string, insecure bool, username, password, scopeType, scopeName string,
scopeActions ...string) (*registry.Registry, error) {
credential := auth.NewBasicAuthCredential(username, password)
authorizer := auth.NewStandardTokenAuthorizer(credential, insecure, scopeType, scopeName, scopeActions...)
store, err := auth.NewAuthorizerStore(endpoint, insecure, authorizer)
if err != nil {
return nil, err
}
client, err := registry.NewRegistryWithModifiers(endpoint, insecure, store)
if err != nil {
return nil, err
}
return client, nil
}
// ListPolicies ...
func (t *TargetAPI) ListPolicies() {
id := t.GetIDFromURL()
target, err := dao.GetRepTarget(id)
if err != nil {
log.Errorf("failed to get target %d: %v", id, err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if target == nil {
t.CustomAbort(http.StatusNotFound, http.StatusText(http.StatusNotFound))
}
policies, err := dao.GetRepPolicyByTarget(id)
if err != nil {
log.Errorf("failed to get policies according target %d: %v", id, err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
t.Data["json"] = policies
t.ServeJSON()
}

32
controllers/addnew.go Normal file
View File

@ -0,0 +1,32 @@
package controllers
import (
"net/http"
"github.com/vmware/harbor/dao"
"github.com/vmware/harbor/utils/log"
)
// AddNewController handles requests to /add_new
type AddNewController struct {
BaseController
}
// Get renders the add new page
func (anc *AddNewController) Get() {
sessionUserID := anc.GetSession("userId")
anc.Data["AddNew"] = false
if sessionUserID != nil {
isAdmin, err := dao.IsAdminRole(sessionUserID.(int))
if err != nil {
log.Errorf("Error occurred in IsAdminRole: %v", err)
anc.CustomAbort(http.StatusInternalServerError, "")
}
if isAdmin && anc.AuthMode == "db_auth" {
anc.Data["AddNew"] = true
anc.Forward("Add User", "sign-up.htm")
return
}
}
anc.CustomAbort(http.StatusUnauthorized, "Status Unauthorized.")
}

View File

@ -18,6 +18,8 @@ func (omc *OptionalMenuController) Get() {
sessionUserID := omc.GetSession("userId") sessionUserID := omc.GetSession("userId")
var hasLoggedIn bool var hasLoggedIn bool
var allowAddNew bool
if sessionUserID != nil { if sessionUserID != nil {
hasLoggedIn = true hasLoggedIn = true
userID := sessionUserID.(int) userID := sessionUserID.(int)
@ -31,7 +33,18 @@ func (omc *OptionalMenuController) Get() {
omc.CustomAbort(http.StatusUnauthorized, "") omc.CustomAbort(http.StatusUnauthorized, "")
} }
omc.Data["Username"] = u.Username omc.Data["Username"] = u.Username
isAdmin, err := dao.IsAdminRole(sessionUserID.(int))
if err != nil {
log.Errorf("Error occurred in IsAdminRole: %v", err)
omc.CustomAbort(http.StatusInternalServerError, "")
} }
if isAdmin && omc.AuthMode == "db_auth" {
allowAddNew = true
}
}
omc.Data["AddNew"] = allowAddNew
omc.Data["HasLoggedIn"] = hasLoggedIn omc.Data["HasLoggedIn"] = hasLoggedIn
omc.TplName = "optional-menu.htm" omc.TplName = "optional-menu.htm"
omc.Render() omc.Render()

View File

@ -32,6 +32,7 @@ func (sic *SignInController) Get() {
} }
username = u.Username username = u.Username
} }
sic.Data["AuthMode"] = sic.AuthMode
sic.Data["Username"] = username sic.Data["Username"] = username
sic.Data["HasLoggedIn"] = hasLoggedIn sic.Data["HasLoggedIn"] = hasLoggedIn
sic.TplName = "sign-in.htm" sic.TplName = "sign-in.htm"

View File

@ -1,5 +1,9 @@
package controllers package controllers
import (
"net/http"
)
// SignUpController handles requests to /sign_up // SignUpController handles requests to /sign_up
type SignUpController struct { type SignUpController struct {
BaseController BaseController
@ -7,5 +11,9 @@ type SignUpController struct {
// Get renders sign up page // Get renders sign up page
func (suc *SignUpController) Get() { func (suc *SignUpController) Get() {
if suc.AuthMode != "db_auth" {
suc.CustomAbort(http.StatusUnauthorized, "Status unauthorized.")
}
suc.Data["AddNew"] = false
suc.Forward("Sign Up", "sign-up.htm") suc.Forward("Sign Up", "sign-up.htm")
} }

View File

@ -926,6 +926,21 @@ func TestGetRepPolicyByTarget(t *testing.T) {
} }
} }
func TestGetRepPolicyByProjectAndTarget(t *testing.T) {
policies, err := GetRepPolicyByProjectAndTarget(1, targetID)
if err != nil {
t.Fatalf("failed to get policy according project %d and target %d: %v", 1, targetID, err)
}
if len(policies) == 0 {
t.Fatal("unexpected length of policies 0, expected is >0")
}
if policies[0].ID != policyID {
t.Fatalf("unexpected policy: %d, expected: %d", policies[0].ID, policyID)
}
}
func TestGetRepPolicyByName(t *testing.T) { func TestGetRepPolicyByName(t *testing.T) {
policy, err := GetRepPolicy(policyID) policy, err := GetRepPolicy(policyID)
if err != nil { if err != nil {

View File

@ -52,6 +52,20 @@ func GetRepTargetByName(name string) (*models.RepTarget, error) {
return &t, err return &t, err
} }
// GetRepTargetByConnInfo ...
func GetRepTargetByConnInfo(endpoint, username string) (*models.RepTarget, error) {
o := GetOrmer()
t := models.RepTarget{
URL: endpoint,
Username: username,
}
err := o.Read(&t, "URL", "Username")
if err == orm.ErrNoRows {
return nil, nil
}
return &t, err
}
// DeleteRepTarget ... // DeleteRepTarget ...
func DeleteRepTarget(id int64) error { func DeleteRepTarget(id int64) error {
o := GetOrmer() o := GetOrmer()
@ -206,6 +220,20 @@ func GetRepPolicyByTarget(targetID int64) ([]*models.RepPolicy, error) {
return policies, nil return policies, nil
} }
// GetRepPolicyByProjectAndTarget ...
func GetRepPolicyByProjectAndTarget(projectID, targetID int64) ([]*models.RepPolicy, error) {
o := GetOrmer()
sql := `select * from replication_policy where project_id = ? and target_id = ?`
var policies []*models.RepPolicy
if _, err := o.Raw(sql, projectID, targetID).QueryRows(&policies); err != nil {
return nil, err
}
return policies, nil
}
// UpdateRepPolicy ... // UpdateRepPolicy ...
func UpdateRepPolicy(policy *models.RepPolicy) error { func UpdateRepPolicy(policy *models.RepPolicy) error {
o := GetOrmer() o := GetOrmer()

View File

@ -26,9 +26,11 @@ import (
const defaultMaxWorkers int = 10 const defaultMaxWorkers int = 10
var maxJobWorkers int var maxJobWorkers int
var localURL string var localUIURL string
var localRegURL string
var logDir string var logDir string
var uiSecret string var uiSecret string
var verifyRemoteCert string
func init() { func init() {
maxWorkersEnv := os.Getenv("MAX_JOB_WORKERS") maxWorkersEnv := os.Getenv("MAX_JOB_WORKERS")
@ -39,9 +41,14 @@ func init() {
maxJobWorkers = defaultMaxWorkers maxJobWorkers = defaultMaxWorkers
} }
localURL = os.Getenv("HARBOR_URL") localRegURL = os.Getenv("REGISTRY_URL")
if len(localURL) == 0 { if len(localRegURL) == 0 {
localURL = "http://registry:5000/" localRegURL = "http://registry:5000"
}
localUIURL = os.Getenv("UI_URL")
if len(localUIURL) == 0 {
localUIURL = "http://ui"
} }
logDir = os.Getenv("LOG_DIR") logDir = os.Getenv("LOG_DIR")
@ -67,8 +74,15 @@ func init() {
panic("UI Secret is not set") panic("UI Secret is not set")
} }
verifyRemoteCert = os.Getenv("VERIFY_REMOTE_CERT")
if len(verifyRemoteCert) == 0 {
verifyRemoteCert = "on"
}
log.Debugf("config: maxJobWorkers: %d", maxJobWorkers) log.Debugf("config: maxJobWorkers: %d", maxJobWorkers)
log.Debugf("config: localHarborURL: %s", localURL) log.Debugf("config: localUIURL: %s", localUIURL)
log.Debugf("config: localRegURL: %s", localRegURL)
log.Debugf("config: verifyRemoteCert: %s", verifyRemoteCert)
log.Debugf("config: logDir: %s", logDir) log.Debugf("config: logDir: %s", logDir)
log.Debugf("config: uiSecret: ******") log.Debugf("config: uiSecret: ******")
} }
@ -78,9 +92,14 @@ func MaxJobWorkers() int {
return maxJobWorkers return maxJobWorkers
} }
// LocalHarborURL returns the local registry url, job service will use this URL to pull manifest and repository. // LocalUIURL returns the local ui url, job service will use this URL to call API hosted on ui process
func LocalHarborURL() string { func LocalUIURL() string {
return localURL return localUIURL
}
// LocalRegURL returns the local registry url, job service will use this URL to pull image from the registry
func LocalRegURL() string {
return localRegURL
} }
// LogDir returns the absolute path to which the log file will be written // LogDir returns the absolute path to which the log file will be written
@ -92,3 +111,8 @@ func LogDir() string {
func UISecret() string { func UISecret() string {
return uiSecret return uiSecret
} }
// VerifyRemoteCert return the flag to tell jobservice whether or not verify the cert of remote registry
func VerifyRemoteCert() bool {
return verifyRemoteCert != "off"
}

View File

@ -16,13 +16,10 @@
package replication package replication
import ( import (
"fmt"
"io/ioutil"
"net/http"
"strings"
"github.com/vmware/harbor/models" "github.com/vmware/harbor/models"
"github.com/vmware/harbor/utils/log" "github.com/vmware/harbor/utils/log"
"github.com/vmware/harbor/utils/registry"
"github.com/vmware/harbor/utils/registry/auth"
) )
const ( const (
@ -39,22 +36,35 @@ type Deleter struct {
dstUsr string // username ... dstUsr string // username ...
dstPwd string // username ... dstPwd string // username ...
insecure bool
dstClient *registry.Repository
logger *log.Logger logger *log.Logger
} }
// NewDeleter returns a Deleter // NewDeleter returns a Deleter
func NewDeleter(repository string, tags []string, dstURL, dstUsr, dstPwd string, logger *log.Logger) *Deleter { func NewDeleter(repository string, tags []string, dstURL, dstUsr, dstPwd string, insecure bool, logger *log.Logger) (*Deleter, error) {
dstCred := auth.NewBasicAuthCredential(dstUsr, dstPwd)
dstClient, err := newRepositoryClient(dstURL, insecure, dstCred,
repository, "repository", repository, "pull", "push", "*")
if err != nil {
return nil, err
}
deleter := &Deleter{ deleter := &Deleter{
repository: repository, repository: repository,
tags: tags, tags: tags,
dstURL: dstURL, dstURL: dstURL,
dstUsr: dstUsr, dstUsr: dstUsr,
dstPwd: dstPwd, dstPwd: dstPwd,
insecure: insecure,
dstClient: dstClient,
logger: logger, logger: logger,
} }
deleter.logger.Infof("initialization completed: repository: %s, tags: %v, destination URL: %s, destination user: %s", deleter.logger.Infof("initialization completed: repository: %s, tags: %v, destination URL: %s, destination user: %s",
deleter.repository, deleter.tags, deleter.dstURL, deleter.dstUsr) deleter.repository, deleter.tags, deleter.dstURL, deleter.dstUsr)
return deleter return deleter, nil
} }
// Exit ... // Exit ...
@ -64,25 +74,22 @@ func (d *Deleter) Exit() error {
// Enter deletes repository or tags // Enter deletes repository or tags
func (d *Deleter) Enter() (string, error) { func (d *Deleter) Enter() (string, error) {
url := strings.TrimRight(d.dstURL, "/") + "/api/repositories/"
// delete repository
if len(d.tags) == 0 { if len(d.tags) == 0 {
u := url + "?repo_name=" + d.repository tags, err := d.dstClient.ListTag()
if err := del(u, d.dstUsr, d.dstPwd); err != nil { if err != nil {
d.logger.Errorf("an error occurred while deleting repository %s on %s with user %s: %v", d.repository, d.dstURL, d.dstUsr, err) d.logger.Errorf("an error occurred while listing tags of repository %s on %s with user %s: %v", d.repository, d.dstURL, d.dstUsr, err)
return "", err return "", err
} }
d.logger.Infof("repository %s on %s has been deleted", d.repository, d.dstURL) d.tags = append(d.tags, tags...)
return models.JobFinished, nil
} }
// delele tags d.logger.Infof("tags %v will be deleted", d.tags)
for _, tag := range d.tags { for _, tag := range d.tags {
u := url + "?repo_name=" + d.repository + "&tag=" + tag
if err := del(u, d.dstUsr, d.dstPwd); err != nil { if err := d.dstClient.DeleteTag(tag); err != nil {
d.logger.Errorf("an error occurred while deleting repository %s:%s on %s with user %s: %v", d.repository, tag, d.dstURL, d.dstUsr, err) d.logger.Errorf("an error occurred while deleting repository %s:%s on %s with user %s: %v", d.repository, tag, d.dstURL, d.dstUsr, err)
return "", err return "", err
} }
@ -92,28 +99,3 @@ func (d *Deleter) Enter() (string, error) {
return models.JobFinished, nil return models.JobFinished, nil
} }
func del(url, username, password string) error {
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
return err
}
req.SetBasicAuth(username, password)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
if resp.StatusCode == http.StatusOK {
return nil
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return fmt.Errorf("%d %s", resp.StatusCode, string(b))
}

View File

@ -61,6 +61,8 @@ type BaseHandler struct {
dstUsr string // username ... dstUsr string // username ...
dstPwd string // password ... dstPwd string // password ...
insecure bool // whether skip secure check when using https
srcClient *registry.Repository srcClient *registry.Repository
dstClient *registry.Repository dstClient *registry.Repository
@ -75,7 +77,7 @@ type BaseHandler struct {
// InitBaseHandler initializes a BaseHandler: creating clients for source and destination registry, // InitBaseHandler initializes a BaseHandler: creating clients for source and destination registry,
// listing tags of the repository if parameter tags is nil. // listing tags of the repository if parameter tags is nil.
func InitBaseHandler(repository, srcURL, srcSecret, func InitBaseHandler(repository, srcURL, srcSecret,
dstURL, dstUsr, dstPwd string, tags []string, logger *log.Logger) (*BaseHandler, error) { dstURL, dstUsr, dstPwd string, insecure bool, tags []string, logger *log.Logger) (*BaseHandler, error) {
logger.Infof("initializing: repository: %s, tags: %v, source URL: %s, destination URL: %s, destination user: %s", logger.Infof("initializing: repository: %s, tags: %v, source URL: %s, destination URL: %s, destination user: %s",
repository, tags, srcURL, dstURL, dstUsr) repository, tags, srcURL, dstURL, dstUsr)
@ -96,14 +98,16 @@ func InitBaseHandler(repository, srcURL, srcSecret,
c := &http.Cookie{Name: models.UISecretCookie, Value: srcSecret} c := &http.Cookie{Name: models.UISecretCookie, Value: srcSecret}
srcCred := auth.NewCookieCredential(c) srcCred := auth.NewCookieCredential(c)
// srcCred := auth.NewBasicAuthCredential("admin", "Harbor12345") // srcCred := auth.NewBasicAuthCredential("admin", "Harbor12345")
srcClient, err := registry.NewRepositoryWithCredential(base.repository, base.srcURL, srcCred) srcClient, err := newRepositoryClient(base.srcURL, base.insecure, srcCred,
base.repository, "repository", base.repository, "pull", "push", "*")
if err != nil { if err != nil {
return nil, err return nil, err
} }
base.srcClient = srcClient base.srcClient = srcClient
dstCred := auth.NewBasicAuthCredential(base.dstUsr, base.dstPwd) dstCred := auth.NewBasicAuthCredential(base.dstUsr, base.dstPwd)
dstClient, err := registry.NewRepositoryWithCredential(base.repository, base.dstURL, dstCred) dstClient, err := newRepositoryClient(base.dstURL, base.insecure, dstCred,
base.repository, "repository", base.repository, "pull", "push", "*")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -416,3 +420,34 @@ func (m *ManifestPusher) Enter() (string, error) {
return StatePullManifest, nil return StatePullManifest, nil
} }
func newRepositoryClient(endpoint string, insecure bool, credential auth.Credential, repository, scopeType, scopeName string,
scopeActions ...string) (*registry.Repository, error) {
authorizer := auth.NewStandardTokenAuthorizer(credential, insecure, scopeType, scopeName, scopeActions...)
store, err := auth.NewAuthorizerStore(endpoint, insecure, authorizer)
if err != nil {
return nil, err
}
uam := &userAgentModifier{
userAgent: "harbor-registry-client",
}
client, err := registry.NewRepositoryWithModifiers(repository, endpoint, insecure, store, uam)
if err != nil {
return nil, err
}
return client, nil
}
type userAgentModifier struct {
userAgent string
}
// Modify adds user-agent header to the request
func (u *userAgentModifier) Modify(req *http.Request) error {
req.Header.Set(http.CanonicalHeaderKey("User-Agent"), u.userAgent)
return nil
}

View File

@ -38,6 +38,7 @@ type RepJobParm struct {
Tags []string Tags []string
Enabled int Enabled int
Operation string Operation string
Insecure bool
} }
// SM is the state machine to handle job, it handles one job at a time. // SM is the state machine to handle job, it handles one job at a time.
@ -205,11 +206,12 @@ func (sm *SM) Reset(jid int64) error {
return fmt.Errorf("The policy doesn't exist in DB, policy id:%d", job.PolicyID) return fmt.Errorf("The policy doesn't exist in DB, policy id:%d", job.PolicyID)
} }
sm.Parms = &RepJobParm{ sm.Parms = &RepJobParm{
LocalRegURL: config.LocalHarborURL(), LocalRegURL: config.LocalRegURL(),
Repository: job.Repository, Repository: job.Repository,
Tags: job.TagList, Tags: job.TagList,
Enabled: policy.Enabled, Enabled: policy.Enabled,
Operation: job.Operation, Operation: job.Operation,
Insecure: !config.VerifyRemoteCert(),
} }
if policy.Enabled == 0 { if policy.Enabled == 0 {
//worker will cancel this job //worker will cancel this job
@ -260,7 +262,7 @@ func (sm *SM) Reset(jid int64) error {
func addImgTransferTransition(sm *SM) error { func addImgTransferTransition(sm *SM) error {
base, err := replication.InitBaseHandler(sm.Parms.Repository, sm.Parms.LocalRegURL, config.UISecret(), base, err := replication.InitBaseHandler(sm.Parms.Repository, sm.Parms.LocalRegURL, config.UISecret(),
sm.Parms.TargetURL, sm.Parms.TargetUsername, sm.Parms.TargetPassword, sm.Parms.TargetURL, sm.Parms.TargetUsername, sm.Parms.TargetPassword,
sm.Parms.Tags, sm.Logger) sm.Parms.Insecure, sm.Parms.Tags, sm.Logger)
if err != nil { if err != nil {
return err return err
} }
@ -274,8 +276,11 @@ func addImgTransferTransition(sm *SM) error {
} }
func addImgDeleteTransition(sm *SM) error { func addImgDeleteTransition(sm *SM) error {
deleter := replication.NewDeleter(sm.Parms.Repository, sm.Parms.Tags, sm.Parms.TargetURL, deleter, err := replication.NewDeleter(sm.Parms.Repository, sm.Parms.Tags, sm.Parms.TargetURL,
sm.Parms.TargetUsername, sm.Parms.TargetPassword, sm.Logger) sm.Parms.TargetUsername, sm.Parms.TargetPassword, sm.Parms.Insecure, sm.Logger)
if err != nil {
return err
}
sm.AddTransition(models.JobRunning, replication.StateDelete, deleter) sm.AddTransition(models.JobRunning, replication.StateDelete, deleter)
sm.AddTransition(replication.StateDelete, models.JobFinished, &StatusUpdater{DummyHandler{JobID: sm.JobID}, models.JobFinished}) sm.AddTransition(replication.StateDelete, models.JobFinished, &StatusUpdater{DummyHandler{JobID: sm.JobID}, models.JobFinished})

View File

@ -17,3 +17,11 @@ Changelog for harbor database schema
- delete data `AMDRWS` from table `role` - delete data `AMDRWS` from table `role`
- delete data `A` from table `access` - delete data `A` from table `access`
## 0.2.0
- create table `replication_policy`
- create table `replication_target`
- create table `replication_job`
- add column `repo_tag` to table `access_log`
- alter column `repo_name` on table `access_log`
- alter column `email` on table `user`

View File

@ -85,3 +85,42 @@ class Project(Base):
deleted = sa.Column(sa.Integer, nullable=False, server_default=sa.text("'0'")) deleted = sa.Column(sa.Integer, nullable=False, server_default=sa.text("'0'"))
public = sa.Column(sa.Integer, nullable=False, server_default=sa.text("'0'")) public = sa.Column(sa.Integer, nullable=False, server_default=sa.text("'0'"))
owner = relationship(u'User') owner = relationship(u'User')
class ReplicationPolicy(Base):
__tablename__ = "replication_policy"
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String(256))
project_id = sa.Column(sa.Integer, nullable=False)
target_id = sa.Column(sa.Integer, nullable=False)
enabled = sa.Column(mysql.TINYINT(1), nullable=False, server_default=sa.text("'1'"))
description = sa.Column(sa.Text)
cron_str = sa.Column(sa.String(256))
start_time = sa.Column(mysql.TIMESTAMP)
creation_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP"))
update_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"))
class ReplicationTarget(Base):
__tablename__ = "replication_target"
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String(64))
url = sa.Column(sa.String(64))
username = sa.Column(sa.String(40))
password = sa.Column(sa.String(40))
target_type = sa.Column(mysql.TINYINT(1), nullable=False, server_default=sa.text("'0'"))
creation_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP"))
update_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"))
class ReplicationJob(Base):
__tablename__ = "replication_job"
id = sa.Column(sa.Integer, primary_key=True)
status = sa.Column(sa.String(64), nullable=False)
policy_id = sa.Column(sa.Integer, nullable=False)
repository = sa.Column(sa.String(256), nullable=False)
operation = sa.Column(sa.String(64), nullable=False)
tags = sa.Column(sa.String(16384))
creation_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP"))
update_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"))

View File

@ -0,0 +1,52 @@
# Copyright (c) 2008-2016 VMware, Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""0.1.1 to 0.2.0
Revision ID: 0.1.1
Revises:
"""
# revision identifiers, used by Alembic.
revision = '0.2.0'
down_revision = '0.1.1'
branch_labels = None
depends_on = None
from alembic import op
from db_meta import *
from sqlalchemy.dialects import mysql
def upgrade():
"""
update schema&data
"""
bind = op.get_bind()
#alter column user.email, alter column access_log.repo_name, and add column access_log.repo_tag
op.alter_column('user', 'email', type_=sa.String(128), existing_type=sa.String(30))
op.alter_column('access_log', 'repo_name', type_=sa.String(256), existing_type=sa.String(40))
op.add_column('access_log', sa.Column('repo_tag', sa.String(128)))
#create tables: replication_policy, replication_target, replication_job
ReplicationPolicy.__table__.create(bind)
ReplicationTarget.__table__.create(bind)
ReplicationJob.__table__.create(bind)
def downgrade():
"""
Downgrade has been disabled.
"""
pass

View File

@ -21,6 +21,7 @@ import (
"github.com/vmware/harbor/utils/log" "github.com/vmware/harbor/utils/log"
"github.com/vmware/harbor/utils/registry" "github.com/vmware/harbor/utils/registry"
"github.com/vmware/harbor/utils/registry/auth"
"github.com/astaxie/beego/cache" "github.com/astaxie/beego/cache"
) )
@ -30,8 +31,6 @@ var (
Cache cache.Cache Cache cache.Cache
endpoint string endpoint string
username string username string
registryClient *registry.Registry
repositoryClients map[string]*registry.Repository
) )
const catalogKey string = "catalog" const catalogKey string = "catalog"
@ -45,23 +44,18 @@ func init() {
endpoint = os.Getenv("REGISTRY_URL") endpoint = os.Getenv("REGISTRY_URL")
username = "admin" username = "admin"
repositoryClients = make(map[string]*registry.Repository, 10)
} }
// RefreshCatalogCache calls registry's API to get repository list and write it to cache. // RefreshCatalogCache calls registry's API to get repository list and write it to cache.
func RefreshCatalogCache() error { func RefreshCatalogCache() error {
log.Debug("refreshing catalog cache...") log.Debug("refreshing catalog cache...")
if registryClient == nil { registryClient, err := NewRegistryClient(endpoint, true, username,
var err error "registry", "catalog", "*")
registryClient, err = registry.NewRegistryWithUsername(endpoint, username)
if err != nil { if err != nil {
log.Errorf("error occurred while initializing registry client used by cache: %v", err)
return err return err
} }
}
var err error
rs, err := registryClient.Catalog() rs, err := registryClient.Catalog()
if err != nil { if err != nil {
return err return err
@ -70,15 +64,13 @@ func RefreshCatalogCache() error {
repos := []string{} repos := []string{}
for _, repo := range rs { for _, repo := range rs {
rc, ok := repositoryClients[repo] rc, err := NewRepositoryClient(endpoint, true, username,
if !ok { repo, "repository", repo, "pull", "push", "*")
rc, err = registry.NewRepositoryWithUsername(repo, endpoint, username)
if err != nil { if err != nil {
log.Errorf("error occurred while initializing repository client used by cache: %s %v", repo, err) log.Errorf("error occurred while initializing repository client used by cache: %s %v", repo, err)
continue continue
} }
repositoryClients[repo] = rc
}
tags, err := rc.ListTag() tags, err := rc.ListTag()
if err != nil { if err != nil {
log.Errorf("error occurred while list tag for %s: %v", repo, err) log.Errorf("error occurred while list tag for %s: %v", repo, err)
@ -112,3 +104,38 @@ func GetRepoFromCache() ([]string, error) {
} }
return result.([]string), nil return result.([]string), nil
} }
// NewRegistryClient ...
func NewRegistryClient(endpoint string, insecure bool, username, scopeType, scopeName string,
scopeActions ...string) (*registry.Registry, error) {
authorizer := auth.NewUsernameTokenAuthorizer(username, scopeType, scopeName, scopeActions...)
store, err := auth.NewAuthorizerStore(endpoint, insecure, authorizer)
if err != nil {
return nil, err
}
client, err := registry.NewRegistryWithModifiers(endpoint, insecure, store)
if err != nil {
return nil, err
}
return client, nil
}
// NewRepositoryClient ...
func NewRepositoryClient(endpoint string, insecure bool, username, repository, scopeType, scopeName string,
scopeActions ...string) (*registry.Repository, error) {
authorizer := auth.NewUsernameTokenAuthorizer(username, scopeType, scopeName, scopeActions...)
store, err := auth.NewAuthorizerStore(endpoint, insecure, authorizer)
if err != nil {
return nil, err
}
client, err := registry.NewRepositoryWithModifiers(repository, endpoint, insecure, store)
if err != nil {
return nil, err
}
return client, nil
}

View File

@ -25,7 +25,6 @@ import (
"github.com/vmware/harbor/models" "github.com/vmware/harbor/models"
"github.com/vmware/harbor/service/cache" "github.com/vmware/harbor/service/cache"
"github.com/vmware/harbor/utils/log" "github.com/vmware/harbor/utils/log"
"github.com/vmware/harbor/utils/registry"
"github.com/astaxie/beego" "github.com/astaxie/beego"
) )
@ -57,7 +56,7 @@ func (n *NotificationHandler) Post() {
matched = false matched = false
} }
if matched && (strings.HasPrefix(e.Request.UserAgent, "docker") || if matched && (strings.HasPrefix(e.Request.UserAgent, "docker") ||
strings.ToLower(strings.TrimSpace(e.Request.UserAgent)) == strings.ToLower(registry.UserAgent)) { strings.ToLower(strings.TrimSpace(e.Request.UserAgent)) == "harbor-registry-client") {
username = e.Actor.Name username = e.Actor.Name
action = e.Action action = e.Action
repo = e.Target.Repository repo = e.Target.Repository

View File

@ -14,6 +14,8 @@
vm.currentLanguage = I18nService().getCurrentLanguage(); vm.currentLanguage = I18nService().getCurrentLanguage();
vm.languageName = I18nService().getLanguageName(vm.currentLanguage); vm.languageName = I18nService().getLanguageName(vm.currentLanguage);
I18nService().setCurrentLanguage(vm.currentLanguage);
console.log('current language:' + vm.languageName); console.log('current language:' + vm.languageName);
vm.supportLanguages = I18nService().getSupportLanguages(); vm.supportLanguages = I18nService().getSupportLanguages();

View File

@ -44,13 +44,13 @@
<div class="form-group col-md-12 form-group-custom"> <div class="form-group col-md-12 form-group-custom">
<label for="destinationName" class="col-md-3 control-label">// 'name' | tr //:</label> <label for="destinationName" class="col-md-3 control-label">// 'name' | tr //:</label>
<div class="col-md-7"> <div class="col-md-7">
<select class="form-control form-control-custom" id="destinationName" ng-model="replication.destination.selection" ng-options="d as d.name for d in vm.destinations track by d.id" ng-click="vm.selectDestination(replication.destination.selection)"></select> <select class="form-control form-control-custom" id="destinationName" ng-model="replication.destination.selection" ng-options="d as d.name for d in vm.destinations track by d.id" ng-click="vm.selectDestination(replication.destination.selection)" ng-disabled="!vm.targetEditable"></select>
</div> </div>
</div> </div>
<div class="form-group col-md-12 form-group-custom"> <div class="form-group col-md-12 form-group-custom">
<label for="endpoint" class="col-md-3 control-label">// 'endpoint' | tr //:</label> <label for="endpoint" class="col-md-3 control-label">// 'endpoint' | tr //:</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="text" class="form-control form-control-custom" id="endpoint" ng-model="replication.destination.endpoint" name="uEndpoint" ng-value="vm.endpoint" required> <input type="text" class="form-control form-control-custom" id="endpoint" ng-model="replication.destination.endpoint" name="uEndpoint" ng-value="vm.endpoint" required ng-disabled="!vm.targetEditable">
<div ng-messages="form.$submitted && form.uEndpoint.$error"> <div ng-messages="form.$submitted && form.uEndpoint.$error">
<span ng-message="required">// 'endpoint_is_required' | tr //</span> <span ng-message="required">// 'endpoint_is_required' | tr //</span>
</div> </div>
@ -59,7 +59,7 @@
<div class="form-group col-md-12 form-group-custom"> <div class="form-group col-md-12 form-group-custom">
<label for="username" class="col-md-3 control-label">// 'username' | tr //:</label> <label for="username" class="col-md-3 control-label">// 'username' | tr //:</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="text" class="form-control" id="username" ng-model="replication.destination.username" name="uUsername" ng-value="vm.username" required> <input type="text" class="form-control" id="username" ng-model="replication.destination.username" name="uUsername" ng-value="vm.username" required ng-disabled="!vm.targetEditable">
<div ng-messages="form.$submitted && form.uUsername.$error"> <div ng-messages="form.$submitted && form.uUsername.$error">
<span ng-message="required">// 'username_is_required' | tr //</span> <span ng-message="required">// 'username_is_required' | tr //</span>
</div> </div>
@ -68,7 +68,7 @@
<div class="form-group col-md-12 form-group-custom"> <div class="form-group col-md-12 form-group-custom">
<label for="password" class="col-md-3 control-label">// 'password' | tr //:</label> <label for="password" class="col-md-3 control-label">// 'password' | tr //:</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="password" class="form-control" id="password" ng-model="replication.destination.password" name="uPassword" ng-value="vm.password" required> <input type="password" class="form-control" id="password" ng-model="replication.destination.password" name="uPassword" ng-value="vm.password" required ng-disabled="!vm.targetEditable">
<div ng-messages="form.$submitted && form.uPassword.$error"> <div ng-messages="form.$submitted && form.uPassword.$error">
<span ng-message="required">// 'password_is_required' | tr //</span> <span ng-message="required">// 'password_is_required' | tr //</span>
</div> </div>

View File

@ -6,9 +6,9 @@
.module('harbor.replication') .module('harbor.replication')
.directive('createPolicy', createPolicy); .directive('createPolicy', createPolicy);
CreatePolicyController.$inject = ['$scope', 'ListReplicationPolicyService', 'ListDestinationService', 'UpdateDestinationService', 'PingDestinationService', 'CreateReplicationPolicyService', 'UpdateReplicationPolicyService', '$location', 'getParameterByName']; CreatePolicyController.$inject = ['$scope', 'ListReplicationPolicyService', 'ListDestinationService', 'UpdateDestinationService', 'PingDestinationService', 'CreateReplicationPolicyService', 'UpdateReplicationPolicyService', 'ListDestinationPolicyService','$location', 'getParameterByName', '$filter', 'trFilter'];
function CreatePolicyController($scope, ListReplicationPolicyService, ListDestinationService, UpdateDestinationService, PingDestinationService, CreateReplicationPolicyService, UpdateReplicationPolicyService, $location, getParameterByName) { function CreatePolicyController($scope, ListReplicationPolicyService, ListDestinationService, UpdateDestinationService, PingDestinationService, CreateReplicationPolicyService, UpdateReplicationPolicyService, ListDestinationPolicyService, $location, getParameterByName, $filter, trFilter) {
var vm = this; var vm = this;
//Since can not set value for textarea by using vm //Since can not set value for textarea by using vm
@ -34,6 +34,8 @@
vm.update = update; vm.update = update;
vm.pingDestination = pingDestination; vm.pingDestination = pingDestination;
vm.targetEditable = true;
$scope.$watch('vm.destinations', function(current) { $scope.$watch('vm.destinations', function(current) {
if(current) { if(current) {
vm1.selection = current[0]; vm1.selection = current[0];
@ -43,23 +45,6 @@
} }
}); });
$scope.$watch('vm.action+","+vm.policyId', function(current) {
if(current) {
console.log('Current action for replication policy:' + current);
var parts = current.split(',');
vm.action = parts[0];
vm.policyId = Number(parts[1]);
switch(parts[0]) {
case 'ADD_NEW':
vm.addNew();
break;
case 'EDIT':
vm.edit(vm.policyId);
break;
}
}
});
function selectDestination(item) { function selectDestination(item) {
vm1.selection = item; vm1.selection = item;
vm1.endpoint = item.endpoint; vm1.endpoint = item.endpoint;
@ -74,6 +59,8 @@
} }
function addNew() { function addNew() {
vm.targetEditable = true;
$filter('tr')('add_new_policy', []);
vm0.name = ''; vm0.name = '';
vm0.description = ''; vm0.description = '';
vm0.enabled = true; vm0.enabled = true;
@ -81,6 +68,9 @@
function edit(policyId) { function edit(policyId) {
console.log('Edit policy ID:' + policyId); console.log('Edit policy ID:' + policyId);
vm.policyId = policyId;
vm.targetEditable = true;
$filter('tr')('edit_policy', []);
ListReplicationPolicyService(policyId) ListReplicationPolicyService(policyId)
.success(listReplicationPolicySuccess) .success(listReplicationPolicySuccess)
.error(listReplicationPolicyFailed); .error(listReplicationPolicyFailed);
@ -129,12 +119,37 @@
function listDestinationFailed(data, status) { function listDestinationFailed(data, status) {
console.log('Failed list destination:' + data); console.log('Failed list destination:' + data);
} }
function listDestinationPolicySuccess(data, status) {
vm.targetEditable = true;
for(var i in data) {
if(data[i].enabled === 1) {
vm.targetEditable = false;
break;
}
}
console.log('current target editable:' + vm.targetEditable + ', policy ID:' + vm.policyId);
}
function listDestinationPolicyFailed(data, status) {
console.log('Failed list destination policy:' + data);
}
function listReplicationPolicySuccess(data, status) { function listReplicationPolicySuccess(data, status) {
console.log(data);
var replicationPolicy = data; var replicationPolicy = data;
vm0.name = replicationPolicy.name; vm0.name = replicationPolicy.name;
vm0.description = replicationPolicy.description; vm0.description = replicationPolicy.description;
vm0.enabled = replicationPolicy.enabled == 1; vm0.enabled = replicationPolicy.enabled == 1;
vm.targetId = replicationPolicy.target_id; vm.targetId = replicationPolicy.target_id;
if(vm0.enabled) {
vm.targetEditable = false;
}else{
ListDestinationPolicyService(vm.targetId)
.success(listDestinationPolicySuccess)
.error(listDestinationPolicyFailed);
}
} }
function listReplicationPolicyFailed(data, status) { function listReplicationPolicyFailed(data, status) {
console.log('Failed list replication policy:' + data); console.log('Failed list replication policy:' + data);
@ -145,7 +160,7 @@
} }
function createReplicationPolicyFailed(data, status) { function createReplicationPolicyFailed(data, status) {
if(status === 409) { if(status === 409) {
alert('Policy name already exists.'); alert($filter('tr')('policy_already_exists', []));
} }
console.log('Failed create replication policy.'); console.log('Failed create replication policy.');
} }
@ -163,10 +178,10 @@
console.log('Failed update destination.'); console.log('Failed update destination.');
} }
function pingDestinationSuccess(data, status) { function pingDestinationSuccess(data, status) {
alert('Successful ping target.'); alert($filter('tr')('successful_ping_target', []));
} }
function pingDestinationFailed(data, status) { function pingDestinationFailed(data, status) {
alert('Failed ping target:' + data); alert($filter('tr')('failed_ping_target', []) + ':' + data);
} }
} }
@ -190,9 +205,20 @@
function link(scope, element, attr, ctrl) { function link(scope, element, attr, ctrl) {
element.find('#createPolicyModal').on('show.bs.modal', function() { element.find('#createPolicyModal').on('show.bs.modal', function() {
ctrl.prepareDestination();
scope.form.$setPristine(); scope.form.$setPristine();
scope.form.$setUntouched(); scope.form.$setUntouched();
ctrl.prepareDestination();
switch(ctrl.action) {
case 'ADD_NEW':
ctrl.addNew();
break;
case 'EDIT':
ctrl.edit(ctrl.policyId);
break;
}
scope.$apply();
}); });
ctrl.save = save; ctrl.save = save;

View File

@ -11,7 +11,7 @@
<div class="form-group col-md-12 form-group-custom"> <div class="form-group col-md-12 form-group-custom">
<label for="name" class="col-md-3 control-label">// 'name' | tr //:</label> <label for="name" class="col-md-3 control-label">// 'name' | tr //:</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="text" class="form-control form-control-custom" id="name" ng-model="destination.name" name="uName" required> <input type="text" class="form-control form-control-custom" id="name" ng-model="destination.name" name="uName" required ng-disabled="!vm.editable">
<div ng-messages="form.$submitted && form.uName.$error"> <div ng-messages="form.$submitted && form.uName.$error">
<span ng-message="required">// 'name_is_required' | tr //</span> <span ng-message="required">// 'name_is_required' | tr //</span>
</div> </div>
@ -20,7 +20,7 @@
<div class="form-group col-md-12 form-group-custom"> <div class="form-group col-md-12 form-group-custom">
<label for="description" class="col-md-3 control-label">// 'endpoint' | tr //:</label> <label for="description" class="col-md-3 control-label">// 'endpoint' | tr //:</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="text" class="form-control form-control-custom" id="endpoint" ng-model="destination.endpoint" name="uEndpoint" required > <input type="text" class="form-control form-control-custom" id="endpoint" ng-model="destination.endpoint" name="uEndpoint" required ng-disabled="!vm.editable">
<div ng-messages="form.$submitted && form.uEndpoint.$error"> <div ng-messages="form.$submitted && form.uEndpoint.$error">
<span ng-message="required">// 'endpoint_is_required' | tr //</span> <span ng-message="required">// 'endpoint_is_required' | tr //</span>
</div> </div>
@ -29,7 +29,7 @@
<div class="form-group col-md-12 form-group-custom"> <div class="form-group col-md-12 form-group-custom">
<label for="username" class="col-md-3 control-label">// 'username' | tr //:</label> <label for="username" class="col-md-3 control-label">// 'username' | tr //:</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="text" class="form-control" id="username" ng-model="destination.username" name="uUsername" required> <input type="text" class="form-control" id="username" ng-model="destination.username" name="uUsername" required ng-disabled="!vm.editable">
<div ng-messages="form.$submitted && form.uUsername.$error"> <div ng-messages="form.$submitted && form.uUsername.$error">
<span ng-message="required">// 'username_is_required' | tr //</span> <span ng-message="required">// 'username_is_required' | tr //</span>
</div> </div>
@ -38,7 +38,7 @@
<div class="form-group col-md-12 form-group-custom"> <div class="form-group col-md-12 form-group-custom">
<label for="password" class="col-md-3 control-label">// 'password' | tr //:</label> <label for="password" class="col-md-3 control-label">// 'password' | tr //:</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="password" class="form-control" id="password" ng-model="destination.password" name="uPassword" required> <input type="password" class="form-control" id="password" ng-model="destination.password" name="uPassword" required ng-disabled="!vm.editable">
<div ng-messages="form.$submitted && form.uPassword.$error"> <div ng-messages="form.$submitted && form.uPassword.$error">
<span ng-message="required">// 'password_is_required' | tr //</span> <span ng-message="required">// 'password_is_required' | tr //</span>
</div> </div>

View File

@ -6,9 +6,9 @@
.module('harbor.system.management') .module('harbor.system.management')
.directive('createDestination', createDestination); .directive('createDestination', createDestination);
CreateDestinationController.$inject = ['$scope', 'ListDestinationService', 'CreateDestinationService', 'UpdateDestinationService', 'PingDestinationService']; CreateDestinationController.$inject = ['$scope', 'ListDestinationService', 'CreateDestinationService', 'UpdateDestinationService', 'PingDestinationService', 'ListDestinationPolicyService', '$filter', 'trFilter'];
function CreateDestinationController($scope, ListDestinationService, CreateDestinationService, UpdateDestinationService, PingDestinationService) { function CreateDestinationController($scope, ListDestinationService, CreateDestinationService, UpdateDestinationService, PingDestinationService, ListDestinationPolicyService, $filter, trFilter) {
var vm = this; var vm = this;
$scope.destination = {}; $scope.destination = {};
@ -20,25 +20,11 @@
vm.update = update; vm.update = update;
vm.pingDestination = pingDestination; vm.pingDestination = pingDestination;
$scope.$watch('vm.action+","+vm.targetId', function(current) { vm.editable = true;
if(current) {
var parts = current.split(',');
vm.action = parts[0];
vm.targetId = parts[1];
switch(vm.action) {
case 'ADD_NEW':
vm.modalTitle = 'Create destination';
vm.addNew();
break;
case 'EDIT':
vm.modalTitle = 'Edit destination';
vm.edit(vm.targetId);
break;
}
}
});
function addNew() { function addNew() {
vm.editable = true;
vm.modalTitle = $filter('tr')('add_new_destination', []);
vm0.name = ''; vm0.name = '';
vm0.endpoint = ''; vm0.endpoint = '';
vm0.username = ''; vm0.username = '';
@ -46,7 +32,11 @@
} }
function edit(targetId) { function edit(targetId) {
getDestination(targetId); vm.editable = true;
vm.modalTitle = $filter('tr')('edit_destination', []);
ListDestinationService(targetId)
.success(getDestinationSuccess)
.error(getDestinationFailed);
} }
function create(destination) { function create(destination) {
@ -63,7 +53,7 @@
function createDestinationFailed(data, status) { function createDestinationFailed(data, status) {
if(status === 409) { if(status === 409) {
alert('Destination already exists.'); alert($filter('tr')('destination_already_exists', []));
} }
console.log('Failed create destination:' + data); console.log('Failed create destination:' + data);
} }
@ -83,11 +73,6 @@
console.log('Failed update destination.'); console.log('Failed update destination.');
} }
function getDestination(targetId) {
ListDestinationService(targetId)
.success(getDestinationSuccess)
.error(getDestinationFailed);
}
function getDestinationSuccess(data, status) { function getDestinationSuccess(data, status) {
var destination = data; var destination = data;
@ -95,12 +80,29 @@
vm0.endpoint = destination.endpoint; vm0.endpoint = destination.endpoint;
vm0.username = destination.username; vm0.username = destination.username;
vm0.password = destination.password; vm0.password = destination.password;
ListDestinationPolicyService(destination.id)
.success(listDestinationPolicySuccess)
.error(listDestinationPolicyFailed);
} }
function getDestinationFailed(data, status) { function getDestinationFailed(data, status) {
console.log('Failed get destination.'); console.log('Failed get destination.');
} }
function listDestinationPolicySuccess(data, status) {
for(var i in data) {
if(data[i].enabled === 1) {
vm.editable = false;
break;
}
}
}
function listDestinationPolicyFailed(data, status) {
console.log('Failed list destination policy:' + data);
}
function pingDestination() { function pingDestination() {
var target = { var target = {
'name': vm0.name, 'name': vm0.name,
@ -113,10 +115,10 @@
.error(pingDestinationFailed); .error(pingDestinationFailed);
} }
function pingDestinationSuccess(data, status) { function pingDestinationSuccess(data, status) {
alert('Successful ping target.'); alert($filter('tr')('successful_ping_target', []));
} }
function pingDestinationFailed(data, status) { function pingDestinationFailed(data, status) {
alert('Failed ping target:' + data); alert($filter('tr')('failed_ping_target', []) + ':' + data);
} }
} }
@ -139,8 +141,19 @@
function link(scope, element, attrs, ctrl) { function link(scope, element, attrs, ctrl) {
element.find('#createDestinationModal').on('show.bs.modal', function() { element.find('#createDestinationModal').on('show.bs.modal', function() {
scope.form.$setPristine(); scope.form.$setPristine();
scope.form.$setUntouched(); scope.form.$setUntouched();
switch(ctrl.action) {
case 'ADD_NEW':
ctrl.addNew();
break;
case 'EDIT':
ctrl.edit(ctrl.targetId);
break;
}
scope.$apply();
}); });
ctrl.save = save; ctrl.save = save;

View File

@ -6,9 +6,9 @@
.module('harbor.system.management') .module('harbor.system.management')
.directive('destination', destination); .directive('destination', destination);
DestinationController.$inject = ['$scope', 'ListDestinationService', 'DeleteDestinationService']; DestinationController.$inject = ['$scope', 'ListDestinationService', 'DeleteDestinationService', '$filter', 'trFilter'];
function DestinationController($scope, ListDestinationService, DeleteDestinationService) { function DestinationController($scope, ListDestinationService, DeleteDestinationService, $filter, trFilter) {
var vm = this; var vm = this;
vm.retrieve = retrieve; vm.retrieve = retrieve;
@ -66,6 +66,7 @@
function deleteDestinationFailed(data, status) { function deleteDestinationFailed(data, status) {
console.log('Failed delete destination.'); console.log('Failed delete destination.');
alert($filter('tr')('failed_delete_destination', []) + ':' + data);
} }
} }

View File

@ -7,8 +7,10 @@
'ngCookies', 'ngCookies',
'harbor.session', 'harbor.session',
'harbor.layout.header', 'harbor.layout.header',
'harbor.layout.footer',
'harbor.layout.navigation', 'harbor.layout.navigation',
'harbor.layout.sign.up', 'harbor.layout.sign.up',
'harbor.layout.add.new',
'harbor.layout.account.setting', 'harbor.layout.account.setting',
'harbor.layout.forgot.password', 'harbor.layout.forgot.password',
'harbor.layout.reset.password', 'harbor.layout.reset.password',

View File

@ -0,0 +1,15 @@
(function() {
'use strict';
angular
.module('harbor.layout.add.new')
.controller('AddNewController', AddNewController);
AddNewController.$inject = [];
function AddNewController() {
var vm = this;
}
})();

View File

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

View File

@ -0,0 +1,13 @@
(function() {
'use strict';
angular
.module('harbor.layout.footer')
.controller('FooterController', FooterController);
function FooterController() {
var vm = this;
}
})();

View File

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

View File

@ -39,9 +39,14 @@
} }
function confirm() { function confirm() {
if(location.pathname === '/add_new') {
$window.location.href = '/dashboard';
}else{
$window.location.href = '/'; $window.location.href = '/';
} }
} }
}
})(); })();

View File

@ -0,0 +1,19 @@
(function() {
'use strict';
angular
.module('harbor.services.destination')
.factory('ListDestinationPolicyService', ListDestinationPolicyService);
ListDestinationPolicyService.$inject = ['$http'];
function ListDestinationPolicyService($http) {
return listDestinationPolicy;
function listDestinationPolicy(targetId) {
return $http
.get('/api/targets/' + targetId + '/policies/');
}
}
})();

View File

@ -160,6 +160,7 @@ var locale_messages = {
'endpoint': 'Endpoint', 'endpoint': 'Endpoint',
'test_connection': 'Test connection', 'test_connection': 'Test connection',
'add_new_destination': 'New Destination', 'add_new_destination': 'New Destination',
'edit_destination': 'Edit Destination',
'successful_changed_password': 'Password has been changed successfully.', 'successful_changed_password': 'Password has been changed successfully.',
'change_profile': 'Change Profile', 'change_profile': 'Change Profile',
'successful_changed_profile': 'User profile has been changed successfully.', 'successful_changed_profile': 'User profile has been changed successfully.',
@ -172,5 +173,15 @@ var locale_messages = {
'send': 'Send', 'send': 'Send',
'successful_signed_up': 'Signed up successfully.', 'successful_signed_up': 'Signed up successfully.',
'add_new_policy': 'Add New Policy', 'add_new_policy': 'Add New Policy',
'edit_policy': 'Edit Policy' 'edit_policy': 'Edit Policy',
'add_new_title': 'Add User',
'add_new': 'Add',
'successful_added': 'Added new user successfully.',
'copyright': 'Copyright',
'all_rights_reserved': 'All Rights Reserved.',
'successful_ping_target': 'Pinged target successfully.',
'failed_ping_target': 'Pinged target failed',
'policy_already_exists': 'Policy alreay exists.',
'destination_already_exists': 'Destination already exists.',
'failed_delete_destination': 'Delete destination failed:'
}; };

View File

@ -159,10 +159,11 @@ var locale_messages = {
'endpoint_is_required': '终端URL为必填项。', 'endpoint_is_required': '终端URL为必填项。',
'test_connection': '测试连接', 'test_connection': '测试连接',
'add_new_destination': '新建目标', 'add_new_destination': '新建目标',
'edit_destination': '编辑目标',
'successful_changed_password': '修改密码操作成功。', 'successful_changed_password': '修改密码操作成功。',
'change_profile': '修改个人信息', 'change_profile': '修改个人信息',
'successful_changed_profile': '修改个人信息操作成功。', 'successful_changed_profile': '修改个人信息操作成功。',
'form_is_invalid': '表单内容无效', 'form_is_invalid': '表单内容无sign_up效',
'form_is_invalid_message': '表单内容无效,请填写必填字段。', 'form_is_invalid_message': '表单内容无效,请填写必填字段。',
'administrator': '管理员', 'administrator': '管理员',
'popular_repositories': '热门镜像仓库', 'popular_repositories': '热门镜像仓库',
@ -171,5 +172,15 @@ var locale_messages = {
'send': '发送', 'send': '发送',
'successful_signed_up': '注册成功。', 'successful_signed_up': '注册成功。',
'add_new_policy': '新增策略', 'add_new_policy': '新增策略',
'edit_policy': '修改策略' 'edit_policy': '修改策略',
'add_new_title': '新增用户',
'add_new': '新增',
'successful_added': '新增用户成功。',
'copyright': '版权所有',
'all_rights_reserved': '保留所有权利。',
'successful_ping_target': 'Ping 目标成功。',
'failed_ping_target': 'Ping 目标失败:',
'policy_already_exists': '策略已存在。',
'destination_already_exists': '目标已存在。',
'failed_delete_destination': '删除目标失败:'
}; };

View File

@ -9,9 +9,11 @@
I18nService.$inject = ['$cookies', '$window']; I18nService.$inject = ['$cookies', '$window'];
function I18nService($cookies, $window) { function I18nService($cookies, $window) {
var cookieOptions = {'path': '/'}; var cookieOptions = {'path': '/'};
var messages = $.extend(true, {}, eval('locale_messages')); var messages = $.extend(true, {}, eval('locale_messages'));
var defaultLanguage = navigator.language || 'en-US'; var defaultLanguage = 'en-US';
var supportLanguages = { var supportLanguages = {
'en-US': 'English', 'en-US': 'English',
'zh-CN': '中文' 'zh-CN': '中文'
@ -25,6 +27,7 @@
return false; return false;
}; };
return tr; return tr;
function tr() { function tr() {
@ -45,6 +48,7 @@
if(!angular.isDefined(language) || !isSupportLanguage(language)) { if(!angular.isDefined(language) || !isSupportLanguage(language)) {
language = defaultLanguage; language = defaultLanguage;
} }
$cookies.put('language', language, cookieOptions);
return supportLanguages[language]; return supportLanguages[language];
}, },
'getSupportLanguages': function() { 'getSupportLanguages': function() {

View File

@ -35,6 +35,7 @@ func initRouters() {
beego.Router("/project", &controllers.ProjectController{}) beego.Router("/project", &controllers.ProjectController{})
beego.Router("/repository", &controllers.RepositoryController{}) beego.Router("/repository", &controllers.RepositoryController{})
beego.Router("/sign_up", &controllers.SignUpController{}) beego.Router("/sign_up", &controllers.SignUpController{})
beego.Router("/add_new", &controllers.AddNewController{})
beego.Router("/account_setting", &controllers.AccountSettingController{}) beego.Router("/account_setting", &controllers.AccountSettingController{})
beego.Router("/admin_option", &controllers.AdminOptionController{}) beego.Router("/admin_option", &controllers.AdminOptionController{})
beego.Router("/forgot_password", &controllers.ForgotPasswordController{}) beego.Router("/forgot_password", &controllers.ForgotPasswordController{})
@ -76,6 +77,7 @@ func initRouters() {
beego.Router("/api/targets/", &api.TargetAPI{}, "get:List") beego.Router("/api/targets/", &api.TargetAPI{}, "get:List")
beego.Router("/api/targets/", &api.TargetAPI{}, "post:Post") beego.Router("/api/targets/", &api.TargetAPI{}, "post:Post")
beego.Router("/api/targets/:id([0-9]+)", &api.TargetAPI{}) beego.Router("/api/targets/:id([0-9]+)", &api.TargetAPI{})
beego.Router("/api/targets/:id([0-9]+)/policies/", &api.TargetAPI{}, "get:ListPolicies")
beego.Router("/api/targets/ping", &api.TargetAPI{}, "post:Ping") beego.Router("/api/targets/ping", &api.TargetAPI{}, "post:Ping")
beego.Router("/api/users/:id/sysadmin", &api.UserAPI{}, "put:ToggleUserAdminRole") beego.Router("/api/users/:id/sysadmin", &api.UserAPI{}, "put:ToggleUserAdminRole")
beego.Router("/api/repositories/top", &api.RepositoryAPI{}, "get:GetTopRepos") beego.Router("/api/repositories/top", &api.RepositoryAPI{}, "get:GetTopRepos")

View File

@ -16,40 +16,63 @@
package auth package auth
import ( import (
"crypto/tls"
"fmt"
"net/http" "net/http"
au "github.com/docker/distribution/registry/client/auth" au "github.com/docker/distribution/registry/client/auth"
"github.com/vmware/harbor/utils/registry/utils"
) )
// Handler authorizes requests according to the schema // Authorizer authorizes requests according to the schema
type Handler interface { type Authorizer interface {
// Scheme : basic, bearer // Scheme : basic, bearer
Scheme() string Scheme() string
//AuthorizeRequest adds basic auth or token auth to the header of request //Authorize adds basic auth or token auth to the header of request
AuthorizeRequest(req *http.Request, params map[string]string) error Authorize(req *http.Request, params map[string]string) error
} }
// RequestAuthorizer holds a handler list, which will authorize request. // AuthorizerStore holds a authorizer list, which will authorize request.
// Implements interface RequestModifier // And it implements interface Modifier
type RequestAuthorizer struct { type AuthorizerStore struct {
handlers []Handler authorizers []Authorizer
challenges []au.Challenge challenges []au.Challenge
} }
// NewRequestAuthorizer ... // NewAuthorizerStore ...
func NewRequestAuthorizer(handlers []Handler, challenges []au.Challenge) *RequestAuthorizer { func NewAuthorizerStore(endpoint string, insecure bool, authorizers ...Authorizer) (*AuthorizerStore, error) {
return &RequestAuthorizer{ endpoint = utils.FormatEndpoint(endpoint)
handlers: handlers,
challenges: challenges, client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: insecure,
},
},
} }
resp, err := client.Get(buildPingURL(endpoint))
if err != nil {
return nil, err
}
challenges := ParseChallengeFromResponse(resp)
return &AuthorizerStore{
authorizers: authorizers,
challenges: challenges,
}, nil
} }
// ModifyRequest adds authorization to the request func buildPingURL(endpoint string) string {
func (r *RequestAuthorizer) ModifyRequest(req *http.Request) error { return fmt.Sprintf("%s/v2/", endpoint)
for _, challenge := range r.challenges { }
for _, handler := range r.handlers {
if handler.Scheme() == challenge.Scheme { // Modify adds authorization to the request
if err := handler.AuthorizeRequest(req, challenge.Parameters); err != nil { func (a *AuthorizerStore) Modify(req *http.Request) error {
for _, challenge := range a.challenges {
for _, authorizer := range a.authorizers {
if authorizer.Scheme() == challenge.Scheme {
if err := authorizer.Authorize(req, challenge.Parameters); err != nil {
return err return err
} }
} }

View File

@ -19,14 +19,11 @@ import (
"net/http" "net/http"
au "github.com/docker/distribution/registry/client/auth" au "github.com/docker/distribution/registry/client/auth"
"github.com/vmware/harbor/utils/log"
) )
// ParseChallengeFromResponse ... // ParseChallengeFromResponse ...
func ParseChallengeFromResponse(resp *http.Response) []au.Challenge { func ParseChallengeFromResponse(resp *http.Response) []au.Challenge {
challenges := au.ResponseChallenges(resp) challenges := au.ResponseChallenges(resp)
log.Debugf("challenges: %v", challenges)
return challenges return challenges
} }

View File

@ -16,11 +16,13 @@
package auth package auth
import ( import (
"crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"os"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -42,8 +44,8 @@ func (s *scope) string() string {
type tokenGenerator func(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error) type tokenGenerator func(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error)
// Implements interface Handler // Implements interface Authorizer
type tokenHandler struct { type tokenAuthorizer struct {
scope *scope scope *scope
tg tokenGenerator tg tokenGenerator
cache string // cached token cache string // cached token
@ -53,12 +55,12 @@ type tokenHandler struct {
} }
// Scheme returns the scheme that the handler can handle // Scheme returns the scheme that the handler can handle
func (t *tokenHandler) Scheme() string { func (t *tokenAuthorizer) Scheme() string {
return "bearer" return "bearer"
} }
// AuthorizeRequest will add authorization header which contains a token before the request is sent // AuthorizeRequest will add authorization header which contains a token before the request is sent
func (t *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error { func (t *tokenAuthorizer) Authorize(req *http.Request, params map[string]string) error {
var scopes []*scope var scopes []*scope
var token string var token string
@ -100,26 +102,23 @@ func (t *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]str
if !hasFrom { if !hasFrom {
t.updateCachedToken(to, expiresIn, issuedAt) t.updateCachedToken(to, expiresIn, issuedAt)
log.Debug("add token to cache")
} }
} else { } else {
token = cachedToken token = cachedToken
log.Debug("get token from cache")
} }
req.Header.Add(http.CanonicalHeaderKey("Authorization"), fmt.Sprintf("Bearer %s", token)) req.Header.Add(http.CanonicalHeaderKey("Authorization"), fmt.Sprintf("Bearer %s", token))
log.Debugf("add token to request: %s %s", req.Method, req.URL.String())
return nil return nil
} }
func (t *tokenHandler) getCachedToken() (string, int, *time.Time) { func (t *tokenAuthorizer) getCachedToken() (string, int, *time.Time) {
t.Lock() t.Lock()
defer t.Unlock() defer t.Unlock()
return t.cache, t.expiresIn, t.issuedAt return t.cache, t.expiresIn, t.issuedAt
} }
func (t *tokenHandler) updateCachedToken(token string, expiresIn int, issuedAt *time.Time) { func (t *tokenAuthorizer) updateCachedToken(token string, expiresIn int, issuedAt *time.Time) {
t.Lock() t.Lock()
defer t.Unlock() defer t.Unlock()
t.cache = token t.cache = token
@ -127,38 +126,45 @@ func (t *tokenHandler) updateCachedToken(token string, expiresIn int, issuedAt *
t.issuedAt = issuedAt t.issuedAt = issuedAt
} }
// Implements interface Handler // Implements interface Authorizer
type standardTokenHandler struct { type standardTokenAuthorizer struct {
tokenHandler tokenAuthorizer
client *http.Client client *http.Client
credential Credential credential Credential
} }
// NewStandardTokenHandler returns a standard token handler. The handler will request a token // NewStandardTokenAuthorizer returns a standard token authorizer. The authorizer will request a token
// from token server and add it to the origin request // from token server and add it to the origin request
// TODO deal with https func NewStandardTokenAuthorizer(credential Credential, insecure bool, scopeType, scopeName string, scopeActions ...string) Authorizer {
func NewStandardTokenHandler(credential Credential, scopeType, scopeName string, scopeActions ...string) Handler { t := &http.Transport{
handler := &standardTokenHandler{ TLSClientConfig: &tls.Config{
InsecureSkipVerify: insecure,
},
}
authorizer := &standardTokenAuthorizer{
client: &http.Client{ client: &http.Client{
Transport: http.DefaultTransport, Transport: t,
}, },
credential: credential, credential: credential,
} }
if len(scopeType) != 0 || len(scopeName) != 0 { if len(scopeType) != 0 || len(scopeName) != 0 {
handler.scope = &scope{ authorizer.scope = &scope{
Type: scopeType, Type: scopeType,
Name: scopeName, Name: scopeName,
Actions: scopeActions, Actions: scopeActions,
} }
} }
handler.tg = handler.generateToken authorizer.tg = authorizer.generateToken
return handler return authorizer
} }
func (s *standardTokenHandler) generateToken(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error) { func (s *standardTokenAuthorizer) generateToken(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error) {
realm = tokenURL(realm)
u, err := url.Parse(realm) u, err := url.Parse(realm)
if err != nil { if err != nil {
return return
@ -217,37 +223,50 @@ func (s *standardTokenHandler) generateToken(realm, service string, scopes []str
} }
} }
log.Debug("get token from token server")
return return
} }
// when the registry client is used inside Harbor, the token request
// can be posted to token service directly rather than going through nginx.
// this solution can resolve two problems:
// 1. performance issue
// 2. the realm field returned by registry is an IP which can not reachable
// inside Harbor
func tokenURL(realm string) string {
extEndpoint := os.Getenv("EXT_ENDPOINT")
tokenURL := os.Getenv("TOKEN_URL")
if len(extEndpoint) != 0 && len(tokenURL) != 0 &&
strings.Contains(realm, extEndpoint) {
realm = strings.TrimRight(tokenURL, "/") + "/service/token"
}
return realm
}
// Implements interface Handler // Implements interface Handler
type usernameTokenHandler struct { type usernameTokenAuthorizer struct {
tokenHandler tokenAuthorizer
username string username string
} }
// NewUsernameTokenHandler returns a handler which will generate a token according to // NewUsernameTokenAuthorizer returns a authorizer which will generate a token according to
// the user's privileges // the user's privileges
func NewUsernameTokenHandler(username string, scopeType, scopeName string, scopeActions ...string) Handler { func NewUsernameTokenAuthorizer(username string, scopeType, scopeName string, scopeActions ...string) Authorizer {
handler := &usernameTokenHandler{ authorizer := &usernameTokenAuthorizer{
username: username, username: username,
} }
handler.scope = &scope{ authorizer.scope = &scope{
Type: scopeType, Type: scopeType,
Name: scopeName, Name: scopeName,
Actions: scopeActions, Actions: scopeActions,
} }
handler.tg = handler.generateToken authorizer.tg = authorizer.generateToken
return handler return authorizer
} }
func (u *usernameTokenHandler) generateToken(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error) { func (u *usernameTokenAuthorizer) generateToken(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error) {
token, expiresIn, issuedAt, err = token_util.GenTokenForUI(u.username, service, scopes) token, expiresIn, issuedAt, err = token_util.GenTokenForUI(u.username, service, scopes)
log.Debug("get token by calling GenTokenForUI directly")
return return
} }

View File

@ -0,0 +1,25 @@
/*
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 (
"net/http"
)
// Modifier modifies request
type Modifier interface {
Modify(*http.Request) error
}

View File

@ -16,21 +16,15 @@
package registry package registry
import ( import (
"crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"strings"
"github.com/vmware/harbor/utils/log"
"github.com/vmware/harbor/utils/registry/auth"
registry_error "github.com/vmware/harbor/utils/registry/error" registry_error "github.com/vmware/harbor/utils/registry/error"
) "github.com/vmware/harbor/utils/registry/utils"
const (
// UserAgent is used to decorate the request so it can be identified by webhook.
UserAgent string = "registry-client"
) )
// Registry holds information of a registry entity // Registry holds information of a registry entity
@ -41,9 +35,7 @@ type Registry struct {
// NewRegistry returns an instance of registry // NewRegistry returns an instance of registry
func NewRegistry(endpoint string, client *http.Client) (*Registry, error) { func NewRegistry(endpoint string, client *http.Client) (*Registry, error) {
endpoint = strings.TrimRight(endpoint, "/") u, err := utils.ParseEndpoint(endpoint)
u, err := url.Parse(endpoint)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -53,64 +45,30 @@ func NewRegistry(endpoint string, client *http.Client) (*Registry, error) {
client: client, client: client,
} }
log.Debugf("initialized a registry client: %s", endpoint)
return registry, nil return registry, nil
} }
// NewRegistryWithUsername returns a Registry instance which will authorize the request // NewRegistryWithModifiers returns an instance of Registry according to the modifiers
// according to the privileges of user func NewRegistryWithModifiers(endpoint string, insecure bool, modifiers ...Modifier) (*Registry, error) {
func NewRegistryWithUsername(endpoint, username string) (*Registry, error) { u, err := utils.ParseEndpoint(endpoint)
endpoint = strings.TrimRight(endpoint, "/")
u, err := url.Parse(endpoint)
if err != nil { if err != nil {
return nil, err return nil, err
} }
client, err := newClient(endpoint, username, nil, "registry", "catalog", "*") t := &http.Transport{
if err != nil { TLSClientConfig: &tls.Config{
return nil, err InsecureSkipVerify: insecure,
},
} }
registry := &Registry{ transport := NewTransport(t, modifiers...)
return &Registry{
Endpoint: u, Endpoint: u,
client: client, client: &http.Client{
} Transport: transport,
},
log.Debugf("initialized a registry client with username: %s %s", endpoint, username) }, nil
return registry, nil
}
// NewRegistryWithCredential returns a Registry instance which associate to a crendential.
// And Credential is essentially a decorator for client to docorate the request before sending it to the registry.
func NewRegistryWithCredential(endpoint string, credential auth.Credential) (*Registry, error) {
endpoint = strings.TrimSpace(endpoint)
endpoint = strings.TrimRight(endpoint, "/")
if !strings.HasPrefix(endpoint, "http://") &&
!strings.HasPrefix(endpoint, "https://") {
endpoint = "http://" + endpoint
}
u, err := url.Parse(endpoint)
if err != nil {
return nil, err
}
client, err := newClient(endpoint, "", credential, "", "", "")
if err != nil {
return nil, err
}
registry := &Registry{
Endpoint: u,
client: client,
}
log.Debugf("initialized a registry client with credential: %s", endpoint)
return registry, nil
} }
// Catalog ... // Catalog ...
@ -163,16 +121,6 @@ func (r *Registry) Ping() error {
resp, err := r.client.Do(req) resp, err := r.client.Do(req)
if err != nil { if err != nil {
// if urlErr, ok := err.(*url.Error); ok {
// if regErr, ok := urlErr.Err.(*registry_error.Error); ok {
// return &registry_error.Error{
// StatusCode: regErr.StatusCode,
// Detail: regErr.Detail,
// }
// }
// return urlErr.Err
// }
return parseError(err) return parseError(err)
} }
@ -196,32 +144,3 @@ func (r *Registry) Ping() error {
func buildCatalogURL(endpoint string) string { func buildCatalogURL(endpoint string) string {
return fmt.Sprintf("%s/v2/_catalog", endpoint) return fmt.Sprintf("%s/v2/_catalog", endpoint)
} }
func newClient(endpoint, username string, credential auth.Credential,
scopeType, scopeName string, scopeActions ...string) (*http.Client, error) {
endpoint = strings.TrimRight(endpoint, "/")
resp, err := http.Get(buildPingURL(endpoint))
if err != nil {
return nil, err
}
var handlers []auth.Handler
var handler auth.Handler
if credential != nil {
handler = auth.NewStandardTokenHandler(credential, scopeType, scopeName, scopeActions...)
} else {
handler = auth.NewUsernameTokenHandler(username, scopeType, scopeName, scopeActions...)
}
handlers = append(handlers, handler)
challenges := auth.ParseChallengeFromResponse(resp)
authorizer := auth.NewRequestAuthorizer(handlers, challenges)
headerModifier := NewHeaderModifier(map[string]string{http.CanonicalHeaderKey("User-Agent"): UserAgent})
transport := NewTransport(http.DefaultTransport, []RequestModifier{authorizer, headerModifier})
return &http.Client{
Transport: transport,
}, nil
}

View File

@ -17,6 +17,7 @@ package registry
import ( import (
"bytes" "bytes"
"crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@ -28,9 +29,9 @@ import (
"github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/manifest/schema2" "github.com/docker/distribution/manifest/schema2"
"github.com/vmware/harbor/utils/log"
"github.com/vmware/harbor/utils/registry/auth"
registry_error "github.com/vmware/harbor/utils/registry/error" registry_error "github.com/vmware/harbor/utils/registry/error"
"github.com/vmware/harbor/utils/registry/utils"
) )
// Repository holds information of a repository entity // Repository holds information of a repository entity
@ -40,14 +41,11 @@ type Repository struct {
client *http.Client client *http.Client
} }
// TODO add agent to header of request, notifications need it
// NewRepository returns an instance of Repository // NewRepository returns an instance of Repository
func NewRepository(name, endpoint string, client *http.Client) (*Repository, error) { func NewRepository(name, endpoint string, client *http.Client) (*Repository, error) {
name = strings.TrimSpace(name) name = strings.TrimSpace(name)
endpoint = strings.TrimRight(endpoint, "/")
u, err := url.Parse(endpoint) u, err := utils.ParseEndpoint(endpoint)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -61,55 +59,30 @@ func NewRepository(name, endpoint string, client *http.Client) (*Repository, err
return repository, nil return repository, nil
} }
// NewRepositoryWithCredential returns a Repository instance which will authorize the request // NewRepositoryWithModifiers returns an instance of Repository according to the modifiers
// according to the credenttial func NewRepositoryWithModifiers(name, endpoint string, insecure bool, modifiers ...Modifier) (*Repository, error) {
func NewRepositoryWithCredential(name, endpoint string, credential auth.Credential) (*Repository, error) {
name = strings.TrimSpace(name) name = strings.TrimSpace(name)
endpoint = strings.TrimRight(endpoint, "/")
u, err := url.Parse(endpoint) u, err := utils.ParseEndpoint(endpoint)
if err != nil { if err != nil {
return nil, err return nil, err
} }
client, err := newClient(endpoint, "", credential, "repository", name, "pull", "push") t := &http.Transport{
if err != nil { TLSClientConfig: &tls.Config{
return nil, err InsecureSkipVerify: insecure,
},
} }
repository := &Repository{ transport := NewTransport(t, modifiers...)
return &Repository{
Name: name, Name: name,
Endpoint: u, Endpoint: u,
client: client, client: &http.Client{
} Transport: transport,
},
log.Debugf("initialized a repository client with credential: %s %s", endpoint, name) }, nil
return repository, nil
}
// NewRepositoryWithUsername returns a Repository instance which will authorize the request
// according to the privileges of user
func NewRepositoryWithUsername(name, endpoint, username string) (*Repository, error) {
name = strings.TrimSpace(name)
endpoint = strings.TrimRight(endpoint, "/")
u, err := url.Parse(endpoint)
if err != nil {
return nil, err
}
client, err := newClient(endpoint, username, nil, "repository", name, "pull", "push")
repository := &Repository{
Name: name,
Endpoint: u,
client: client,
}
log.Debugf("initialized a repository client with username: %s %s %s", endpoint, name, username)
return repository, nil
} }
func parseError(err error) error { func parseError(err error) error {

View File

@ -26,7 +26,7 @@ import (
"time" "time"
"github.com/vmware/harbor/utils/registry/auth" "github.com/vmware/harbor/utils/registry/auth"
"github.com/vmware/harbor/utils/registry/error" registry_error "github.com/vmware/harbor/utils/registry/error"
) )
var ( var (
@ -139,7 +139,8 @@ func serveToken(w http.ResponseWriter, r *http.Request) {
} }
func TestListTag(t *testing.T) { func TestListTag(t *testing.T) {
client, err := NewRepositoryWithCredential(repo, registryServer.URL, credential) client, err := newRepositoryClient(registryServer.URL, true, credential,
repo, "repository", repo, "pull", "push", "*")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -158,13 +159,14 @@ func TestListTag(t *testing.T) {
func TestListTagWithInvalidCredential(t *testing.T) { func TestListTagWithInvalidCredential(t *testing.T) {
credential := auth.NewBasicAuthCredential(username, "wrong_password") credential := auth.NewBasicAuthCredential(username, "wrong_password")
client, err := NewRepositoryWithCredential(repo, registryServer.URL, credential) client, err := newRepositoryClient(registryServer.URL, true, credential,
repo, "repository", repo, "pull", "push", "*")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
if _, err = client.ListTag(); err != nil { if _, err = client.ListTag(); err != nil {
e, ok := err.(*error.Error) e, ok := err.(*registry_error.Error)
if ok && e.StatusCode == http.StatusUnauthorized { if ok && e.StatusCode == http.StatusUnauthorized {
return return
} }
@ -173,3 +175,20 @@ func TestListTagWithInvalidCredential(t *testing.T) {
return return
} }
} }
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
}
client, err := NewRepositoryWithModifiers(repository, endpoint, insecure, store)
if err != nil {
return nil, err
}
return client, nil
}

View File

@ -21,39 +21,14 @@ import (
"github.com/vmware/harbor/utils/log" "github.com/vmware/harbor/utils/log"
) )
// RequestModifier modifies request
type RequestModifier interface {
ModifyRequest(*http.Request) error
}
// HeaderModifier adds headers to request
type HeaderModifier struct {
headers map[string]string
}
// NewHeaderModifier ...
func NewHeaderModifier(headers map[string]string) *HeaderModifier {
return &HeaderModifier{
headers: headers,
}
}
// ModifyRequest adds headers to the request
func (h *HeaderModifier) ModifyRequest(req *http.Request) error {
for key, value := range h.headers {
req.Header.Add(key, value)
}
return nil
}
// Transport holds information about base transport and modifiers // Transport holds information about base transport and modifiers
type Transport struct { type Transport struct {
transport http.RoundTripper transport http.RoundTripper
modifiers []RequestModifier modifiers []Modifier
} }
// NewTransport ... // NewTransport ...
func NewTransport(transport http.RoundTripper, modifiers []RequestModifier) *Transport { func NewTransport(transport http.RoundTripper, modifiers ...Modifier) *Transport {
return &Transport{ return &Transport{
transport: transport, transport: transport,
modifiers: modifiers, modifiers: modifiers,
@ -63,7 +38,7 @@ func NewTransport(transport http.RoundTripper, modifiers []RequestModifier) *Tra
// RoundTrip ... // RoundTrip ...
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
for _, modifier := range t.modifiers { for _, modifier := range t.modifiers {
if err := modifier.ModifyRequest(req); err != nil { if err := modifier.Modify(req); err != nil {
return nil, err return nil, err
} }
} }

View File

@ -0,0 +1,44 @@
/*
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 utils
import (
"net/url"
"strings"
)
// FormatEndpoint formats endpoint
func FormatEndpoint(endpoint string) string {
endpoint = strings.TrimSpace(endpoint)
endpoint = strings.TrimRight(endpoint, "/")
if !strings.HasPrefix(endpoint, "http://") &&
!strings.HasPrefix(endpoint, "https://") {
endpoint = "http://" + endpoint
}
return endpoint
}
// ParseEndpoint parses endpoint to a URL
func ParseEndpoint(endpoint string) (*url.URL, error) {
endpoint = FormatEndpoint(endpoint)
u, err := url.Parse(endpoint)
if err != nil {
return nil, err
}
return u, nil
}

View File

@ -6,7 +6,7 @@
<h1 class="col-md-12 col-md-offset-2 main-title title-color">// 'forgot_password' | tr //</h1> <h1 class="col-md-12 col-md-offset-2 main-title title-color">// 'forgot_password' | tr //</h1>
<div class="row"> <div class="row">
<div class="col-md-12 col-md-offset-2 main-content"> <div class="col-md-12 col-md-offset-2 main-content">
<form name="form" class="form-horizontal css-form"> <form name="form" class="form-horizontal css-form" novalidate>
<div class="form-group"> <div class="form-group">
<label for="email" class="col-sm-3 control-label">// 'email' | tr //:</label> <label for="email" class="col-sm-3 control-label">// 'email' | tr //:</label>
<div class="col-sm-7"> <div class="col-sm-7">

View File

@ -4,6 +4,9 @@
<span class="glyphicon glyphicon-user"></span> {{ .Username }} <span class="glyphicon glyphicon-user"></span> {{ .Username }}
</a> </a>
<ul class="dropdown-menu multi-level" role="menu" aria-labelledby="dropdownMenu"> <ul class="dropdown-menu multi-level" role="menu" aria-labelledby="dropdownMenu">
{{ if eq .AddNew true }}
<li><a href="/add_new"><span class="glyphicon glyphicon-plus"></span> // 'add_new_title' | tr //</a></li>
{{ end }}
<li><a href="/account_setting"><span class="glyphicon glyphicon-pencil"></span> // 'account_setting' | tr //</a></li> <li><a href="/account_setting"><span class="glyphicon glyphicon-pencil"></span> // 'account_setting' | tr //</a></li>
<li class="dropdown-submenu"> <li class="dropdown-submenu">
<a tabindex="-1" href="#"><span class="glyphicon glyphicon-globe"></span> //vm.languageName//</a> <a tabindex="-1" href="#"><span class="glyphicon glyphicon-globe"></span> //vm.languageName//</a>

View File

@ -1,3 +1,3 @@
<div class="footer-absolute footer"> <div class="footer-absolute footer" ng-controller="FooterController as vm">
<p>Copyright © 2015-2016 VMware, Inc. All Rights Reserved.</p> <p>// 'copyright' | tr // © 2015-2016 VMware, Inc. // 'all_rights_reserved' | tr //</p>
</div> </div>

View File

@ -48,6 +48,9 @@
<script src="/static/resources/js/layout/header/header.module.js"></script> <script src="/static/resources/js/layout/header/header.module.js"></script>
<script src="/static/resources/js/layout/header/header.controller.js"></script> <script src="/static/resources/js/layout/header/header.controller.js"></script>
<script src="/static/resources/js/layout/footer/footer.module.js"></script>
<script src="/static/resources/js/layout/footer/footer.controller.js"></script>
<script src="/static/resources/js/layout/navigation/navigation.module.js"></script> <script src="/static/resources/js/layout/navigation/navigation.module.js"></script>
<script src="/static/resources/js/layout/navigation/navigation-header.directive.js"></script> <script src="/static/resources/js/layout/navigation/navigation-header.directive.js"></script>
<script src="/static/resources/js/layout/navigation/navigation-details.directive.js"></script> <script src="/static/resources/js/layout/navigation/navigation-details.directive.js"></script>
@ -56,6 +59,9 @@
<script src="/static/resources/js/layout/sign-up/sign-up.module.js"></script> <script src="/static/resources/js/layout/sign-up/sign-up.module.js"></script>
<script src="/static/resources/js/layout/sign-up/sign-up.controller.js"></script> <script src="/static/resources/js/layout/sign-up/sign-up.controller.js"></script>
<script src="/static/resources/js/layout/add-new/add-new.module.js"></script>
<script src="/static/resources/js/layout/add-new/add-new.controller.js"></script>
<script src="/static/resources/js/layout/account-setting/account-setting.module.js"></script> <script src="/static/resources/js/layout/account-setting/account-setting.module.js"></script>
<script src="/static/resources/js/layout/account-setting/account-setting.controller.js"></script> <script src="/static/resources/js/layout/account-setting/account-setting.controller.js"></script>
@ -151,6 +157,7 @@
<script src="/static/resources/js/services/destination/services.ping-destination.js"></script> <script src="/static/resources/js/services/destination/services.ping-destination.js"></script>
<script src="/static/resources/js/services/destination/services.update-destination.js"></script> <script src="/static/resources/js/services/destination/services.update-destination.js"></script>
<script src="/static/resources/js/services/destination/services.delete-destination.js"></script> <script src="/static/resources/js/services/destination/services.delete-destination.js"></script>
<script src="/static/resources/js/services/destination/services.list-destination-policy.js"></script>
<script src="/static/resources/js/session/session.module.js"></script> <script src="/static/resources/js/session/session.module.js"></script>
<script src="/static/resources/js/session/session.current-user.js"></script> <script src="/static/resources/js/session/session.current-user.js"></script>

View File

@ -32,10 +32,13 @@
<div class="col-sm-offset-1 col-sm-10"> <div class="col-sm-offset-1 col-sm-10">
<div class="pull-right"> <div class="pull-right">
<button type="submit" class="btn btn-default" ng-click="vm.doSignIn(user)">// 'sign_in' | tr //</button> <button type="submit" class="btn btn-default" ng-click="vm.doSignIn(user)">// 'sign_in' | tr //</button>
{{ if eq .AuthMode "db_auth" }}
<button type="button" class="btn btn-success" ng-click="vm.doSignUp()">// 'sign_up' | tr //</button> <button type="button" class="btn btn-success" ng-click="vm.doSignUp()">// 'sign_up' | tr //</button>
{{ end }}
</div> </div>
</div> </div>
</div> </div>
{{ if eq .AuthMode "db_auth" }}
<div class="form-group"> <div class="form-group">
<div class="col-sm-offset-1 col-sm-10"> <div class="col-sm-offset-1 col-sm-10">
<div class="pull-right"> <div class="pull-right">
@ -43,5 +46,6 @@
</div> </div>
</div> </div>
</div> </div>
{{ end }}
</form> </form>
{{ end }} {{ end }}

View File

@ -2,8 +2,18 @@
<div class="container container-custom"> <div class="container container-custom">
<div class="row extend-height"> <div class="row extend-height">
<div class="section"> <div class="section">
{{ if eq .AddNew true }}
<modal-dialog modal-title="// 'add_new_title' | tr //" modal-message="// 'successful_added' | tr //" confirm-only="true" action="vm.confirm()"></modal-dialog>
{{ else }}
<modal-dialog modal-title="// 'sign_up' | tr //" modal-message="// 'successful_signed_up' | tr //" confirm-only="true" action="vm.confirm()"></modal-dialog> <modal-dialog modal-title="// 'sign_up' | tr //" modal-message="// 'successful_signed_up' | tr //" confirm-only="true" action="vm.confirm()"></modal-dialog>
<h1 class="col-md-12 col-md-offset-2 main-title title-color">// 'sign_up' | tr //</h1> {{ end }}
<h1 class="col-md-12 col-md-offset-2 main-title title-color">
{{ if eq .AddNew true }}
// 'add_new_title' | tr //
{{ else }}
// 'sign_up' | tr //
{{ end }}
</h1>
<div class="row"> <div class="row">
<div class="col-md-12 col-md-offset-2 main-content"> <div class="col-md-12 col-md-offset-2 main-content">
<form name="form" class="form-horizontal css-form" ng-submit="form.$valid"> <form name="form" class="form-horizontal css-form" ng-submit="form.$valid">
@ -90,7 +100,11 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-md-offset-8 col-md-10"> <div class="col-md-offset-8 col-md-10">
<input type="submit" class="btn btn-success" ng-disabled="form.$invalid" ng-click="vm.signUp(user)" value="Sign Up"> {{ if eq .AddNew true }}
<input type="submit" class="btn btn-success" ng-disabled="form.$invalid" ng-click="vm.signUp(user)" value="// 'add_new' | tr //">
{{ else }}
<input type="submit" class="btn btn-success" ng-disabled="form.$invalid" ng-click="vm.signUp(user)" value="// 'sign_up' | tr //">
{{ end }}
</div> </div>
</div> </div>
</form> </form>