diff --git a/Deploy/db/registry.sql b/Deploy/db/registry.sql index 2d32b4cbb..66bb219e6 100644 --- a/Deploy/db/registry.sql +++ b/Deploy/db/registry.sql @@ -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'); diff --git a/Deploy/harbor.cfg b/Deploy/harbor.cfg index 7cd7548eb..e1acfa6aa 100644 --- a/Deploy/harbor.cfg +++ b/Deploy/harbor.cfg @@ -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. diff --git a/Deploy/prepare b/Deploy/prepare index d0afc365c..d861f58e3 100755 --- a/Deploy/prepare +++ b/Deploy/prepare @@ -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("/") \ diff --git a/Deploy/templates/jobservice/env b/Deploy/templates/jobservice/env index 079cc3670..beb75a11d 100644 --- a/Deploy/templates/jobservice/env +++ b/Deploy/templates/jobservice/env @@ -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 diff --git a/Deploy/templates/ui/env b/Deploy/templates/ui/env index de50c4a76..5098fa1a7 100644 --- a/Deploy/templates/ui/env +++ b/Deploy/templates/ui/env @@ -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 diff --git a/api/jobs/replication.go b/api/jobs/replication.go index d62d4e4fe..0dee3765b 100644 --- a/api/jobs/replication.go +++ b/api/jobs/replication.go @@ -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 { diff --git a/api/project.go b/api/project.go index 3d57abb02..da26b8a0a 100644 --- a/api/project.go +++ b/api/project.go @@ -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") } } diff --git a/api/replication_policy.go b/api/replication_policy.go index 4f851a343..be24aa0e9 100644 --- a/api/replication_policy.go +++ b/api/replication_policy.go @@ -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) diff --git a/api/repository.go b/api/repository.go index 1296a3e5b..b4b36c090 100644 --- a/api/repository.go +++ b/api/repository.go @@ -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 +} diff --git a/api/target.go b/api/target.go index 089771c2b..2271b142f 100644 --- a/api/target.go +++ b/api/target.go @@ -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() +} diff --git a/controllers/addnew.go b/controllers/addnew.go new file mode 100644 index 000000000..2bf6f1d57 --- /dev/null +++ b/controllers/addnew.go @@ -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.") +} diff --git a/controllers/optionalmenu.go b/controllers/optionalmenu.go index 21118c7f1..ac4cc3098 100644 --- a/controllers/optionalmenu.go +++ b/controllers/optionalmenu.go @@ -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() diff --git a/controllers/signin.go b/controllers/signin.go index 4adfd0942..8f70acf32 100644 --- a/controllers/signin.go +++ b/controllers/signin.go @@ -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" diff --git a/controllers/signup.go b/controllers/signup.go index 97214aab1..4f2cd2b4f 100644 --- a/controllers/signup.go +++ b/controllers/signup.go @@ -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") } diff --git a/dao/dao_test.go b/dao/dao_test.go index 1adbce2c0..7742a3dda 100644 --- a/dao/dao_test.go +++ b/dao/dao_test.go @@ -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 { diff --git a/dao/replication_job.go b/dao/replication_job.go index f1f533b4d..98842db38 100644 --- a/dao/replication_job.go +++ b/dao/replication_job.go @@ -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() diff --git a/job/config/config.go b/job/config/config.go index fb5d86fbd..d97292979 100644 --- a/job/config/config.go +++ b/job/config/config.go @@ -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" +} diff --git a/job/replication/delete.go b/job/replication/delete.go index cc4fd0d5a..8bf7813f8 100644 --- a/job/replication/delete.go +++ b/job/replication/delete.go @@ -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)) -} diff --git a/job/replication/transfer.go b/job/replication/transfer.go index 5e63f9fcb..0851b7fb9 100644 --- a/job/replication/transfer.go +++ b/job/replication/transfer.go @@ -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 +} diff --git a/job/statemachine.go b/job/statemachine.go index 94c892a70..959a9f797 100644 --- a/job/statemachine.go +++ b/job/statemachine.go @@ -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}) diff --git a/migration/changelog.md b/migration/changelog.md index ab14c17d5..888aeb5a6 100644 --- a/migration/changelog.md +++ b/migration/changelog.md @@ -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` diff --git a/migration/db_meta.py b/migration/db_meta.py index dcbdd4311..fecb2aed3 100644 --- a/migration/db_meta.py +++ b/migration/db_meta.py @@ -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")) + diff --git a/migration/migration_harbor/versions/0_2_0.py b/migration/migration_harbor/versions/0_2_0.py new file mode 100644 index 000000000..79ff28be9 --- /dev/null +++ b/migration/migration_harbor/versions/0_2_0.py @@ -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 diff --git a/service/cache/cache.go b/service/cache/cache.go index 869afc54a..c7b2c005d 100644 --- a/service/cache/cache.go +++ b/service/cache/cache.go @@ -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 +} diff --git a/service/notification.go b/service/notification.go index 20efcb42d..2bd391924 100644 --- a/service/notification.go +++ b/service/notification.go @@ -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 diff --git a/static/resources/js/components/optional-menu/optional-menu.directive.js b/static/resources/js/components/optional-menu/optional-menu.directive.js index c477ee15f..1fd270e04 100644 --- a/static/resources/js/components/optional-menu/optional-menu.directive.js +++ b/static/resources/js/components/optional-menu/optional-menu.directive.js @@ -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; diff --git a/static/resources/js/components/replication/create-policy.directive.html b/static/resources/js/components/replication/create-policy.directive.html index b77da9eab..75fd8f35d 100644 --- a/static/resources/js/components/replication/create-policy.directive.html +++ b/static/resources/js/components/replication/create-policy.directive.html @@ -44,13 +44,13 @@