diff --git a/AUTHORS b/AUTHORS index e03722c33..f2416e0c0 100644 --- a/AUTHORS +++ b/AUTHORS @@ -5,6 +5,7 @@ Alexey Erkak Allen Heavey Amanda Zhang Benniu Ji +Bin Liu Bobby Zhang Chaofeng Wu Daniel Jiang diff --git a/Deploy/templates/ui/app.conf b/Deploy/templates/ui/app.conf index 090bcdc47..f75b673c8 100644 --- a/Deploy/templates/ui/app.conf +++ b/Deploy/templates/ui/app.conf @@ -2,8 +2,8 @@ appname = registry runmode = dev [lang] -types = en-US|zh-CN|de-DE|ru-RU -names = en-US|zh-CN|de-DE|ru-RU +types = en-US|zh-CN|de-DE|ru-RU|ja-JP +names = en-US|zh-CN|de-DE|ru-RU|ja-JP [dev] httpport = 80 diff --git a/README.md b/README.md index 479707edd..1d76c2964 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Project Harbor is an enterprise-class registry server, which extends the open so * **Graphical user portal**: User can easily browse, search Docker repositories, manage projects/namespaces. * **AD/LDAP support**: Harbor integrates with existing enterprise AD/LDAP for user authentication and management. * **Auditing**: All the operations to the repositories are tracked. -* **Internationalization**: Already localized for English, Chinese, German and Russian. More languages can be added. +* **Internationalization**: Already localized for English, Chinese, German, Japanese and Russian. More languages can be added. * **RESTful API**: RESTful APIs for most administrative operations, easing intergration with external management platforms. ### Getting Started @@ -67,7 +67,7 @@ Harbor is available under the [Apache 2 license](LICENSE).     CaiCloud ### Users -MaDaiLiCai +MaDaiLiCai Dianrong ### Supporting Technologies beego Harbor is powered by Beego, an open source framework to build and develop applications in the Go way. diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 000000000..ed4e382c0 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,47 @@ +## Harbor Roadmap + +### About this document + +This document provides description of items that are gathered from the community and planned in Harbor's roadmap. This should serve as a reference point for Harbor users and contributors to understand where the project is heading, and help determine if a contribution could be conflicting with a longer term plan. + +### How to help? + +Discussion on the roadmap can take place in threads under [Issues](https://github.com/vmware/harbor/issues). Please open and comment on an issue if you want to provide suggestions and feedback to an item in the roadmap. Please review the roadmap to avoid potential duplicated effort. + +### How to add an item to the roadmap? +Please open an issue to track any initiative on the roadmap of Harbor. We will work with and rely on our community to focus our efforts to improve Harbor. + + +--- + + +### 1. Image replication between Harbor instances +Enable images to be replicated between two or more Harbor instances. This is useful to have multiple registry servers servicing a large cluster of nodes, or have distributed registry instances with identical images. + +### 2. Image deletion and garbage collection +a) Images can be deleted from UI. The files of deleted images are not removed immediately. + +b) The files of deleted images are recycled by an administrator during system maintenance(Garbage collection). The registry service must be shut down during the process of garbage collection. + + +### 3. Authentication (OAuth2) +In addition to LDAP/AD and local users, OAuth 2.0 can be used to authenticate a user. + +### 4. High Availability +Support multi-node deployment of Harbor for high availability, scalability and load-balancing purposes. + +### 5. Statistics and description for repositories +User can add a description to a repository. The access count of a repo can be aggregated and displayed. + + +### 6. Audit all operations in the system +Currently only image related operations are logged. Other operations in Harbor, such as user creation/deletion, role changes, password reset, should be tracked as well. + + +### 7. Migration tool to move from an existing registry to Harbor +A tool to migrate images from a vanilla registry server to Harbor, without the need to export/import a large amount of data. + + +### 8. Support API versioning +Provide versioning of Harbor's API. + diff --git a/api/base.go b/api/base.go index f2529b61e..7a7ff80df 100644 --- a/api/base.go +++ b/api/base.go @@ -17,8 +17,10 @@ package api import ( "encoding/json" + "fmt" "net/http" + "github.com/astaxie/beego/validation" "github.com/vmware/harbor/auth" "github.com/vmware/harbor/dao" "github.com/vmware/harbor/models" @@ -51,6 +53,30 @@ func (b *BaseAPI) DecodeJSONReq(v interface{}) { } } +// Validate validates v if it implements interface validation.ValidFormer +func (b *BaseAPI) Validate(v interface{}) { + validator := validation.Validation{} + isValid, err := validator.Valid(v) + if err != nil { + log.Errorf("failed to validate: %v", err) + b.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + } + + if !isValid { + message := "" + for _, e := range validator.Errors { + message += fmt.Sprintf("%s %s \n", e.Field, e.Message) + } + b.CustomAbort(http.StatusBadRequest, message) + } +} + +// DecodeJSONReqAndValidate does both decoding and validation +func (b *BaseAPI) DecodeJSONReqAndValidate(v interface{}) { + b.DecodeJSONReq(v) + b.Validate(v) +} + // ValidateUser checks if the request triggered by a valid user func (b *BaseAPI) ValidateUser() int { diff --git a/api/replication_policy.go b/api/replication_policy.go index 10906ba39..57a7c91a2 100644 --- a/api/replication_policy.go +++ b/api/replication_policy.go @@ -69,9 +69,40 @@ func (pa *RepPolicyAPI) Get() { // Post creates a policy, and if it is enbled, the replication will be triggered right now. func (pa *RepPolicyAPI) Post() { - policy := models.RepPolicy{} - pa.DecodeJSONReq(&policy) - pid, err := dao.AddRepPolicy(policy) + policy := &models.RepPolicy{} + pa.DecodeJSONReqAndValidate(policy) + + po, err := dao.GetRepPolicyByName(policy.Name) + if err != nil { + log.Errorf("failed to get policy %s: %v", policy.Name, err) + pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + } + + if po != nil { + pa.CustomAbort(http.StatusConflict, "name is already used") + } + + project, err := dao.GetProjectByID(policy.ProjectID) + if err != nil { + log.Errorf("failed to get project %d: %v", policy.ProjectID, err) + pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + } + + if project == nil { + pa.CustomAbort(http.StatusBadRequest, fmt.Sprintf("project %d does not exist", policy.ProjectID)) + } + + target, err := dao.GetRepTarget(policy.TargetID) + if err != nil { + log.Errorf("failed to get target %d: %v", policy.TargetID, err) + pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + } + + if target == nil { + pa.CustomAbort(http.StatusBadRequest, fmt.Sprintf("target %d does not exist", policy.TargetID)) + } + + pid, err := dao.AddRepPolicy(*policy) if err != nil { log.Errorf("Failed to add policy to DB, error: %v", err) pa.RenderError(http.StatusInternalServerError, "Internal Error") diff --git a/api/target.go b/api/target.go index e159b5779..9a9366cf1 100644 --- a/api/target.go +++ b/api/target.go @@ -164,10 +164,16 @@ func (t *TargetAPI) Get() { // Post ... func (t *TargetAPI) Post() { target := &models.RepTarget{} - t.DecodeJSONReq(target) + t.DecodeJSONReqAndValidate(target) - if len(target.Name) == 0 || len(target.URL) == 0 { - t.CustomAbort(http.StatusBadRequest, "name or URL is nil") + ta, err := dao.GetRepTargetByName(target.Name) + if err != nil { + log.Errorf("failed to get target %s: %v", target.Name, err) + t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + } + + if ta != nil { + t.CustomAbort(http.StatusConflict, "name is already used") } if len(target.Password) != 0 { @@ -187,16 +193,32 @@ func (t *TargetAPI) Post() { func (t *TargetAPI) Put() { id := t.getIDFromURL() if id == 0 { - t.CustomAbort(http.StatusBadRequest, http.StatusText(http.StatusBadRequest)) + t.CustomAbort(http.StatusBadRequest, "id can not be empty or 0") } target := &models.RepTarget{} - t.DecodeJSONReq(target) + t.DecodeJSONReqAndValidate(target) - if target.ID == 0 { - target.ID = id + originTarget, 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.Name != originTarget.Name { + ta, err := dao.GetRepTargetByName(target.Name) + if err != nil { + log.Errorf("failed to get target %s: %v", target.Name, err) + t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + } + + if ta != nil { + t.CustomAbort(http.StatusConflict, "name is already used") + } + } + + target.ID = id + if len(target.Password) != 0 { target.Password = utils.ReversibleEncrypt(target.Password) } diff --git a/controllers/login.go b/controllers/login.go index 2bfb29983..d608f8fbb 100644 --- a/controllers/login.go +++ b/controllers/login.go @@ -69,7 +69,7 @@ func (c *CommonController) Login() { // SwitchLanguage handles UI request to switch between different languages and re-render template based on language. func (c *CommonController) SwitchLanguage() { lang := c.GetString("lang") - if lang == "en-US" || lang == "zh-CN" || lang == "de-DE" || lang == "ru-RU" { + if lang == "en-US" || lang == "zh-CN" || lang == "de-DE" || lang == "ru-RU" || lang == "ja-JP" { c.SetSession("lang", lang) c.Data["Lang"] = lang } diff --git a/dao/dao_test.go b/dao/dao_test.go index 25b3f941c..8655a179c 100644 --- a/dao/dao_test.go +++ b/dao/dao_test.go @@ -766,6 +766,78 @@ func TestAddRepTarget(t *testing.T) { } } +func TestGetRepTargetByName(t *testing.T) { + target, err := GetRepTarget(targetID) + if err != nil { + t.Fatalf("failed to get target %d: %v", targetID, err) + } + + target2, err := GetRepTargetByName(target.Name) + if err != nil { + t.Fatalf("failed to get target %s: %v", target.Name, err) + } + + if target.Name != target2.Name { + t.Errorf("unexpected target name: %s, expected: %s", target2.Name, target.Name) + } +} + +func TestUpdateRepTarget(t *testing.T) { + target := &models.RepTarget{ + Name: "name", + URL: "http://url", + Username: "username", + Password: "password", + } + + id, err := AddRepTarget(*target) + if err != nil { + t.Fatalf("failed to add target: %v", err) + } + defer func() { + if err := DeleteRepTarget(id); err != nil { + t.Logf("failed to delete target %d: %v", id, err) + } + }() + + target.ID = id + target.Name = "new_name" + target.URL = "http://new_url" + target.Username = "new_username" + target.Password = "new_password" + + if err = UpdateRepTarget(*target); err != nil { + t.Fatalf("failed to update target: %v", err) + } + + target, err = GetRepTarget(id) + if err != nil { + t.Fatalf("failed to get target %d: %v", id, err) + } + + if target.Name != "new_name" { + t.Errorf("unexpected name: %s, expected: %s", target.Name, "new_name") + } + + if target.URL != "http://new_url" { + t.Errorf("unexpected url: %s, expected: %s", target.URL, "http://new_url") + } + + if target.Username != "new_username" { + t.Errorf("unexpected username: %s, expected: %s", target.Username, "new_username") + } + + if target.Password != "new_password" { + t.Errorf("unexpected password: %s, expected: %s", target.Password, "new_password") + } +} + +func TestGetAllRepTargets(t *testing.T) { + if _, err := GetAllRepTargets(); err != nil { + t.Fatalf("failed to get all targets: %v", err) + } +} + func TestAddRepPolicy(t *testing.T) { policy := models.RepPolicy{ ProjectID: 1, @@ -800,6 +872,23 @@ func TestAddRepPolicy(t *testing.T) { } +func TestGetRepPolicyByName(t *testing.T) { + policy, err := GetRepPolicy(policyID) + if err != nil { + t.Fatalf("failed to get policy %d: %v", policyID, err) + } + + policy2, err := GetRepPolicyByName(policy.Name) + if err != nil { + t.Fatalf("failed to get policy %s: %v", policy.Name, err) + } + + if policy.Name != policy2.Name { + t.Errorf("unexpected name: %s, expected: %s", policy2.Name, policy.Name) + } + +} + func TestDisableRepPolicy(t *testing.T) { err := DisableRepPolicy(policyID) if err != nil { diff --git a/dao/replication_job.go b/dao/replication_job.go index 58d5669a5..dd2b29702 100644 --- a/dao/replication_job.go +++ b/dao/replication_job.go @@ -11,13 +11,13 @@ import ( // AddRepTarget ... func AddRepTarget(target models.RepTarget) (int64, error) { - o := orm.NewOrm() + o := GetOrmer() return o.Insert(&target) } // GetRepTarget ... func GetRepTarget(id int64) (*models.RepTarget, error) { - o := orm.NewOrm() + o := GetOrmer() t := models.RepTarget{ID: id} err := o.Read(&t) if err == orm.ErrNoRows { @@ -26,28 +26,34 @@ func GetRepTarget(id int64) (*models.RepTarget, error) { return &t, err } +// GetRepTargetByName ... +func GetRepTargetByName(name string) (*models.RepTarget, error) { + o := GetOrmer() + t := models.RepTarget{Name: name} + err := o.Read(&t, "Name") + if err == orm.ErrNoRows { + return nil, nil + } + return &t, err +} + // DeleteRepTarget ... func DeleteRepTarget(id int64) error { - o := orm.NewOrm() + o := GetOrmer() _, err := o.Delete(&models.RepTarget{ID: id}) return err } // UpdateRepTarget ... func UpdateRepTarget(target models.RepTarget) error { - o := orm.NewOrm() - if len(target.Password) != 0 { - _, err := o.Update(&target) - return err - } - - _, err := o.Update(&target, "URL", "Name", "Username") + o := GetOrmer() + _, err := o.Update(&target, "URL", "Name", "Username", "Password") return err } // GetAllRepTargets ... func GetAllRepTargets() ([]*models.RepTarget, error) { - o := orm.NewOrm() + o := GetOrmer() qs := o.QueryTable(&models.RepTarget{}) var targets []*models.RepTarget _, err := qs.All(&targets) @@ -56,7 +62,7 @@ func GetAllRepTargets() ([]*models.RepTarget, error) { // AddRepPolicy ... func AddRepPolicy(policy models.RepPolicy) (int64, error) { - o := orm.NewOrm() + o := GetOrmer() sqlTpl := `insert into replication_policy (name, project_id, target_id, enabled, description, cron_str, start_time, creation_time, update_time ) values (?, ?, ?, ?, ?, ?, %s, NOW(), NOW())` var sql string if policy.Enabled == 1 { @@ -78,7 +84,7 @@ func AddRepPolicy(policy models.RepPolicy) (int64, error) { // GetRepPolicy ... func GetRepPolicy(id int64) (*models.RepPolicy, error) { - o := orm.NewOrm() + o := GetOrmer() p := models.RepPolicy{ID: id} err := o.Read(&p) if err == orm.ErrNoRows { @@ -87,24 +93,35 @@ func GetRepPolicy(id int64) (*models.RepPolicy, error) { return &p, err } +// GetRepPolicyByName ... +func GetRepPolicyByName(name string) (*models.RepPolicy, error) { + o := GetOrmer() + p := models.RepPolicy{Name: name} + err := o.Read(&p, "Name") + if err == orm.ErrNoRows { + return nil, nil + } + return &p, err +} + // GetRepPolicyByProject ... func GetRepPolicyByProject(projectID int64) ([]*models.RepPolicy, error) { var res []*models.RepPolicy - o := orm.NewOrm() + o := GetOrmer() _, err := o.QueryTable("replication_policy").Filter("project_id", projectID).All(&res) return res, err } // DeleteRepPolicy ... func DeleteRepPolicy(id int64) error { - o := orm.NewOrm() + o := GetOrmer() _, err := o.Delete(&models.RepPolicy{ID: id}) return err } // UpdateRepPolicyEnablement ... func UpdateRepPolicyEnablement(id int64, enabled int) error { - o := orm.NewOrm() + o := GetOrmer() p := models.RepPolicy{ ID: id, Enabled: enabled} @@ -125,7 +142,7 @@ func DisableRepPolicy(id int64) error { // AddRepJob ... func AddRepJob(job models.RepJob) (int64, error) { - o := orm.NewOrm() + o := GetOrmer() if len(job.Status) == 0 { job.Status = models.JobPending } @@ -137,7 +154,7 @@ func AddRepJob(job models.RepJob) (int64, error) { // GetRepJob ... func GetRepJob(id int64) (*models.RepJob, error) { - o := orm.NewOrm() + o := GetOrmer() j := models.RepJob{ID: id} err := o.Read(&j) if err == orm.ErrNoRows { @@ -164,20 +181,20 @@ func GetRepJobToStop(policyID int64) ([]*models.RepJob, error) { } func repJobPolicyIDQs(policyID int64) orm.QuerySeter { - o := orm.NewOrm() + o := GetOrmer() return o.QueryTable("replication_job").Filter("policy_id", policyID) } // DeleteRepJob ... func DeleteRepJob(id int64) error { - o := orm.NewOrm() + o := GetOrmer() _, err := o.Delete(&models.RepJob{ID: id}) return err } // UpdateRepJobStatus ... func UpdateRepJobStatus(id int64, status string) error { - o := orm.NewOrm() + o := GetOrmer() j := models.RepJob{ ID: id, Status: status, diff --git a/docs/img/dianrong.png b/docs/img/dianrong.png new file mode 100644 index 000000000..08f3cf2c8 Binary files /dev/null and b/docs/img/dianrong.png differ diff --git a/migration/README.md b/migration/README.md index dde8e00c9..ee1b12dcb 100644 --- a/migration/README.md +++ b/migration/README.md @@ -1,54 +1,56 @@ -# migration -Migration is a module for migrating database schema between different version of project [harbor](https://github.com/vmware/harbor) +# Migration guide +Migration is a module for migrating database schema between different version of project [Harbor](https://github.com/vmware/harbor) + +This module is for those machine running Harbor's old version, such as 0.1.0. If your Harbor' version is up to date, please ignore this module. **WARNING!!** You must backup your data before migrating -###installation -- step 1: modify migration.cfg +###Installation +- step 1: change `db_username`, `db_password`, `db_port`, `db_name` in migration.cfg - step 2: build image from dockerfile ``` cd harbor-migration - docker build -t your-image-name . + docker build -t migrate-tool . ``` -###migration operation -- show instruction of harbor-migration - - ```docker run your-image-name help``` - -- test mysql connection in harbor-migration - - ```docker run -v /data/database:/var/lib/mysql your-image-name test``` - -- create backup file in `/path/to/backup` - - ``` - docker run -ti -v /data/database:/var/lib/mysql -v /path/to/backup:/harbor-migration/backup your-image-name backup - ``` - -- restore from backup file in `/path/to/backup` - - ``` - docker run -ti -v /data/database:/var/lib/mysql -v /path/to/backup:/harbor-migration/backup your-image-name restore - ``` - -- perform database schema upgrade - - ```docker run -ti -v /data/database:/var/lib/mysql your-image-name up head``` - -you can use `-v /etc/localtime:/etc/localtime` to sync container timezone with host timezone. - -you may change `/data/database` to the mysql volumes path you set in docker-compose.yml. -###migration step -- step 1: stop and remove harbor service +###Migrate Step +- step 1: stop and remove Harbor service ``` docker-compose down ``` -- step 2: perform migration operation -- step 3: rebuild newest harbor images and restart service +- step 2: create backup file in `/path/to/backup` + + ``` + docker run -ti --rm -v /data/database:/var/lib/mysql -v /path/to/backup:/harbor-migration/backup migrate-tool backup + ``` + +- step 3: perform database schema upgrade + + ```docker run -ti --rm -v /data/database:/var/lib/mysql migrate-tool up head``` + + + +- step 4: rebuild newest Harbor images and restart service ``` docker-compose build && docker-compose up -d ``` + +You may change `/data/database` to the mysql volumes path you set in docker-compose.yml. + +###Migration operation reference +- You can use `help` to show instruction of Harbor migration + + ```docker run migrate-tool help``` + +- You can use `test` to test mysql connection in Harbor migration + + ```docker run --rm -v /data/database:/var/lib/mysql migrate-tool test``` + +- You can restore from backup file in `/path/to/backup` + + ``` + docker run -ti --rm -v /data/database:/var/lib/mysql -v /path/to/backup:/harbor-migration/backup migrate-tool restore + ``` diff --git a/migration/db_meta.py b/migration/db_meta.py index e20dd924c..dcbdd4311 100644 --- a/migration/db_meta.py +++ b/migration/db_meta.py @@ -4,6 +4,7 @@ import sqlalchemy as sa from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker, relationship +from sqlalchemy.dialects import mysql Base = declarative_base() @@ -20,8 +21,8 @@ class User(Base): reset_uuid = sa.Column(sa.String(40)) salt = sa.Column(sa.String(40)) sysadmin_flag = sa.Column(sa.Integer) - creation_time = sa.Column(sa.DateTime) - update_time = sa.Column(sa.DateTime) + creation_time = sa.Column(mysql.TIMESTAMP) + update_time = sa.Column(mysql.TIMESTAMP) class Properties(Base): __tablename__ = 'properties' @@ -35,8 +36,8 @@ class ProjectMember(Base): project_id = sa.Column(sa.Integer(), primary_key = True) user_id = sa.Column(sa.Integer(), primary_key = True) role = sa.Column(sa.Integer(), nullable = False) - creation_time = sa.Column(sa.DateTime(), nullable = True) - update_time = sa.Column(sa.DateTime(), nullable = True) + creation_time = sa.Column(mysql.TIMESTAMP, nullable = True) + update_time = sa.Column(mysql.TIMESTAMP, nullable = True) sa.ForeignKeyConstraint(['project_id'], [u'project.project_id'], ), sa.ForeignKeyConstraint(['role'], [u'role.role_id'], ), sa.ForeignKeyConstraint(['user_id'], [u'user.user_id'], ), @@ -79,8 +80,8 @@ class Project(Base): project_id = sa.Column(sa.Integer, primary_key=True) owner_id = sa.Column(sa.ForeignKey(u'user.user_id'), nullable=False, index=True) name = sa.Column(sa.String(30), nullable=False, unique=True) - creation_time = sa.Column(sa.DateTime) - update_time = sa.Column(sa.DateTime) + creation_time = sa.Column(mysql.TIMESTAMP) + update_time = sa.Column(mysql.TIMESTAMP) 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') diff --git a/migration/migration_harbor/versions/0_1_1.py b/migration/migration_harbor/versions/0_1_1.py index ecec2cfb3..f3ea874a5 100644 --- a/migration/migration_harbor/versions/0_1_1.py +++ b/migration/migration_harbor/versions/0_1_1.py @@ -27,9 +27,10 @@ branch_labels = None depends_on = None from alembic import op -from datetime import datetime from db_meta import * +from sqlalchemy.dialects import mysql + Session = sessionmaker() def upgrade(): @@ -44,12 +45,9 @@ def upgrade(): session.add(Properties(k='schema_version', v='0.1.1')) #add column to table user - op.add_column('user', sa.Column('creation_time', sa.DateTime(), nullable=True)) + op.add_column('user', sa.Column('creation_time', mysql.TIMESTAMP, nullable=True)) op.add_column('user', sa.Column('sysadmin_flag', sa.Integer(), nullable=True)) - op.add_column('user', sa.Column('update_time', sa.DateTime(), nullable=True)) - - #fill update_time data into table user - session.query(User).update({User.update_time: datetime.now()}) + op.add_column('user', sa.Column('update_time', mysql.TIMESTAMP, nullable=True)) #init all sysadmin_flag = 0 session.query(User).update({User.sysadmin_flag: 0}) @@ -62,7 +60,7 @@ def upgrade(): for result in join_result: session.add(ProjectMember(project_id=result.project_role.project_id, \ user_id=result.user_id, role=result.project_role.role_id, \ - creation_time=datetime.now(), update_time=datetime.now())) + creation_time=None, update_time=None)) #update sysadmin_flag sys_admin_result = session.query(UserProjectRole).\ @@ -88,11 +86,9 @@ def upgrade(): session.delete(acc) session.query(Access).update({Access.access_id: Access.access_id - 1}) - #add column to table project - op.add_column('project', sa.Column('update_time', sa.DateTime(), nullable=True)) + #add column to table project + op.add_column('project', sa.Column('update_time', mysql.TIMESTAMP, nullable=True)) - #fill update_time data into table project - session.query(Project).update({Project.update_time: datetime.now()}) session.commit() def downgrade(): diff --git a/models/replication_job.go b/models/replication_job.go index 97fc8443a..7f3f94082 100644 --- a/models/replication_job.go +++ b/models/replication_job.go @@ -2,6 +2,8 @@ package models import ( "time" + + "github.com/astaxie/beego/validation" ) const ( @@ -42,6 +44,33 @@ type RepPolicy struct { UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"` } +// Valid ... +func (r *RepPolicy) Valid(v *validation.Validation) { + if len(r.Name) == 0 { + v.SetError("name", "can not be empty") + } + + if len(r.Name) > 256 { + v.SetError("name", "max length is 256") + } + + if r.ProjectID <= 0 { + v.SetError("project_id", "invalid") + } + + if r.TargetID <= 0 { + v.SetError("target_id", "invalid") + } + + if r.Enabled != 0 && r.Enabled != 1 { + v.SetError("enabled", "must be 0 or 1") + } + + if len(r.CronStr) > 256 { + v.SetError("cron_str", "max length is 256") + } +} + // RepJob is the model for a replication job, which is the execution unit on job service, currently it is used to transfer/remove // a repository to/from a remote registry instance. type RepJob struct { @@ -68,17 +97,42 @@ type RepTarget struct { UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"` } +// Valid ... +func (r *RepTarget) Valid(v *validation.Validation) { + if len(r.Name) == 0 { + v.SetError("name", "can not be empty") + } + + if len(r.Name) > 64 { + v.SetError("name", "max length is 64") + } + + if len(r.URL) == 0 { + v.SetError("endpoint", "can not be empty") + } + + if len(r.URL) > 64 { + v.SetError("endpoint", "max length is 64") + } + + // password is encoded using base64, the length of this field + // in DB is 64, so the max length in request is 48 + if len(r.Password) > 48 { + v.SetError("password", "max length is 48") + } +} + //TableName is required by by beego orm to map RepTarget to table replication_target -func (rt *RepTarget) TableName() string { +func (r *RepTarget) TableName() string { return "replication_target" } //TableName is required by by beego orm to map RepJob to table replication_job -func (rj *RepJob) TableName() string { +func (r *RepJob) TableName() string { return "replication_job" } //TableName is required by by beego orm to map RepPolicy to table replication_policy -func (rp *RepPolicy) TableName() string { +func (r *RepPolicy) TableName() string { return "replication_policy" } diff --git a/static/i18n/locale_de-DE.ini b/static/i18n/locale_de-DE.ini index 79d08dded..c5303e2f9 100644 --- a/static/i18n/locale_de-DE.ini +++ b/static/i18n/locale_de-DE.ini @@ -75,6 +75,7 @@ language_en-US = English language_zh-CN = 中文 language_de-DE = Deutsch language_ru-RU = Русский +language_ja-JP = 日本語 copyright = Copyright all_rights_reserved = Alle Rechte vorbehalten. index_desc = Project Harbor ist ein zuverlässiger Enterprise-Class Registry Server. Unternehmen können ihren eigenen Registry Server aufsetzen um die Produktivität und Sicherheit zu erhöhen. Project Harbor kann für Entwicklungs- wie auch Produktiv-Umgebungen genutzt werden. diff --git a/static/i18n/locale_en-US.ini b/static/i18n/locale_en-US.ini index 414e6c647..7444d4537 100644 --- a/static/i18n/locale_en-US.ini +++ b/static/i18n/locale_en-US.ini @@ -76,6 +76,7 @@ language_en-US = English language_zh-CN = 中文 language_de-DE = Deutsch language_ru-RU = Русский +language_ja-JP = 日本語 copyright = Copyright all_rights_reserved = All rights reserved. index_desc = Project Harbor is to build an enterprise-class, reliable registry server. Enterprises can set up a private registry server in their own environment to improve productivity as well as security. Project Harbor can be used in both development and production environment. diff --git a/static/i18n/locale_ja-JP.ini b/static/i18n/locale_ja-JP.ini new file mode 100644 index 000000000..cf93bcbe1 --- /dev/null +++ b/static/i18n/locale_ja-JP.ini @@ -0,0 +1,89 @@ +page_title_index = Harbor +page_title_sign_in = ログイン - Harbor +page_title_project = プロジェクト - Harbor +page_title_item_details = 詳しい - Harbor +page_title_registration = 登録 - Harbor +page_title_add_user = ユーザを追加 - Harbor +page_title_forgot_password = パスワードを忘れました - Harbor +title_forgot_password = パスワードを忘れました +page_title_reset_password = パスワードをリセット - Harbor +title_reset_password = パスワードをリセット +page_title_change_password = パスワードを変更 - Harbor +title_change_password = パスワードを変更 +page_title_search = サーチ - Harbor +sign_in = ログイン +sign_up = 登録 +add_user = ユーザを追加 +log_out = ログアウト +search_placeholder = プロジェクト名またはイメージ名 +change_password = パスワードを変更 +username_email = ユーザ名/メールアドレス +password = パスワード +forgot_password = パスワードを忘れました +welcome = ようこそ +my_projects = マイプロジェクト +public_projects = パブリックプロジェクト +admin_options = 管理者 +project_name = プロジェクト名 +creation_time = 作成日時 +publicity = パブリック +add_project = プロジェクトを追加 +check_for_publicity = パブリックプロジェクト +button_save = 保存する +button_cancel = 取り消しする +button_submit = 送信する +username = ユーザ名 +email = メールアドレス +system_admin = システム管理者 +dlg_button_ok = OK +dlg_button_cancel = 取り消し +registration = 登録 +username_description = ログイン際に使うユーザ名を入力してください。 +email_description = メールアドレスはパスワードをリセットする際に使われます。 +full_name = フルネーム +full_name_description = フルネームを入力してください。 +password_description = パスワード7英数字以上で、少なくとも 1小文字、 1大文字と 1数字でなければなりません。 +confirm_password = パスワードを確認する +note_to_the_admin = メモ +old_password = 現在のパスワード +new_password = 新しいパスワード +forgot_password_description = ぱプロジェクトをリセットするメールはこのアドレスに送信します。 + +projects = プロジェクト +repositories = リポジトリ +search = サーチ +home = ホーム +project = プロジェクト +owner = オーナー +repo = リポジトリ +user = ユーザ +logs = ログ +repo_name = リポジトリ名 +repo_tag = リポジトリタグ +add_members = メンバーを追加 +operation = 操作 +advance = さらに絞りこみで検索 +all = 全部 +others = その他 +start_date = 開始日 +end_date = 終了日 +timestamp = タイムスタンプ +role = 役割 +reset_email_hint = このリンクをクリックしてパスワードリセットの処理を続けてください +reset_email_subject = パスワードをリセットします +language = 日本語 +language_en-US = English +language_zh-CN = 中文 +language_de-DE = Deutsch +language_ru-RU = Русский +language_ja-JP = 日本語 +copyright = コピーライト +all_rights_reserved = 無断複写・転載を禁じます +index_desc = Harborは、信頼性の高いエンタープライズクラスのRegistryサーバです。タープライズユーザはHarborを利用し、プライベートのRegistryサビースを構築し、生産性および安全性を向上させる事ができます。開発環境はもちろん、生産環境にも使用する事ができます。 +index_desc_0 = 主な利点: +index_desc_1 = 1. セキュリティ: 知的財産権を組織内で確保する。 +index_desc_2 = 2. 効率: プライベートなので、パブリックRegistryサビースにネットワーク通信が減らす。 +index_desc_3 = 3. アクセス制御: ロールベースアクセス制御機能を実装し、更に既存のユーザ管理システム(AD/LDAP)と統合することも可能。 +index_desc_4 = 4. 監査: すべてRegistryサビースへの操作が記録され、検査にに利用できる。 +index_desc_5 = 5. 管理UI: 使いやすい管理UIが搭載する。 +index_title = エンタープライズ Registry サビース diff --git a/static/i18n/locale_messages.js b/static/i18n/locale_messages.js index 6ba513b7b..819dd53d0 100644 --- a/static/i18n/locale_messages.js +++ b/static/i18n/locale_messages.js @@ -16,378 +16,441 @@ var global_messages = { "username_is_required" : { "en-US": "Username is required.", "zh-CN": "用户名为必填项。", + "ja-JP": "ユーザ名は必須項目です。", "de-DE": "Benutzername erforderlich.", "ru-RU": "Требуется ввести имя пользователя." }, "username_has_been_taken" : { "en-US": "Username has been taken.", "zh-CN": "用户名已被占用。", + "ja-JP": "ユーザ名はすでに登録されました。", "de-DE": "Benutzername bereits vergeben.", "ru-RU": "Имя пользователя уже используется." }, "username_is_too_long" : { "en-US": "Username is too long. (maximum 20 characters)", "zh-CN": "用户名长度超出限制。(最长为20个字符)", + "ja-JP": "ユーザ名が長すぎです。(20文字まで)", "de-DE": "Benutzername ist zu lang. (maximal 20 Zeichen)", "ru-RU": "Имя пользователя слишком длинное. (максимум 20 символов)" }, "username_contains_illegal_chars": { "en-US": "Username contains illegal character(s).", "zh-CN": "用户名包含不合法的字符。", + "ja-JP": "ユーザ名に使えない文字が入っています。", "de-DE": "Benutzername enthält ungültige Zeichen.", "ru-RU": "Имя пользователя содержит недопустимые символы." }, "email_is_required" : { "en-US": "Email is required.", "zh-CN": "邮箱为必填项。", + "ja-JP": "メールアドレスが必須です。", "de-DE": "E-Mail Adresse erforderlich.", "ru-RU": "Требуется ввести E-mail адрес." }, "email_contains_illegal_chars" : { "en-US": "Email contains illegal character(s).", "zh-CN": "邮箱包含不合法的字符。", + "ja-JP": "メールアドレスに使えない文字が入っています。", "de-DE": "E-Mail Adresse enthält ungültige Zeichen.", "ru-RU": "E-mail адрес содержит недопеустимые символы." }, "email_has_been_taken" : { "en-US": "Email has been taken.", "zh-CN": "邮箱已被占用。", + "ja-JP": "メールアドレスがすでに使われました。", "de-DE": "E-Mail Adresse wird bereits verwendet.", "ru-RU": "Такой E-mail адрес уже используется." }, "email_content_illegal" : { "en-US": "Email format is illegal.", "zh-CN": "邮箱格式不合法。", + "ja-JP": "メールアドレスフォーマットエラー。", "de-DE": "Format der E-Mail Adresse ist ungültig.", "ru-RU": "Недопустимый формат E-mail адреса." }, "email_does_not_exist" : { "en-US": "Email does not exist.", "zh-CN": "邮箱不存在。", + "ja-JP": "メールアドレスが存在しません。", "de-DE": "E-Mail Adresse existiert nicht.", "ru-RU": "E-mail адрес не существует." }, "realname_is_required" : { "en-US": "Full name is required.", "zh-CN": "全名为必填项。", + "ja-JP": "フルネームが必須です。", "de-DE": "Vollständiger Name erforderlich.", "ru-RU": "Требуется ввести полное имя." }, "realname_is_too_long" : { "en-US": "Full name is too long. (maximum 20 characters)", "zh-CN": "全名长度超出限制。(最长为20个字符)", + "ja-JP": "フルネームは長すぎです。(20文字まで)", "de-DE": "Vollständiger Name zu lang. (maximal 20 Zeichen)", "ru-RU": "Полное имя слишком длинное. (максимум 20 символов)" }, "realname_contains_illegal_chars" : { "en-US": "Full name contains illegal character(s).", "zh-CN": "全名包含不合法的字符。", + "ja-JP": "フルネームに使えない文字が入っています。", "de-DE": "Vollständiger Name enthält ungültige Zeichen.", "ru-RU": "Полное имя содержит недопустимые символы." }, "password_is_required" : { "en-US": "Password is required.", "zh-CN": "密码为必填项。", + "ja-JP": "パスワードは必須です。", "de-DE": "Passwort erforderlich.", "ru-RU": "Требуется ввести пароль." }, "password_is_invalid" : { "en-US": "Password is invalid. At least 7 characters with 1 lowercase letter, 1 capital letter and 1 numeric character.", "zh-CN": "密码无效。至少输入 7个字符且包含 1个小写字母,1个大写字母和 1个数字。", + "ja-JP": "無効なパスワードです。7英数字以上で、 少なくとも1小文字、1大文字と1数字となります。", "de-DE": "Passwort ungültig. Mindestens sieben Zeichen bestehend aus einem Kleinbuchstaben, einem Großbuchstaben und einer Zahl", "ru-RU": "Такой пароль недопустим. Парольл должен содержать Минимум 7 символов, в которых будет присутствовать по меньшей мере 1 буква нижнего регистра, 1 буква верхнего регистра и 1 цифра" }, "password_is_too_long" : { "en-US": "Password is too long. (maximum 20 characters)", "zh-CN": "密码长度超出限制。(最长为20个字符)", + "ja-JP": "パスワードは長すぎです。(20文字まで)", "de-DE": "Passwort zu lang. (maximal 20 Zeichen)", "ru-RU": "Пароль слишком длинный (максимум 20 символов)" }, "password_does_not_match" : { "en-US": "Passwords do not match.", "zh-CN": "两次密码输入不一致。", + "ja-JP": "確認のパスワードが正しくありません。", "de-DE": "Passwörter stimmen nicht überein.", "ru-RU": "Пароли не совпадают." }, "comment_is_too_long" : { "en-US": "Comment is too long. (maximum 20 characters)", "zh-CN": "备注长度超出限制。(最长为20个字符)", + "ja-JP": "コメントは長すぎです。(20文字まで)", "de-DE": "Kommentar zu lang. (maximal 20 Zeichen)", "ru-RU": "Комментарий слишком длинный. (максимум 20 символов)" }, "comment_contains_illegal_chars" : { "en-US": "Comment contains illegal character(s).", "zh-CN": "备注包含不合法的字符。", + "ja-JP": "コメントに使えない文字が入っています。", "de-DE": "Kommentar enthält ungültige Zeichen.", "ru-RU": "Комментарий содержит недопустимые символы." }, "project_name_is_required" : { "en-US": "Project name is required.", "zh-CN": "项目名称为必填项。", + "ja-JP": "プロジェクト名は必須です。", "de-DE": "Projektname erforderlich.", "ru-RU": "Необходимо ввести название Проекта." }, "project_name_is_too_short" : { "en-US": "Project name is too short. (minimum 4 characters)", "zh-CN": "项目名称至少要求 4个字符。", + "ja-JP": "プロジェクト名は4文字以上です。", "de-DE": "Projektname zu kurz. (mindestens 4 Zeichen)", "ru-RU": "Название проекта слишком короткое. (миниму 4 символа)" }, "project_name_is_too_long" : { "en-US": "Project name is too long. (maximum 30 characters)", "zh-CN": "项目名称长度超出限制。(最长为30个字符)", + "ja-JP": "プロジェクト名は長すぎです。(30文字まで)", "de-DE": "Projektname zu lang. (maximal 30 Zeichen)", "ru-RU": "Название проекта слишком длинное (максимум 30 символов)" }, "project_name_contains_illegal_chars" : { "en-US": "Project name contains illegal character(s).", "zh-CN": "项目名称包含不合法的字符。", + "ja-JP": "プロジェクト名に使えない文字が入っています。", "de-DE": "Projektname enthält ungültige Zeichen.", "ru-RU": "Название проекта содержит недопустимые символы." }, "project_exists" : { "en-US": "Project exists.", "zh-CN": "项目已存在。", + "ja-JP": "プロジェクトはすでに存在しました。", "de-DE": "Projekt existiert bereits.", "ru-RU": "Такой проект уже существует." }, "delete_user" : { "en-US": "Delete User", "zh-CN": "删除用户", + "ja-JP": "ユーザを削除", "de-DE": "Benutzer löschen", "ru-RU": "Удалить пользователя" }, "are_you_sure_to_delete_user" : { "en-US": "Are you sure to delete ", "zh-CN": "确认要删除用户 ", + "ja-JP": "ユーザを削除でよろしでしょうか ", "de-DE": "Sind Sie sich sicher, dass Sie folgenden Benutzer löschen möchten: ", "ru-RU": "Вы уверены что хотите удалить пользователя? " }, "input_your_username_and_password" : { "en-US": "Please input your username and password.", "zh-CN": "请输入用户名和密码。", + "ja-JP": "ユーザ名とパスワードを入力してください。", "de-DE": "Bitte geben Sie ihr Benutzername und Passwort ein.", "ru-RU": "Введите имя пользователя и пароль." }, "check_your_username_or_password" : { "en-US": "Please check your username or password.", "zh-CN": "请输入正确的用户名或密码。", + "ja-JP": "正しいユーザ名とパスワードを入力してください。", "de-DE": "Bitte überprüfen Sie ihren Benutzernamen und Passwort.", "ru-RU": "Проверьте свои имя пользователя и пароль." }, "title_login_failed" : { "en-US": "Login Failed", "zh-CN": "登录失败", + "ja-JP": "ログインに失敗しました。", "de-DE": "Anmeldung fehlgeschlagen", "ru-RU": "Ошибка входа" }, "title_change_password" : { "en-US": "Change Password", "zh-CN": "修改密码", + "ja-JP": "パスワードを変更します。", "de-DE": "Passwort ändern", "ru-RU": "Сменить пароль" }, "change_password_successfully" : { "en-US": "Password changed successfully.", "zh-CN": "密码已修改。", + "ja-JP": "パスワードを変更しました。", "de-DE": "Passwort erfolgreich geändert.", "ru-RU": "Пароль успешно изменен." }, "title_forgot_password" : { "en-US": "Forgot Password", "zh-CN": "忘记密码", + "ja-JP": "パスワードをリセットします。", "de-DE": "Passwort vergessen", "ru-RU": "Забыли пароль?" }, "email_has_been_sent" : { "en-US": "Email for resetting password has been sent.", "zh-CN": "重置密码邮件已发送。", + "ja-JP": "パスワードをリセットするメールを送信しました。", "de-DE": "Eine E-Mail mit einem Wiederherstellungslink wurde an Sie gesendet.", "ru-RU": "На ваш E-mail было выслано письмо с инструкциями по сбросу пароля." }, "send_email_failed" : { "en-US": "Failed to send Email for resetting password.", "zh-CN": "重置密码邮件发送失败。", + "ja-JP": "パスワードをリセットするメールを送信する際エラーが出ました", "de-DE": "Fehler beim Senden der Wiederherstellungs-E-Mail.", "ru-RU": "Ошибка отправки сообщения." }, "please_login_first" : { "en-US": "Please login first.", "zh-CN": "请先登录。", + "ja-JP": "この先にログインが必要です。", "de-DE": "Bitte melden Sie sich zuerst an.", "ru-RU": "Сначала выполните вход в систему." }, "old_password_is_not_correct" : { "en-US": "Old password is not correct.", "zh-CN": "原密码输入不正确。", + "ja-JP": "現在のパスワードが正しく入力されていません。", "de-DE": "Altes Passwort ist nicht korrekt.", "ru-RU": "Старый пароль введен неверно." }, "please_input_new_password" : { "en-US": "Please input new password.", "zh-CN": "请输入新密码。", + "ja-JP": "あたらしいパスワードを入力してください", "de-DE": "Bitte geben Sie ihr neues Passwort ein.", "ru-RU": "Пожалуйста, введите новый пароль." }, "invalid_reset_url": { "en-US": "Invalid URL for resetting password.", "zh-CN": "无效密码重置链接。", + "ja-JP": "無効なパスワードをリセットするリンク。", "de-DE": "Ungültige URL zum Passwort wiederherstellen.", "ru-RU": "Неверный URL для сброса пароля." }, "reset_password_successfully" : { "en-US": "Reset password successfully.", "zh-CN": "密码重置成功。", + "ja-JP": "パスワードをリセットしました。", "de-DE": "Passwort erfolgreich wiederhergestellt.", "ru-RU": "Пароль успешно сброшен." }, "internal_error": { "en-US": "Internal error.", "zh-CN": "内部错误,请联系系统管理员。", + "ja-JP": "エラーが出ました、管理者に連絡してください。", "de-DE": "Interner Fehler.", "ru-RU": "Внутренняя ошибка." }, "title_reset_password" : { "en-US": "Reset Password", "zh-CN": "重置密码", + "ja-JP": "パスワードをリセットする", "de-DE": "Passwort zurücksetzen", "ru-RU": "Сбросить пароль" }, "title_sign_up" : { "en-US": "Sign Up", "zh-CN": "注册", + "ja-JP": "登録", "de-DE": "Registrieren", "ru-RU": "Регистрация" }, "title_add_user": { "en-US": "Add User", "zh-CN": "新增用户", + "ja-JP": "ユーザを追加", "de-DE": "Benutzer hinzufügen", "ru-RU": "Добавить пользователя" }, "registered_successfully": { "en-US": "Signed up successfully.", "zh-CN": "注册成功。", + "ja-JP": "登録しました。", "de-DE": "Erfolgreich registriert.", "ru-RU": "Регистрация прошла успешно." }, "registered_failed" : { "en-US": "Failed to sign up.", "zh-CN": "注册失败。", + "ja-JP": "登録でませんでした。", "de-DE": "Registrierung fehlgeschlagen.", "ru-RU": "Ошибка регистрации." }, "added_user_successfully": { "en-US": "Added user successfully.", "zh-CN": "新增用户成功。", + "ja-JP": "ユーザを追加しました。", "de-DE": "Benutzer erfolgreich erstellt.", "ru-RU": "Пользователь успешно добавлен." }, "added_user_failed": { "en-US": "Adding user failed.", "zh-CN": "新增用户失败。", + "ja-JP": "ユーザを追加できませんでした。", "de-DE": "Benutzer erstellen fehlgeschlagen.", "ru-RU": "Ошибка добавления пользователя." }, "projects": { "en-US": "Projects", "zh-CN": "项目", + "ja-JP": "プロジェクト", "de-DE": "Projekte", "ru-RU": "Проекты" }, "repositories" : { "en-US": "Repositories", "zh-CN": "镜像仓库", + "ja-JP": "リポジトリ", "de-DE": "Repositories", "ru-RU": "Репозитории" }, "no_repo_exists" : { "en-US": "No repositories found, please use 'docker push' to upload images.", "zh-CN": "未发现镜像,请用‘docker push’命令上传镜像。", + "ja-JP": "イメージが見つかりませんでした。’docker push’を利用しイメージをアップロードしてください。", "de-DE": "Keine Repositories gefunden, bitte benutzen Sie 'docker push' um ein Image hochzuladen.", "ru-RU": "Репозитории не найдены, используйте команду 'docker push' для добавления образов." }, "tag" : { "en-US": "Tag", "zh-CN": "标签", + "ja-JP": "タグ", "de-DE": "Tag", "ru-RU": "Метка" }, "pull_command": { "en-US": "Pull Command", "zh-CN": "Pull 命令", + "ja-JP": "Pull コマンド", "de-DE": "Pull Befehl", "ru-RU": "Команда для скачивания образа" }, "image_details" : { "en-US": "Image Details", "zh-CN": "镜像详细信息", + "ja-JP": "イメージ詳細", "de-DE": "Image Details", "ru-RU": "Информация об образе" }, "add_members" : { "en-US": "Add Member", "zh-CN": "添加成员", + "ja-JP": "メンバーを追加する", "de-DE": "Mitglied hinzufügen", "ru-RU": "Добавить Участника" }, "edit_members" : { "en-US": "Edit Members", "zh-CN": "编辑成员", + "ja-JP": "メンバーを編集する", "de-DE": "Mitglieder bearbeiten", "ru-RU": "Редактировать Участников" }, "add_member_failed" : { "en-US": "Adding Member Failed", "zh-CN": "添加成员失败", + "ja-JP": "メンバーを追加できません出した", "de-DE": "Mitglied hinzufügen fehlgeschlagen", "ru-RU": "Ошибка при добавлении нового участника" }, "please_input_username" : { "en-US": "Please input a username.", "zh-CN": "请输入用户名。", + "ja-JP": "ユーザ名を入力してください。", "de-DE": "Bitte geben Sie einen Benutzernamen ein.", "ru-RU": "Пожалуйста, введите имя пользователя." }, "please_assign_a_role_to_user" : { "en-US": "Please assign a role to the user.", "zh-CN": "请为用户分配角色。", + "ja-JP": "ユーザーに役割を割り当てるしてください。", "de-DE": "Bitte weisen Sie dem Benutzer eine Rolle zu.", "ru-RU": "Пожалуйста, назначьте роль пользователю." }, "user_id_exists" : { "en-US": "User is already a member.", "zh-CN": "用户已经是成员。", + "ja-JP": "すでにメンバーに登録しました。", "de-DE": "Benutzer ist bereits Mitglied.", "ru-RU": "Пользователь уже является участником." }, "user_id_does_not_exist" : { "en-US": "User does not exist.", "zh-CN": "不存在此用户。", + "ja-JP": "ユーザが見つかりませんでした。", "de-DE": "Benutzer existiert nicht.", "ru-RU": "Пользователя с таким именем не существует." }, "insufficient_privileges" : { "en-US": "Insufficient privileges.", "zh-CN": "权限不足。", + "ja-JP": "権限エラー。", "de-DE": "Unzureichende Berechtigungen.", "ru-RU": "Недостаточно прав." }, "operation_failed" : { "en-US": "Operation Failed", "zh-CN": "操作失败", + "ja-JP": "操作に失敗しました。", "de-DE": "Befehl fehlgeschlagen", "ru-RU": "Ошибка при выполнении данной операции" }, "button_on" : { "en-US": "On", "zh-CN": "打开", + "ja-JP": "オン", "de-DE": "An", "ru-RU": "Вкл." }, "button_off" : { "en-US": "Off", "zh-CN": "关闭", + "ja-JP": "オフ", "de-DE": "Aus", "ru-RU": "Откл." } diff --git a/static/i18n/locale_ru-RU.ini b/static/i18n/locale_ru-RU.ini index 731584abb..eb5521e0b 100644 --- a/static/i18n/locale_ru-RU.ini +++ b/static/i18n/locale_ru-RU.ini @@ -76,6 +76,7 @@ language_en-US = English language_zh-CN = 中文 language_de-DE = Deutsch language_ru-RU = Русский +language_ja-JP = 日本語 copyright = Copyright all_rights_reserved = Все права защищены. index_desc = Проект Harbor представляет собой надежный сервер управления docker-образами корпоративного класса. Компании могут использовать данный сервер в своей инфарструктуе для повышения производительности и безопасности . Проект Harbor может использоваться как в среде разработки так и в продуктивной среде. diff --git a/static/i18n/locale_zh-CN.ini b/static/i18n/locale_zh-CN.ini index 69c34886b..f5940a9af 100644 --- a/static/i18n/locale_zh-CN.ini +++ b/static/i18n/locale_zh-CN.ini @@ -76,6 +76,7 @@ language_en-US = English language_zh-CN = 中文 language_de-DE = Deutsch language_ru-RU = Русский +language_ja-JP = 日本語 copyright = 版权所有 all_rights_reserved = 保留所有权利。 index_desc = Harbor是可靠的企业级Registry服务器。企业用户可使用Harbor搭建私有容器Registry服务,提高生产效率和安全度,既可应用于生产环境,也可以在开发环境中使用。 diff --git a/static/resources/js/common.js b/static/resources/js/common.js index 7c7397d6b..78cdfc638 100644 --- a/static/resources/js/common.js +++ b/static/resources/js/common.js @@ -70,7 +70,8 @@ var SUPPORT_LANGUAGES = { "en-US": "English", "zh-CN": "Chinese", "de-DE": "German", - "ru-RU": "Russian" + "ru-RU": "Russian", + "ja-JP": "Japanese" }; var DEFAULT_LANGUAGE = "en-US"; diff --git a/static/resources/js/item-detail.js b/static/resources/js/item-detail.js index 6f445f717..c63b1a69b 100644 --- a/static/resources/js/item-detail.js +++ b/static/resources/js/item-detail.js @@ -62,7 +62,7 @@ jQuery(function(){ return; } $.each(data, function(i, e){ - var targetId = e.replace(/\//g, "------"); + var targetId = e.replace(/\//g, "------").replace(/\./g, "---"); var row = '
' + '