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,
user_id int NOT NULL,
project_id int NOT NULL,
repo_name varchar (40),
repo_tag varchar (20),
repo_name varchar (256),
repo_tag varchar (128),
GUID varchar(64),
operation varchar(20) NOT NULL,
op_time timestamp,
@ -159,4 +159,4 @@ CREATE TABLE IF NOT EXISTS `alembic_version` (
`version_num` varchar(32) NOT NULL
) 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
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.
#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.

View File

@ -47,6 +47,7 @@ crt_organizationalunit = rcp.get("configuration", "crt_organizationalunit")
crt_commonname = rcp.get("configuration", "crt_commonname")
crt_email = rcp.get("configuration", "crt_email")
max_job_workers = rcp.get("configuration", "max_job_workers")
verify_remote_cert = rcp.get("configuration", "verify_remote_cert")
########
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,
ui_secret=ui_secret,
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):
subj_list = [item for item in dirty_subj.strip().split("/") \

View File

@ -3,7 +3,10 @@ MYSQL_PORT=3306
MYSQL_USR=root
MYSQL_PWD=$db_password
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
LOG_LEVEL=debug
GODEBUG=netdns=cgo
EXT_ENDPOINT=$ui_url
TOKEN_URL=http://ui

View File

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

View File

@ -1,16 +1,16 @@
/*
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.
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package api
@ -165,7 +165,7 @@ func getRepoList(projectID int64) ([]string, error) {
uiPwd = "Harbor12345"
}
*/
uiURL := config.LocalHarborURL()
uiURL := config.LocalUIURL()
client := &http.Client{}
req, err := http.NewRequest("GET", uiURL+"/api/repositories?project_id="+strconv.Itoa(int(projectID)), nil)
if err != nil {

View File

@ -159,7 +159,7 @@ func (p *ProjectAPI) List() {
if len(isPublic) > 0 {
public, err = strconv.Atoi(isPublic)
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")
}
}

View File

@ -1,16 +1,16 @@
/*
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.
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package api
@ -121,6 +121,16 @@ func (pa *RepPolicyAPI) Post() {
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)
if err != nil {
log.Errorf("Failed to add policy to DB, error: %v", err)
@ -159,6 +169,7 @@ func (pa *RepPolicyAPI) Put() {
policy.ProjectID = originalPolicy.ProjectID
pa.Validate(policy)
// check duplicate name
if policy.Name != originalPolicy.Name {
po, err := dao.GetRepPolicyByName(policy.Name)
if err != nil {
@ -172,6 +183,12 @@ func (pa *RepPolicyAPI) Put() {
}
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)
if err != nil {
log.Errorf("failed to get target %d: %v", policy.TargetID, err)
@ -181,67 +198,95 @@ func (pa *RepPolicyAPI) Put() {
if target == nil {
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
isTargetChanged := !(policy.TargetID == originalPolicy.TargetID)
isEnablementChanged := !(policy.Enabled == policy.Enabled)
/*
isTargetChanged := !(policy.TargetID == originalPolicy.TargetID)
isEnablementChanged := !(policy.Enabled == policy.Enabled)
var shouldStop, shouldTrigger bool
var shouldStop, shouldTrigger bool
// if target and enablement are not changed, do nothing
if !isTargetChanged && !isEnablementChanged {
shouldStop = false
shouldTrigger = false
} else if !isTargetChanged && isEnablementChanged {
// target is not changed, but enablement is changed
if policy.Enabled == 0 {
shouldStop = true
shouldTrigger = false
} else {
shouldStop = false
shouldTrigger = true
}
} else if isTargetChanged && !isEnablementChanged {
// target is changed, but enablement is not changed
if policy.Enabled == 0 {
// enablement is 0, do nothing
// if target and enablement are not changed, do nothing
if !isTargetChanged && !isEnablementChanged {
shouldStop = false
shouldTrigger = false
} else if !isTargetChanged && isEnablementChanged {
// target is not changed, but enablement is changed
if policy.Enabled == 0 {
shouldStop = true
shouldTrigger = false
} else {
shouldStop = false
shouldTrigger = true
}
} else if isTargetChanged && !isEnablementChanged {
// target is changed, but enablement is not changed
if policy.Enabled == 0 {
// enablement is 0, do nothing
shouldStop = false
shouldTrigger = false
} else {
// enablement is 1, so stop original target's jobs
// and trigger new target's jobs
shouldStop = true
shouldTrigger = true
}
} else {
// enablement is 1, so stop original target's jobs
// and trigger new target's jobs
shouldStop = true
shouldTrigger = true
}
} else {
// both target and enablement are changed
// both target and enablement are changed
// enablement: 1 -> 0
if policy.Enabled == 0 {
shouldStop = true
shouldTrigger = false
} else {
shouldStop = false
shouldTrigger = true
// enablement: 1 -> 0
if policy.Enabled == 0 {
shouldStop = true
shouldTrigger = false
} else {
shouldStop = false
shouldTrigger = true
}
}
}
if shouldStop {
if err := postReplicationAction(id, "stop"); err != nil {
log.Errorf("failed to stop replication of %d: %v", id, err)
if shouldStop {
if err := postReplicationAction(id, "stop"); err != nil {
log.Errorf("failed to stop replication of %d: %v", id, err)
pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
log.Infof("replication of %d has been stopped", id)
}
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))
}
log.Infof("replication of %d has been stopped", id)
}
if shouldTrigger {
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)
}
}()
}
*/
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 shouldTrigger {
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)

View File

@ -255,11 +255,13 @@ func (ra *RepositoryAPI) GetManifests() {
func (ra *RepositoryAPI) initRepositoryClient(repoName string) (r *registry.Repository, err error) {
endpoint := os.Getenv("REGISTRY_URL")
// TODO read variable from config file
insecure := true
username, password, ok := ra.Ctx.Request.BasicAuth()
if ok {
credential := auth.NewBasicAuthCredential(username, password)
return registry.NewRepositoryWithCredential(repoName, endpoint, credential)
return newRepositoryClient(endpoint, insecure, username, password,
repoName, "repository", repoName, "pull", "push", "*")
}
username, err = ra.getUsername()
@ -267,7 +269,8 @@ func (ra *RepositoryAPI) initRepositoryClient(repoName string) (r *registry.Repo
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) {
@ -327,3 +330,21 @@ func (ra *RepositoryAPI) GetTopRepos() {
ra.Data["json"] = repos
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/utils"
"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"
registry_error "github.com/vmware/harbor/utils/registry/error"
)
@ -92,8 +92,10 @@ func (t *TargetAPI) Ping() {
password = t.GetString("password")
}
credential := auth.NewBasicAuthCredential(username, password)
registry, err := registry_util.NewRegistryWithCredential(endpoint, credential)
// TODO read variable from config file
insecure := true
registry, err := newRegistryClient(endpoint, insecure, username, password,
"", "", "")
if err != nil {
// timeout, dns resolve error, connection refused, etc.
if urlErr, ok := err.(*url.Error); ok {
@ -190,6 +192,16 @@ func (t *TargetAPI) Post() {
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 {
target.Password = utils.ReversibleEncrypt(target.Password)
}
@ -217,6 +229,24 @@ func (t *TargetAPI) Put() {
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{}
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
if len(target.Password) != 0 {
@ -273,3 +315,44 @@ func (t *TargetAPI) Delete() {
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")
var hasLoggedIn bool
var allowAddNew bool
if sessionUserID != nil {
hasLoggedIn = true
userID := sessionUserID.(int)
@ -31,7 +33,18 @@ func (omc *OptionalMenuController) Get() {
omc.CustomAbort(http.StatusUnauthorized, "")
}
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.TplName = "optional-menu.htm"
omc.Render()

View File

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

View File

@ -1,5 +1,9 @@
package controllers
import (
"net/http"
)
// SignUpController handles requests to /sign_up
type SignUpController struct {
BaseController
@ -7,5 +11,9 @@ type SignUpController struct {
// Get renders sign up page
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")
}

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) {
policy, err := GetRepPolicy(policyID)
if err != nil {

View File

@ -52,6 +52,20 @@ func GetRepTargetByName(name string) (*models.RepTarget, error) {
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 ...
func DeleteRepTarget(id int64) error {
o := GetOrmer()
@ -206,6 +220,20 @@ func GetRepPolicyByTarget(targetID int64) ([]*models.RepPolicy, error) {
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 ...
func UpdateRepPolicy(policy *models.RepPolicy) error {
o := GetOrmer()

View File

@ -1,16 +1,16 @@
/*
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.
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 config
@ -26,9 +26,11 @@ import (
const defaultMaxWorkers int = 10
var maxJobWorkers int
var localURL string
var localUIURL string
var localRegURL string
var logDir string
var uiSecret string
var verifyRemoteCert string
func init() {
maxWorkersEnv := os.Getenv("MAX_JOB_WORKERS")
@ -39,9 +41,14 @@ func init() {
maxJobWorkers = defaultMaxWorkers
}
localURL = os.Getenv("HARBOR_URL")
if len(localURL) == 0 {
localURL = "http://registry:5000/"
localRegURL = os.Getenv("REGISTRY_URL")
if len(localRegURL) == 0 {
localRegURL = "http://registry:5000"
}
localUIURL = os.Getenv("UI_URL")
if len(localUIURL) == 0 {
localUIURL = "http://ui"
}
logDir = os.Getenv("LOG_DIR")
@ -67,8 +74,15 @@ func init() {
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: 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: uiSecret: ******")
}
@ -78,9 +92,14 @@ func MaxJobWorkers() int {
return maxJobWorkers
}
// LocalHarborURL returns the local registry url, job service will use this URL to pull manifest and repository.
func LocalHarborURL() string {
return localURL
// LocalUIURL returns the local ui url, job service will use this URL to call API hosted on ui process
func LocalUIURL() string {
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
@ -92,3 +111,8 @@ func LogDir() string {
func UISecret() string {
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
import (
"fmt"
"io/ioutil"
"net/http"
"strings"
"github.com/vmware/harbor/models"
"github.com/vmware/harbor/utils/log"
"github.com/vmware/harbor/utils/registry"
"github.com/vmware/harbor/utils/registry/auth"
)
const (
@ -39,22 +36,35 @@ type Deleter struct {
dstUsr string // username ...
dstPwd string // username ...
insecure bool
dstClient *registry.Repository
logger *log.Logger
}
// 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{
repository: repository,
tags: tags,
dstURL: dstURL,
dstUsr: dstUsr,
dstPwd: dstPwd,
insecure: insecure,
dstClient: dstClient,
logger: logger,
}
deleter.logger.Infof("initialization completed: repository: %s, tags: %v, destination URL: %s, destination user: %s",
deleter.repository, deleter.tags, deleter.dstURL, deleter.dstUsr)
return deleter
return deleter, nil
}
// Exit ...
@ -64,25 +74,22 @@ func (d *Deleter) Exit() error {
// Enter deletes repository or tags
func (d *Deleter) Enter() (string, error) {
url := strings.TrimRight(d.dstURL, "/") + "/api/repositories/"
// delete repository
if len(d.tags) == 0 {
u := url + "?repo_name=" + d.repository
if err := del(u, d.dstUsr, d.dstPwd); 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)
tags, err := d.dstClient.ListTag()
if err != nil {
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
}
d.logger.Infof("repository %s on %s has been deleted", d.repository, d.dstURL)
return models.JobFinished, nil
d.tags = append(d.tags, tags...)
}
// delele tags
d.logger.Infof("tags %v will be deleted", 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)
return "", err
}
@ -92,28 +99,3 @@ func (d *Deleter) Enter() (string, error) {
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 ...
dstPwd string // password ...
insecure bool // whether skip secure check when using https
srcClient *registry.Repository
dstClient *registry.Repository
@ -75,7 +77,7 @@ type BaseHandler struct {
// InitBaseHandler initializes a BaseHandler: creating clients for source and destination registry,
// listing tags of the repository if parameter tags is nil.
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",
repository, tags, srcURL, dstURL, dstUsr)
@ -96,14 +98,16 @@ func InitBaseHandler(repository, srcURL, srcSecret,
c := &http.Cookie{Name: models.UISecretCookie, Value: srcSecret}
srcCred := auth.NewCookieCredential(c)
// 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 {
return nil, err
}
base.srcClient = srcClient
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 {
return nil, err
}
@ -416,3 +420,34 @@ func (m *ManifestPusher) Enter() (string, error) {
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

@ -1,16 +1,16 @@
/*
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.
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 job
@ -38,6 +38,7 @@ type RepJobParm struct {
Tags []string
Enabled int
Operation string
Insecure bool
}
// 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)
}
sm.Parms = &RepJobParm{
LocalRegURL: config.LocalHarborURL(),
LocalRegURL: config.LocalRegURL(),
Repository: job.Repository,
Tags: job.TagList,
Enabled: policy.Enabled,
Operation: job.Operation,
Insecure: !config.VerifyRemoteCert(),
}
if policy.Enabled == 0 {
//worker will cancel this job
@ -260,7 +262,7 @@ func (sm *SM) Reset(jid int64) error {
func addImgTransferTransition(sm *SM) error {
base, err := replication.InitBaseHandler(sm.Parms.Repository, sm.Parms.LocalRegURL, config.UISecret(),
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 {
return err
}
@ -274,8 +276,11 @@ func addImgTransferTransition(sm *SM) error {
}
func addImgDeleteTransition(sm *SM) error {
deleter := replication.NewDeleter(sm.Parms.Repository, sm.Parms.Tags, sm.Parms.TargetURL,
sm.Parms.TargetUsername, sm.Parms.TargetPassword, sm.Logger)
deleter, err := replication.NewDeleter(sm.Parms.Repository, sm.Parms.Tags, sm.Parms.TargetURL,
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(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 `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'"))
public = sa.Column(sa.Integer, nullable=False, server_default=sa.text("'0'"))
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,17 +21,16 @@ import (
"github.com/vmware/harbor/utils/log"
"github.com/vmware/harbor/utils/registry"
"github.com/vmware/harbor/utils/registry/auth"
"github.com/astaxie/beego/cache"
)
var (
// Cache is the global cache in system.
Cache cache.Cache
endpoint string
username string
registryClient *registry.Registry
repositoryClients map[string]*registry.Repository
Cache cache.Cache
endpoint string
username string
)
const catalogKey string = "catalog"
@ -45,23 +44,18 @@ func init() {
endpoint = os.Getenv("REGISTRY_URL")
username = "admin"
repositoryClients = make(map[string]*registry.Repository, 10)
}
// RefreshCatalogCache calls registry's API to get repository list and write it to cache.
func RefreshCatalogCache() error {
log.Debug("refreshing catalog cache...")
if registryClient == nil {
var err error
registryClient, err = registry.NewRegistryWithUsername(endpoint, username)
if err != nil {
log.Errorf("error occurred while initializing registry client used by cache: %v", err)
return err
}
registryClient, err := NewRegistryClient(endpoint, true, username,
"registry", "catalog", "*")
if err != nil {
return err
}
var err error
rs, err := registryClient.Catalog()
if err != nil {
return err
@ -70,15 +64,13 @@ func RefreshCatalogCache() error {
repos := []string{}
for _, repo := range rs {
rc, ok := repositoryClients[repo]
if !ok {
rc, err = registry.NewRepositoryWithUsername(repo, endpoint, username)
if err != nil {
log.Errorf("error occurred while initializing repository client used by cache: %s %v", repo, err)
continue
}
repositoryClients[repo] = rc
rc, err := NewRepositoryClient(endpoint, true, username,
repo, "repository", repo, "pull", "push", "*")
if err != nil {
log.Errorf("error occurred while initializing repository client used by cache: %s %v", repo, err)
continue
}
tags, err := rc.ListTag()
if err != nil {
log.Errorf("error occurred while list tag for %s: %v", repo, err)
@ -112,3 +104,38 @@ func GetRepoFromCache() ([]string, error) {
}
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/service/cache"
"github.com/vmware/harbor/utils/log"
"github.com/vmware/harbor/utils/registry"
"github.com/astaxie/beego"
)
@ -57,7 +56,7 @@ func (n *NotificationHandler) Post() {
matched = false
}
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
action = e.Action
repo = e.Target.Repository

View File

@ -14,13 +14,15 @@
vm.currentLanguage = I18nService().getCurrentLanguage();
vm.languageName = I18nService().getLanguageName(vm.currentLanguage);
I18nService().setCurrentLanguage(vm.currentLanguage);
console.log('current language:' + vm.languageName);
vm.supportLanguages = I18nService().getSupportLanguages();
vm.user = currentUser.get();
vm.setLanguage = setLanguage;
vm.logOut = logOut;
function setLanguage(language) {
I18nService().setCurrentLanguage(language);
$window.location.href = '/language?lang=' + language;

View File

@ -44,13 +44,13 @@
<div class="form-group col-md-12 form-group-custom">
<label for="destinationName" class="col-md-3 control-label">// 'name' | tr //:</label>
<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 class="form-group col-md-12 form-group-custom">
<label for="endpoint" class="col-md-3 control-label">// 'endpoint' | tr //:</label>
<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">
<span ng-message="required">// 'endpoint_is_required' | tr //</span>
</div>
@ -59,7 +59,7 @@
<div class="form-group col-md-12 form-group-custom">
<label for="username" class="col-md-3 control-label">// 'username' | tr //:</label>
<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">
<span ng-message="required">// 'username_is_required' | tr //</span>
</div>
@ -68,7 +68,7 @@
<div class="form-group col-md-12 form-group-custom">
<label for="password" class="col-md-3 control-label">// 'password' | tr //:</label>
<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">
<span ng-message="required">// 'password_is_required' | tr //</span>
</div>

View File

@ -6,9 +6,9 @@
.module('harbor.replication')
.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;
//Since can not set value for textarea by using vm
@ -33,6 +33,8 @@
vm.create = create;
vm.update = update;
vm.pingDestination = pingDestination;
vm.targetEditable = true;
$scope.$watch('vm.destinations', function(current) {
if(current) {
@ -42,24 +44,7 @@
vm1.password = vm1.selection.password;
}
});
$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) {
vm1.selection = item;
vm1.endpoint = item.endpoint;
@ -74,6 +59,8 @@
}
function addNew() {
vm.targetEditable = true;
$filter('tr')('add_new_policy', []);
vm0.name = '';
vm0.description = '';
vm0.enabled = true;
@ -81,6 +68,9 @@
function edit(policyId) {
console.log('Edit policy ID:' + policyId);
vm.policyId = policyId;
vm.targetEditable = true;
$filter('tr')('edit_policy', []);
ListReplicationPolicyService(policyId)
.success(listReplicationPolicySuccess)
.error(listReplicationPolicyFailed);
@ -129,12 +119,37 @@
function listDestinationFailed(data, status) {
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) {
console.log(data);
var replicationPolicy = data;
vm0.name = replicationPolicy.name;
vm0.description = replicationPolicy.description;
vm0.enabled = replicationPolicy.enabled == 1;
vm.targetId = replicationPolicy.target_id;
if(vm0.enabled) {
vm.targetEditable = false;
}else{
ListDestinationPolicyService(vm.targetId)
.success(listDestinationPolicySuccess)
.error(listDestinationPolicyFailed);
}
}
function listReplicationPolicyFailed(data, status) {
console.log('Failed list replication policy:' + data);
@ -145,7 +160,7 @@
}
function createReplicationPolicyFailed(data, status) {
if(status === 409) {
alert('Policy name already exists.');
alert($filter('tr')('policy_already_exists', []));
}
console.log('Failed create replication policy.');
}
@ -163,10 +178,10 @@
console.log('Failed update destination.');
}
function pingDestinationSuccess(data, status) {
alert('Successful ping target.');
alert($filter('tr')('successful_ping_target', []));
}
function pingDestinationFailed(data, status) {
alert('Failed ping target:' + data);
alert($filter('tr')('failed_ping_target', []) + ':' + data);
}
}
@ -190,11 +205,22 @@
function link(scope, element, attr, ctrl) {
element.find('#createPolicyModal').on('show.bs.modal', function() {
ctrl.prepareDestination();
scope.form.$setPristine();
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;
function save(form) {

View File

@ -11,7 +11,7 @@
<div class="form-group col-md-12 form-group-custom">
<label for="name" class="col-md-3 control-label">// 'name' | tr //:</label>
<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">
<span ng-message="required">// 'name_is_required' | tr //</span>
</div>
@ -20,7 +20,7 @@
<div class="form-group col-md-12 form-group-custom">
<label for="description" class="col-md-3 control-label">// 'endpoint' | tr //:</label>
<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">
<span ng-message="required">// 'endpoint_is_required' | tr //</span>
</div>
@ -29,7 +29,7 @@
<div class="form-group col-md-12 form-group-custom">
<label for="username" class="col-md-3 control-label">// 'username' | tr //:</label>
<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">
<span ng-message="required">// 'username_is_required' | tr //</span>
</div>
@ -38,7 +38,7 @@
<div class="form-group col-md-12 form-group-custom">
<label for="password" class="col-md-3 control-label">// 'password' | tr //:</label>
<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">
<span ng-message="required">// 'password_is_required' | tr //</span>
</div>

View File

@ -6,9 +6,9 @@
.module('harbor.system.management')
.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;
$scope.destination = {};
@ -20,25 +20,11 @@
vm.update = update;
vm.pingDestination = pingDestination;
$scope.$watch('vm.action+","+vm.targetId', function(current) {
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;
}
}
});
vm.editable = true;
function addNew() {
vm.editable = true;
vm.modalTitle = $filter('tr')('add_new_destination', []);
vm0.name = '';
vm0.endpoint = '';
vm0.username = '';
@ -46,7 +32,11 @@
}
function edit(targetId) {
getDestination(targetId);
vm.editable = true;
vm.modalTitle = $filter('tr')('edit_destination', []);
ListDestinationService(targetId)
.success(getDestinationSuccess)
.error(getDestinationFailed);
}
function create(destination) {
@ -63,7 +53,7 @@
function createDestinationFailed(data, status) {
if(status === 409) {
alert('Destination already exists.');
alert($filter('tr')('destination_already_exists', []));
}
console.log('Failed create destination:' + data);
}
@ -83,11 +73,6 @@
console.log('Failed update destination.');
}
function getDestination(targetId) {
ListDestinationService(targetId)
.success(getDestinationSuccess)
.error(getDestinationFailed);
}
function getDestinationSuccess(data, status) {
var destination = data;
@ -95,12 +80,29 @@
vm0.endpoint = destination.endpoint;
vm0.username = destination.username;
vm0.password = destination.password;
ListDestinationPolicyService(destination.id)
.success(listDestinationPolicySuccess)
.error(listDestinationPolicyFailed);
}
function getDestinationFailed(data, status) {
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() {
var target = {
'name': vm0.name,
@ -113,10 +115,10 @@
.error(pingDestinationFailed);
}
function pingDestinationSuccess(data, status) {
alert('Successful ping target.');
alert($filter('tr')('successful_ping_target', []));
}
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) {
element.find('#createDestinationModal').on('show.bs.modal', function() {
scope.form.$setPristine();
scope.form.$setUntouched();
switch(ctrl.action) {
case 'ADD_NEW':
ctrl.addNew();
break;
case 'EDIT':
ctrl.edit(ctrl.targetId);
break;
}
scope.$apply();
});
ctrl.save = save;

View File

@ -6,9 +6,9 @@
.module('harbor.system.management')
.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;
vm.retrieve = retrieve;
@ -66,6 +66,7 @@
function deleteDestinationFailed(data, status) {
console.log('Failed delete destination.');
alert($filter('tr')('failed_delete_destination', []) + ':' + data);
}
}

View File

@ -7,8 +7,10 @@
'ngCookies',
'harbor.session',
'harbor.layout.header',
'harbor.layout.footer',
'harbor.layout.navigation',
'harbor.layout.sign.up',
'harbor.layout.add.new',
'harbor.layout.account.setting',
'harbor.layout.forgot.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,7 +39,12 @@
}
function confirm() {
$window.location.href = '/';
if(location.pathname === '/add_new') {
$window.location.href = '/dashboard';
}else{
$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',
'test_connection': 'Test connection',
'add_new_destination': 'New Destination',
'edit_destination': 'Edit Destination',
'successful_changed_password': 'Password has been changed successfully.',
'change_profile': 'Change Profile',
'successful_changed_profile': 'User profile has been changed successfully.',
@ -172,5 +173,15 @@ var locale_messages = {
'send': 'Send',
'successful_signed_up': 'Signed up successfully.',
'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为必填项。',
'test_connection': '测试连接',
'add_new_destination': '新建目标',
'edit_destination': '编辑目标',
'successful_changed_password': '修改密码操作成功。',
'change_profile': '修改个人信息',
'successful_changed_profile': '修改个人信息操作成功。',
'form_is_invalid': '表单内容无效',
'form_is_invalid': '表单内容无sign_up效',
'form_is_invalid_message': '表单内容无效,请填写必填字段。',
'administrator': '管理员',
'popular_repositories': '热门镜像仓库',
@ -171,5 +172,15 @@ var locale_messages = {
'send': '发送',
'successful_signed_up': '注册成功。',
'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'];
function I18nService($cookies, $window) {
var cookieOptions = {'path': '/'};
var messages = $.extend(true, {}, eval('locale_messages'));
var defaultLanguage = navigator.language || 'en-US';
var defaultLanguage = 'en-US';
var supportLanguages = {
'en-US': 'English',
'zh-CN': '中文'
@ -24,6 +26,7 @@
}
return false;
};
return tr;
function tr() {
@ -45,6 +48,7 @@
if(!angular.isDefined(language) || !isSupportLanguage(language)) {
language = defaultLanguage;
}
$cookies.put('language', language, cookieOptions);
return supportLanguages[language];
},
'getSupportLanguages': function() {

View File

@ -35,6 +35,7 @@ func initRouters() {
beego.Router("/project", &controllers.ProjectController{})
beego.Router("/repository", &controllers.RepositoryController{})
beego.Router("/sign_up", &controllers.SignUpController{})
beego.Router("/add_new", &controllers.AddNewController{})
beego.Router("/account_setting", &controllers.AccountSettingController{})
beego.Router("/admin_option", &controllers.AdminOptionController{})
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{}, "post:Post")
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/users/:id/sysadmin", &api.UserAPI{}, "put:ToggleUserAdminRole")
beego.Router("/api/repositories/top", &api.RepositoryAPI{}, "get:GetTopRepos")

View File

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

View File

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

View File

@ -16,11 +16,13 @@
package auth
import (
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
"sync"
"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)
// Implements interface Handler
type tokenHandler struct {
// Implements interface Authorizer
type tokenAuthorizer struct {
scope *scope
tg tokenGenerator
cache string // cached token
@ -53,12 +55,12 @@ type tokenHandler struct {
}
// Scheme returns the scheme that the handler can handle
func (t *tokenHandler) Scheme() string {
func (t *tokenAuthorizer) Scheme() string {
return "bearer"
}
// 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 token string
@ -100,26 +102,23 @@ func (t *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]str
if !hasFrom {
t.updateCachedToken(to, expiresIn, issuedAt)
log.Debug("add token to cache")
}
} else {
token = cachedToken
log.Debug("get token from cache")
}
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
}
func (t *tokenHandler) getCachedToken() (string, int, *time.Time) {
func (t *tokenAuthorizer) getCachedToken() (string, int, *time.Time) {
t.Lock()
defer t.Unlock()
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()
defer t.Unlock()
t.cache = token
@ -127,38 +126,45 @@ func (t *tokenHandler) updateCachedToken(token string, expiresIn int, issuedAt *
t.issuedAt = issuedAt
}
// Implements interface Handler
type standardTokenHandler struct {
tokenHandler
// Implements interface Authorizer
type standardTokenAuthorizer struct {
tokenAuthorizer
client *http.Client
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
// TODO deal with https
func NewStandardTokenHandler(credential Credential, scopeType, scopeName string, scopeActions ...string) Handler {
handler := &standardTokenHandler{
func NewStandardTokenAuthorizer(credential Credential, insecure bool, scopeType, scopeName string, scopeActions ...string) Authorizer {
t := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: insecure,
},
}
authorizer := &standardTokenAuthorizer{
client: &http.Client{
Transport: http.DefaultTransport,
Transport: t,
},
credential: credential,
}
if len(scopeType) != 0 || len(scopeName) != 0 {
handler.scope = &scope{
authorizer.scope = &scope{
Type: scopeType,
Name: scopeName,
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)
if err != nil {
return
@ -217,37 +223,50 @@ func (s *standardTokenHandler) generateToken(realm, service string, scopes []str
}
}
log.Debug("get token from token server")
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
type usernameTokenHandler struct {
tokenHandler
type usernameTokenAuthorizer struct {
tokenAuthorizer
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
func NewUsernameTokenHandler(username string, scopeType, scopeName string, scopeActions ...string) Handler {
handler := &usernameTokenHandler{
func NewUsernameTokenAuthorizer(username string, scopeType, scopeName string, scopeActions ...string) Authorizer {
authorizer := &usernameTokenAuthorizer{
username: username,
}
handler.scope = &scope{
authorizer.scope = &scope{
Type: scopeType,
Name: scopeName,
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)
log.Debug("get token by calling GenTokenForUI directly")
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
import (
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"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"
)
const (
// UserAgent is used to decorate the request so it can be identified by webhook.
UserAgent string = "registry-client"
"github.com/vmware/harbor/utils/registry/utils"
)
// Registry holds information of a registry entity
@ -41,9 +35,7 @@ type Registry struct {
// NewRegistry returns an instance of registry
func NewRegistry(endpoint string, client *http.Client) (*Registry, error) {
endpoint = strings.TrimRight(endpoint, "/")
u, err := url.Parse(endpoint)
u, err := utils.ParseEndpoint(endpoint)
if err != nil {
return nil, err
}
@ -53,64 +45,30 @@ func NewRegistry(endpoint string, client *http.Client) (*Registry, error) {
client: client,
}
log.Debugf("initialized a registry client: %s", endpoint)
return registry, nil
}
// NewRegistryWithUsername returns a Registry instance which will authorize the request
// according to the privileges of user
func NewRegistryWithUsername(endpoint, username string) (*Registry, error) {
endpoint = strings.TrimRight(endpoint, "/")
u, err := url.Parse(endpoint)
// NewRegistryWithModifiers returns an instance of Registry according to the modifiers
func NewRegistryWithModifiers(endpoint string, insecure bool, modifiers ...Modifier) (*Registry, error) {
u, err := utils.ParseEndpoint(endpoint)
if err != nil {
return nil, err
}
client, err := newClient(endpoint, username, nil, "registry", "catalog", "*")
if err != nil {
return nil, err
t := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: insecure,
},
}
registry := &Registry{
transport := NewTransport(t, modifiers...)
return &Registry{
Endpoint: u,
client: client,
}
log.Debugf("initialized a registry client with username: %s %s", endpoint, username)
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
client: &http.Client{
Transport: transport,
},
}, nil
}
// Catalog ...
@ -163,16 +121,6 @@ func (r *Registry) Ping() error {
resp, err := r.client.Do(req)
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)
}
@ -196,32 +144,3 @@ func (r *Registry) Ping() error {
func buildCatalogURL(endpoint string) string {
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 (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io"
@ -28,9 +29,9 @@ import (
"github.com/docker/distribution/manifest/schema1"
"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"
"github.com/vmware/harbor/utils/registry/utils"
)
// Repository holds information of a repository entity
@ -40,14 +41,11 @@ type Repository struct {
client *http.Client
}
// TODO add agent to header of request, notifications need it
// NewRepository returns an instance of Repository
func NewRepository(name, endpoint string, client *http.Client) (*Repository, error) {
name = strings.TrimSpace(name)
endpoint = strings.TrimRight(endpoint, "/")
u, err := url.Parse(endpoint)
u, err := utils.ParseEndpoint(endpoint)
if err != nil {
return nil, err
}
@ -61,55 +59,30 @@ func NewRepository(name, endpoint string, client *http.Client) (*Repository, err
return repository, nil
}
// NewRepositoryWithCredential returns a Repository instance which will authorize the request
// according to the credenttial
func NewRepositoryWithCredential(name, endpoint string, credential auth.Credential) (*Repository, error) {
// NewRepositoryWithModifiers returns an instance of Repository according to the modifiers
func NewRepositoryWithModifiers(name, endpoint string, insecure bool, modifiers ...Modifier) (*Repository, error) {
name = strings.TrimSpace(name)
endpoint = strings.TrimRight(endpoint, "/")
u, err := url.Parse(endpoint)
u, err := utils.ParseEndpoint(endpoint)
if err != nil {
return nil, err
}
client, err := newClient(endpoint, "", credential, "repository", name, "pull", "push")
if err != nil {
return nil, err
t := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: insecure,
},
}
repository := &Repository{
transport := NewTransport(t, modifiers...)
return &Repository{
Name: name,
Endpoint: u,
client: client,
}
log.Debugf("initialized a repository client with credential: %s %s", endpoint, name)
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
client: &http.Client{
Transport: transport,
},
}, nil
}
func parseError(err error) error {

View File

@ -26,7 +26,7 @@ import (
"time"
"github.com/vmware/harbor/utils/registry/auth"
"github.com/vmware/harbor/utils/registry/error"
registry_error "github.com/vmware/harbor/utils/registry/error"
)
var (
@ -139,7 +139,8 @@ func serveToken(w http.ResponseWriter, r *http.Request) {
}
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 {
t.Error(err)
}
@ -158,13 +159,14 @@ func TestListTag(t *testing.T) {
func TestListTagWithInvalidCredential(t *testing.T) {
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 {
t.Error(err)
}
if _, err = client.ListTag(); err != nil {
e, ok := err.(*error.Error)
e, ok := err.(*registry_error.Error)
if ok && e.StatusCode == http.StatusUnauthorized {
return
}
@ -173,3 +175,20 @@ func TestListTagWithInvalidCredential(t *testing.T) {
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"
)
// 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
type Transport struct {
transport http.RoundTripper
modifiers []RequestModifier
modifiers []Modifier
}
// NewTransport ...
func NewTransport(transport http.RoundTripper, modifiers []RequestModifier) *Transport {
func NewTransport(transport http.RoundTripper, modifiers ...Modifier) *Transport {
return &Transport{
transport: transport,
modifiers: modifiers,
@ -63,7 +38,7 @@ func NewTransport(transport http.RoundTripper, modifiers []RequestModifier) *Tra
// RoundTrip ...
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
for _, modifier := range t.modifiers {
if err := modifier.ModifyRequest(req); err != nil {
if err := modifier.Modify(req); err != nil {
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>
<div class="row">
<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">
<label for="email" class="col-sm-3 control-label">// 'email' | tr //:</label>
<div class="col-sm-7">

View File

@ -4,6 +4,9 @@
<span class="glyphicon glyphicon-user"></span> {{ .Username }}
</a>
<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 class="dropdown-submenu">
<a tabindex="-1" href="#"><span class="glyphicon glyphicon-globe"></span> //vm.languageName//</a>

View File

@ -1,3 +1,3 @@
<div class="footer-absolute footer">
<p>Copyright © 2015-2016 VMware, Inc. All Rights Reserved.</p>
<div class="footer-absolute footer" ng-controller="FooterController as vm">
<p>// 'copyright' | tr // © 2015-2016 VMware, Inc. // 'all_rights_reserved' | tr //</p>
</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.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-header.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.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.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.update-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.current-user.js"></script>

View File

@ -32,10 +32,13 @@
<div class="col-sm-offset-1 col-sm-10">
<div class="pull-right">
<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>
{{ end }}
</div>
</div>
</div>
{{ if eq .AuthMode "db_auth" }}
<div class="form-group">
<div class="col-sm-offset-1 col-sm-10">
<div class="pull-right">
@ -43,5 +46,6 @@
</div>
</div>
</div>
{{ end }}
</form>
{{ end }}

View File

@ -2,8 +2,18 @@
<div class="container container-custom">
<div class="row extend-height">
<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>
<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="col-md-12 col-md-offset-2 main-content">
<form name="form" class="form-horizontal css-form" ng-submit="form.$valid">
@ -90,7 +100,11 @@
</div>
<div class="form-group">
<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>
</form>