Merge pull request #317 from ywk253100/sync_image

Sync image
This commit is contained in:
Wenkai Yin 2016-06-07 13:58:16 +08:00
commit 52c7f9716b
24 changed files with 542 additions and 98 deletions

View File

@ -5,6 +5,7 @@ Alexey Erkak <eryigin at mail.ru>
Allen Heavey <xheavey at gmail.com> Allen Heavey <xheavey at gmail.com>
Amanda Zhang <amzhang at vmware.com> Amanda Zhang <amzhang at vmware.com>
Benniu Ji <benniuji at gmail.com> Benniu Ji <benniuji at gmail.com>
Bin Liu <liubin0329 at gmail.com>
Bobby Zhang <junzhang at vmware.com> Bobby Zhang <junzhang at vmware.com>
Chaofeng Wu <chaofengw at vmware.com> Chaofeng Wu <chaofengw at vmware.com>
Daniel Jiang <jiangd at vmware.com> Daniel Jiang <jiangd at vmware.com>

View File

@ -2,8 +2,8 @@ appname = registry
runmode = dev runmode = dev
[lang] [lang]
types = 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 names = en-US|zh-CN|de-DE|ru-RU|ja-JP
[dev] [dev]
httpport = 80 httpport = 80

View File

@ -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. * **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. * **AD/LDAP support**: Harbor integrates with existing enterprise AD/LDAP for user authentication and management.
* **Auditing**: All the operations to the repositories are tracked. * **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. * **RESTful API**: RESTful APIs for most administrative operations, easing intergration with external management platforms.
### Getting Started ### Getting Started
@ -67,7 +67,7 @@ Harbor is available under the [Apache 2 license](LICENSE).
&nbsp; &nbsp; <a href="https://www.caicloud.io" border="0"><img alt="CaiCloud" src="docs/img/caicloudLogoWeb.png"></a> &nbsp; &nbsp; <a href="https://www.caicloud.io" border="0"><img alt="CaiCloud" src="docs/img/caicloudLogoWeb.png"></a>
### Users ### Users
<a href="https://www.madailicai.com/" border="0" target="_blank"><img alt="MaDaiLiCai" src="docs/img/UserMaDai.jpg"></a> <a href="https://www.madailicai.com/" border="0" target="_blank"><img alt="MaDaiLiCai" src="docs/img/UserMaDai.jpg"></a> <a href="https://www.dianrong.com/" border="0" target="_blank"><img alt="Dianrong" src="docs/img/dianrong.png"></a>
### Supporting Technologies ### Supporting Technologies
<img alt="beego" src="docs/img/beegoLogo.png"> Harbor is powered by <a href="http://beego.me/">Beego</a>, an open source framework to build and develop applications in the Go way. <img alt="beego" src="docs/img/beegoLogo.png"> Harbor is powered by <a href="http://beego.me/">Beego</a>, an open source framework to build and develop applications in the Go way.

47
ROADMAP.md Normal file
View File

@ -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.

View File

@ -17,8 +17,10 @@ package api
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"github.com/astaxie/beego/validation"
"github.com/vmware/harbor/auth" "github.com/vmware/harbor/auth"
"github.com/vmware/harbor/dao" "github.com/vmware/harbor/dao"
"github.com/vmware/harbor/models" "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 // ValidateUser checks if the request triggered by a valid user
func (b *BaseAPI) ValidateUser() int { func (b *BaseAPI) ValidateUser() int {

View File

@ -69,9 +69,40 @@ func (pa *RepPolicyAPI) Get() {
// Post creates a policy, and if it is enbled, the replication will be triggered right now. // Post creates a policy, and if it is enbled, the replication will be triggered right now.
func (pa *RepPolicyAPI) Post() { func (pa *RepPolicyAPI) Post() {
policy := models.RepPolicy{} policy := &models.RepPolicy{}
pa.DecodeJSONReq(&policy) pa.DecodeJSONReqAndValidate(policy)
pid, err := dao.AddRepPolicy(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 { if err != nil {
log.Errorf("Failed to add policy to DB, error: %v", err) log.Errorf("Failed to add policy to DB, error: %v", err)
pa.RenderError(http.StatusInternalServerError, "Internal Error") pa.RenderError(http.StatusInternalServerError, "Internal Error")

View File

@ -164,10 +164,16 @@ func (t *TargetAPI) Get() {
// Post ... // Post ...
func (t *TargetAPI) Post() { func (t *TargetAPI) Post() {
target := &models.RepTarget{} target := &models.RepTarget{}
t.DecodeJSONReq(target) t.DecodeJSONReqAndValidate(target)
if len(target.Name) == 0 || len(target.URL) == 0 { ta, err := dao.GetRepTargetByName(target.Name)
t.CustomAbort(http.StatusBadRequest, "name or URL is nil") 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 { if len(target.Password) != 0 {
@ -187,16 +193,32 @@ func (t *TargetAPI) Post() {
func (t *TargetAPI) Put() { func (t *TargetAPI) Put() {
id := t.getIDFromURL() id := t.getIDFromURL()
if id == 0 { 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{} target := &models.RepTarget{}
t.DecodeJSONReq(target) t.DecodeJSONReqAndValidate(target)
if target.ID == 0 { originTarget, err := dao.GetRepTarget(id)
target.ID = 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 { if len(target.Password) != 0 {
target.Password = utils.ReversibleEncrypt(target.Password) target.Password = utils.ReversibleEncrypt(target.Password)
} }

View File

@ -69,7 +69,7 @@ func (c *CommonController) Login() {
// SwitchLanguage handles UI request to switch between different languages and re-render template based on language. // SwitchLanguage handles UI request to switch between different languages and re-render template based on language.
func (c *CommonController) SwitchLanguage() { func (c *CommonController) SwitchLanguage() {
lang := c.GetString("lang") 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.SetSession("lang", lang)
c.Data["Lang"] = lang c.Data["Lang"] = lang
} }

View File

@ -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) { func TestAddRepPolicy(t *testing.T) {
policy := models.RepPolicy{ policy := models.RepPolicy{
ProjectID: 1, 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) { func TestDisableRepPolicy(t *testing.T) {
err := DisableRepPolicy(policyID) err := DisableRepPolicy(policyID)
if err != nil { if err != nil {

View File

@ -11,13 +11,13 @@ import (
// AddRepTarget ... // AddRepTarget ...
func AddRepTarget(target models.RepTarget) (int64, error) { func AddRepTarget(target models.RepTarget) (int64, error) {
o := orm.NewOrm() o := GetOrmer()
return o.Insert(&target) return o.Insert(&target)
} }
// GetRepTarget ... // GetRepTarget ...
func GetRepTarget(id int64) (*models.RepTarget, error) { func GetRepTarget(id int64) (*models.RepTarget, error) {
o := orm.NewOrm() o := GetOrmer()
t := models.RepTarget{ID: id} t := models.RepTarget{ID: id}
err := o.Read(&t) err := o.Read(&t)
if err == orm.ErrNoRows { if err == orm.ErrNoRows {
@ -26,28 +26,34 @@ func GetRepTarget(id int64) (*models.RepTarget, error) {
return &t, err 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 ... // DeleteRepTarget ...
func DeleteRepTarget(id int64) error { func DeleteRepTarget(id int64) error {
o := orm.NewOrm() o := GetOrmer()
_, err := o.Delete(&models.RepTarget{ID: id}) _, err := o.Delete(&models.RepTarget{ID: id})
return err return err
} }
// UpdateRepTarget ... // UpdateRepTarget ...
func UpdateRepTarget(target models.RepTarget) error { func UpdateRepTarget(target models.RepTarget) error {
o := orm.NewOrm() o := GetOrmer()
if len(target.Password) != 0 { _, err := o.Update(&target, "URL", "Name", "Username", "Password")
_, err := o.Update(&target)
return err
}
_, err := o.Update(&target, "URL", "Name", "Username")
return err return err
} }
// GetAllRepTargets ... // GetAllRepTargets ...
func GetAllRepTargets() ([]*models.RepTarget, error) { func GetAllRepTargets() ([]*models.RepTarget, error) {
o := orm.NewOrm() o := GetOrmer()
qs := o.QueryTable(&models.RepTarget{}) qs := o.QueryTable(&models.RepTarget{})
var targets []*models.RepTarget var targets []*models.RepTarget
_, err := qs.All(&targets) _, err := qs.All(&targets)
@ -56,7 +62,7 @@ func GetAllRepTargets() ([]*models.RepTarget, error) {
// AddRepPolicy ... // AddRepPolicy ...
func AddRepPolicy(policy models.RepPolicy) (int64, error) { 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())` 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 var sql string
if policy.Enabled == 1 { if policy.Enabled == 1 {
@ -78,7 +84,7 @@ func AddRepPolicy(policy models.RepPolicy) (int64, error) {
// GetRepPolicy ... // GetRepPolicy ...
func GetRepPolicy(id int64) (*models.RepPolicy, error) { func GetRepPolicy(id int64) (*models.RepPolicy, error) {
o := orm.NewOrm() o := GetOrmer()
p := models.RepPolicy{ID: id} p := models.RepPolicy{ID: id}
err := o.Read(&p) err := o.Read(&p)
if err == orm.ErrNoRows { if err == orm.ErrNoRows {
@ -87,24 +93,35 @@ func GetRepPolicy(id int64) (*models.RepPolicy, error) {
return &p, err 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 ... // GetRepPolicyByProject ...
func GetRepPolicyByProject(projectID int64) ([]*models.RepPolicy, error) { func GetRepPolicyByProject(projectID int64) ([]*models.RepPolicy, error) {
var res []*models.RepPolicy var res []*models.RepPolicy
o := orm.NewOrm() o := GetOrmer()
_, err := o.QueryTable("replication_policy").Filter("project_id", projectID).All(&res) _, err := o.QueryTable("replication_policy").Filter("project_id", projectID).All(&res)
return res, err return res, err
} }
// DeleteRepPolicy ... // DeleteRepPolicy ...
func DeleteRepPolicy(id int64) error { func DeleteRepPolicy(id int64) error {
o := orm.NewOrm() o := GetOrmer()
_, err := o.Delete(&models.RepPolicy{ID: id}) _, err := o.Delete(&models.RepPolicy{ID: id})
return err return err
} }
// UpdateRepPolicyEnablement ... // UpdateRepPolicyEnablement ...
func UpdateRepPolicyEnablement(id int64, enabled int) error { func UpdateRepPolicyEnablement(id int64, enabled int) error {
o := orm.NewOrm() o := GetOrmer()
p := models.RepPolicy{ p := models.RepPolicy{
ID: id, ID: id,
Enabled: enabled} Enabled: enabled}
@ -125,7 +142,7 @@ func DisableRepPolicy(id int64) error {
// AddRepJob ... // AddRepJob ...
func AddRepJob(job models.RepJob) (int64, error) { func AddRepJob(job models.RepJob) (int64, error) {
o := orm.NewOrm() o := GetOrmer()
if len(job.Status) == 0 { if len(job.Status) == 0 {
job.Status = models.JobPending job.Status = models.JobPending
} }
@ -137,7 +154,7 @@ func AddRepJob(job models.RepJob) (int64, error) {
// GetRepJob ... // GetRepJob ...
func GetRepJob(id int64) (*models.RepJob, error) { func GetRepJob(id int64) (*models.RepJob, error) {
o := orm.NewOrm() o := GetOrmer()
j := models.RepJob{ID: id} j := models.RepJob{ID: id}
err := o.Read(&j) err := o.Read(&j)
if err == orm.ErrNoRows { if err == orm.ErrNoRows {
@ -164,20 +181,20 @@ func GetRepJobToStop(policyID int64) ([]*models.RepJob, error) {
} }
func repJobPolicyIDQs(policyID int64) orm.QuerySeter { func repJobPolicyIDQs(policyID int64) orm.QuerySeter {
o := orm.NewOrm() o := GetOrmer()
return o.QueryTable("replication_job").Filter("policy_id", policyID) return o.QueryTable("replication_job").Filter("policy_id", policyID)
} }
// DeleteRepJob ... // DeleteRepJob ...
func DeleteRepJob(id int64) error { func DeleteRepJob(id int64) error {
o := orm.NewOrm() o := GetOrmer()
_, err := o.Delete(&models.RepJob{ID: id}) _, err := o.Delete(&models.RepJob{ID: id})
return err return err
} }
// UpdateRepJobStatus ... // UpdateRepJobStatus ...
func UpdateRepJobStatus(id int64, status string) error { func UpdateRepJobStatus(id int64, status string) error {
o := orm.NewOrm() o := GetOrmer()
j := models.RepJob{ j := models.RepJob{
ID: id, ID: id,
Status: status, Status: status,

BIN
docs/img/dianrong.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -1,54 +1,56 @@
# migration # Migration guide
Migration is a module for migrating database schema between different version of project [harbor](https://github.com/vmware/harbor) 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 **WARNING!!** You must backup your data before migrating
###installation ###Installation
- step 1: modify migration.cfg - step 1: change `db_username`, `db_password`, `db_port`, `db_name` in migration.cfg
- step 2: build image from dockerfile - step 2: build image from dockerfile
``` ```
cd harbor-migration cd harbor-migration
docker build -t your-image-name . docker build -t migrate-tool .
``` ```
###migration operation ###Migrate Step
- show instruction of harbor-migration - step 1: stop and remove Harbor service
```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
``` ```
docker-compose down docker-compose down
``` ```
- step 2: perform migration operation - step 2: create backup file in `/path/to/backup`
- step 3: rebuild newest harbor images and restart service
```
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 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
```

View File

@ -4,6 +4,7 @@
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship from sqlalchemy.orm import sessionmaker, relationship
from sqlalchemy.dialects import mysql
Base = declarative_base() Base = declarative_base()
@ -20,8 +21,8 @@ class User(Base):
reset_uuid = sa.Column(sa.String(40)) reset_uuid = sa.Column(sa.String(40))
salt = sa.Column(sa.String(40)) salt = sa.Column(sa.String(40))
sysadmin_flag = sa.Column(sa.Integer) sysadmin_flag = sa.Column(sa.Integer)
creation_time = sa.Column(sa.DateTime) creation_time = sa.Column(mysql.TIMESTAMP)
update_time = sa.Column(sa.DateTime) update_time = sa.Column(mysql.TIMESTAMP)
class Properties(Base): class Properties(Base):
__tablename__ = 'properties' __tablename__ = 'properties'
@ -35,8 +36,8 @@ class ProjectMember(Base):
project_id = sa.Column(sa.Integer(), primary_key = True) project_id = sa.Column(sa.Integer(), primary_key = True)
user_id = sa.Column(sa.Integer(), primary_key = True) user_id = sa.Column(sa.Integer(), primary_key = True)
role = sa.Column(sa.Integer(), nullable = False) role = sa.Column(sa.Integer(), nullable = False)
creation_time = sa.Column(sa.DateTime(), nullable = True) creation_time = sa.Column(mysql.TIMESTAMP, nullable = True)
update_time = sa.Column(sa.DateTime(), nullable = True) update_time = sa.Column(mysql.TIMESTAMP, nullable = True)
sa.ForeignKeyConstraint(['project_id'], [u'project.project_id'], ), sa.ForeignKeyConstraint(['project_id'], [u'project.project_id'], ),
sa.ForeignKeyConstraint(['role'], [u'role.role_id'], ), sa.ForeignKeyConstraint(['role'], [u'role.role_id'], ),
sa.ForeignKeyConstraint(['user_id'], [u'user.user_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) project_id = sa.Column(sa.Integer, primary_key=True)
owner_id = sa.Column(sa.ForeignKey(u'user.user_id'), nullable=False, index=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) name = sa.Column(sa.String(30), nullable=False, unique=True)
creation_time = sa.Column(sa.DateTime) creation_time = sa.Column(mysql.TIMESTAMP)
update_time = sa.Column(sa.DateTime) update_time = sa.Column(mysql.TIMESTAMP)
deleted = sa.Column(sa.Integer, nullable=False, server_default=sa.text("'0'")) deleted = sa.Column(sa.Integer, nullable=False, server_default=sa.text("'0'"))
public = sa.Column(sa.Integer, nullable=False, server_default=sa.text("'0'")) public = sa.Column(sa.Integer, nullable=False, server_default=sa.text("'0'"))
owner = relationship(u'User') owner = relationship(u'User')

View File

@ -27,9 +27,10 @@ branch_labels = None
depends_on = None depends_on = None
from alembic import op from alembic import op
from datetime import datetime
from db_meta import * from db_meta import *
from sqlalchemy.dialects import mysql
Session = sessionmaker() Session = sessionmaker()
def upgrade(): def upgrade():
@ -44,12 +45,9 @@ def upgrade():
session.add(Properties(k='schema_version', v='0.1.1')) session.add(Properties(k='schema_version', v='0.1.1'))
#add column to table user #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('sysadmin_flag', sa.Integer(), nullable=True))
op.add_column('user', sa.Column('update_time', sa.DateTime(), nullable=True)) op.add_column('user', sa.Column('update_time', mysql.TIMESTAMP, nullable=True))
#fill update_time data into table user
session.query(User).update({User.update_time: datetime.now()})
#init all sysadmin_flag = 0 #init all sysadmin_flag = 0
session.query(User).update({User.sysadmin_flag: 0}) session.query(User).update({User.sysadmin_flag: 0})
@ -62,7 +60,7 @@ def upgrade():
for result in join_result: for result in join_result:
session.add(ProjectMember(project_id=result.project_role.project_id, \ session.add(ProjectMember(project_id=result.project_role.project_id, \
user_id=result.user_id, role=result.project_role.role_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 #update sysadmin_flag
sys_admin_result = session.query(UserProjectRole).\ sys_admin_result = session.query(UserProjectRole).\
@ -89,10 +87,8 @@ def upgrade():
session.query(Access).update({Access.access_id: Access.access_id - 1}) session.query(Access).update({Access.access_id: Access.access_id - 1})
#add column to table project #add column to table project
op.add_column('project', sa.Column('update_time', sa.DateTime(), nullable=True)) 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() session.commit()
def downgrade(): def downgrade():

View File

@ -2,6 +2,8 @@ package models
import ( import (
"time" "time"
"github.com/astaxie/beego/validation"
) )
const ( const (
@ -42,6 +44,33 @@ type RepPolicy struct {
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"` 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 // 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. // a repository to/from a remote registry instance.
type RepJob struct { type RepJob struct {
@ -68,17 +97,42 @@ type RepTarget struct {
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"` 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 //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" return "replication_target"
} }
//TableName is required by by beego orm to map RepJob to table replication_job //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" return "replication_job"
} }
//TableName is required by by beego orm to map RepPolicy to table replication_policy //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" return "replication_policy"
} }

View File

@ -75,6 +75,7 @@ language_en-US = English
language_zh-CN = 中文 language_zh-CN = 中文
language_de-DE = Deutsch language_de-DE = Deutsch
language_ru-RU = Русский language_ru-RU = Русский
language_ja-JP = 日本語
copyright = Copyright copyright = Copyright
all_rights_reserved = Alle Rechte vorbehalten. 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. 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.

View File

@ -76,6 +76,7 @@ language_en-US = English
language_zh-CN = 中文 language_zh-CN = 中文
language_de-DE = Deutsch language_de-DE = Deutsch
language_ru-RU = Русский language_ru-RU = Русский
language_ja-JP = 日本語
copyright = Copyright copyright = Copyright
all_rights_reserved = All rights reserved. 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. 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.

View File

@ -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 サビース

View File

@ -16,378 +16,441 @@ var global_messages = {
"username_is_required" : { "username_is_required" : {
"en-US": "Username is required.", "en-US": "Username is required.",
"zh-CN": "用户名为必填项。", "zh-CN": "用户名为必填项。",
"ja-JP": "ユーザ名は必須項目です。",
"de-DE": "Benutzername erforderlich.", "de-DE": "Benutzername erforderlich.",
"ru-RU": "Требуется ввести имя пользователя." "ru-RU": "Требуется ввести имя пользователя."
}, },
"username_has_been_taken" : { "username_has_been_taken" : {
"en-US": "Username has been taken.", "en-US": "Username has been taken.",
"zh-CN": "用户名已被占用。", "zh-CN": "用户名已被占用。",
"ja-JP": "ユーザ名はすでに登録されました。",
"de-DE": "Benutzername bereits vergeben.", "de-DE": "Benutzername bereits vergeben.",
"ru-RU": "Имя пользователя уже используется." "ru-RU": "Имя пользователя уже используется."
}, },
"username_is_too_long" : { "username_is_too_long" : {
"en-US": "Username is too long. (maximum 20 characters)", "en-US": "Username is too long. (maximum 20 characters)",
"zh-CN": "用户名长度超出限制。最长为20个字符", "zh-CN": "用户名长度超出限制。最长为20个字符",
"ja-JP": "ユーザ名が長すぎです。20文字まで",
"de-DE": "Benutzername ist zu lang. (maximal 20 Zeichen)", "de-DE": "Benutzername ist zu lang. (maximal 20 Zeichen)",
"ru-RU": "Имя пользователя слишком длинное. (максимум 20 символов)" "ru-RU": "Имя пользователя слишком длинное. (максимум 20 символов)"
}, },
"username_contains_illegal_chars": { "username_contains_illegal_chars": {
"en-US": "Username contains illegal character(s).", "en-US": "Username contains illegal character(s).",
"zh-CN": "用户名包含不合法的字符。", "zh-CN": "用户名包含不合法的字符。",
"ja-JP": "ユーザ名に使えない文字が入っています。",
"de-DE": "Benutzername enthält ungültige Zeichen.", "de-DE": "Benutzername enthält ungültige Zeichen.",
"ru-RU": "Имя пользователя содержит недопустимые символы." "ru-RU": "Имя пользователя содержит недопустимые символы."
}, },
"email_is_required" : { "email_is_required" : {
"en-US": "Email is required.", "en-US": "Email is required.",
"zh-CN": "邮箱为必填项。", "zh-CN": "邮箱为必填项。",
"ja-JP": "メールアドレスが必須です。",
"de-DE": "E-Mail Adresse erforderlich.", "de-DE": "E-Mail Adresse erforderlich.",
"ru-RU": "Требуется ввести E-mail адрес." "ru-RU": "Требуется ввести E-mail адрес."
}, },
"email_contains_illegal_chars" : { "email_contains_illegal_chars" : {
"en-US": "Email contains illegal character(s).", "en-US": "Email contains illegal character(s).",
"zh-CN": "邮箱包含不合法的字符。", "zh-CN": "邮箱包含不合法的字符。",
"ja-JP": "メールアドレスに使えない文字が入っています。",
"de-DE": "E-Mail Adresse enthält ungültige Zeichen.", "de-DE": "E-Mail Adresse enthält ungültige Zeichen.",
"ru-RU": "E-mail адрес содержит недопеустимые символы." "ru-RU": "E-mail адрес содержит недопеустимые символы."
}, },
"email_has_been_taken" : { "email_has_been_taken" : {
"en-US": "Email has been taken.", "en-US": "Email has been taken.",
"zh-CN": "邮箱已被占用。", "zh-CN": "邮箱已被占用。",
"ja-JP": "メールアドレスがすでに使われました。",
"de-DE": "E-Mail Adresse wird bereits verwendet.", "de-DE": "E-Mail Adresse wird bereits verwendet.",
"ru-RU": "Такой E-mail адрес уже используется." "ru-RU": "Такой E-mail адрес уже используется."
}, },
"email_content_illegal" : { "email_content_illegal" : {
"en-US": "Email format is illegal.", "en-US": "Email format is illegal.",
"zh-CN": "邮箱格式不合法。", "zh-CN": "邮箱格式不合法。",
"ja-JP": "メールアドレスフォーマットエラー。",
"de-DE": "Format der E-Mail Adresse ist ungültig.", "de-DE": "Format der E-Mail Adresse ist ungültig.",
"ru-RU": "Недопустимый формат E-mail адреса." "ru-RU": "Недопустимый формат E-mail адреса."
}, },
"email_does_not_exist" : { "email_does_not_exist" : {
"en-US": "Email does not exist.", "en-US": "Email does not exist.",
"zh-CN": "邮箱不存在。", "zh-CN": "邮箱不存在。",
"ja-JP": "メールアドレスが存在しません。",
"de-DE": "E-Mail Adresse existiert nicht.", "de-DE": "E-Mail Adresse existiert nicht.",
"ru-RU": "E-mail адрес не существует." "ru-RU": "E-mail адрес не существует."
}, },
"realname_is_required" : { "realname_is_required" : {
"en-US": "Full name is required.", "en-US": "Full name is required.",
"zh-CN": "全名为必填项。", "zh-CN": "全名为必填项。",
"ja-JP": "フルネームが必須です。",
"de-DE": "Vollständiger Name erforderlich.", "de-DE": "Vollständiger Name erforderlich.",
"ru-RU": "Требуется ввести полное имя." "ru-RU": "Требуется ввести полное имя."
}, },
"realname_is_too_long" : { "realname_is_too_long" : {
"en-US": "Full name is too long. (maximum 20 characters)", "en-US": "Full name is too long. (maximum 20 characters)",
"zh-CN": "全名长度超出限制。最长为20个字符", "zh-CN": "全名长度超出限制。最长为20个字符",
"ja-JP": "フルネームは長すぎです。20文字まで",
"de-DE": "Vollständiger Name zu lang. (maximal 20 Zeichen)", "de-DE": "Vollständiger Name zu lang. (maximal 20 Zeichen)",
"ru-RU": "Полное имя слишком длинное. (максимум 20 символов)" "ru-RU": "Полное имя слишком длинное. (максимум 20 символов)"
}, },
"realname_contains_illegal_chars" : { "realname_contains_illegal_chars" : {
"en-US": "Full name contains illegal character(s).", "en-US": "Full name contains illegal character(s).",
"zh-CN": "全名包含不合法的字符。", "zh-CN": "全名包含不合法的字符。",
"ja-JP": "フルネームに使えない文字が入っています。",
"de-DE": "Vollständiger Name enthält ungültige Zeichen.", "de-DE": "Vollständiger Name enthält ungültige Zeichen.",
"ru-RU": "Полное имя содержит недопустимые символы." "ru-RU": "Полное имя содержит недопустимые символы."
}, },
"password_is_required" : { "password_is_required" : {
"en-US": "Password is required.", "en-US": "Password is required.",
"zh-CN": "密码为必填项。", "zh-CN": "密码为必填项。",
"ja-JP": "パスワードは必須です。",
"de-DE": "Passwort erforderlich.", "de-DE": "Passwort erforderlich.",
"ru-RU": "Требуется ввести пароль." "ru-RU": "Требуется ввести пароль."
}, },
"password_is_invalid" : { "password_is_invalid" : {
"en-US": "Password is invalid. At least 7 characters with 1 lowercase letter, 1 capital letter and 1 numeric character.", "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个数字。", "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", "de-DE": "Passwort ungültig. Mindestens sieben Zeichen bestehend aus einem Kleinbuchstaben, einem Großbuchstaben und einer Zahl",
"ru-RU": "Такой пароль недопустим. Парольл должен содержать Минимум 7 символов, в которых будет присутствовать по меньшей мере 1 буква нижнего регистра, 1 буква верхнего регистра и 1 цифра" "ru-RU": "Такой пароль недопустим. Парольл должен содержать Минимум 7 символов, в которых будет присутствовать по меньшей мере 1 буква нижнего регистра, 1 буква верхнего регистра и 1 цифра"
}, },
"password_is_too_long" : { "password_is_too_long" : {
"en-US": "Password is too long. (maximum 20 characters)", "en-US": "Password is too long. (maximum 20 characters)",
"zh-CN": "密码长度超出限制。最长为20个字符", "zh-CN": "密码长度超出限制。最长为20个字符",
"ja-JP": "パスワードは長すぎです。20文字まで",
"de-DE": "Passwort zu lang. (maximal 20 Zeichen)", "de-DE": "Passwort zu lang. (maximal 20 Zeichen)",
"ru-RU": "Пароль слишком длинный (максимум 20 символов)" "ru-RU": "Пароль слишком длинный (максимум 20 символов)"
}, },
"password_does_not_match" : { "password_does_not_match" : {
"en-US": "Passwords do not match.", "en-US": "Passwords do not match.",
"zh-CN": "两次密码输入不一致。", "zh-CN": "两次密码输入不一致。",
"ja-JP": "確認のパスワードが正しくありません。",
"de-DE": "Passwörter stimmen nicht überein.", "de-DE": "Passwörter stimmen nicht überein.",
"ru-RU": "Пароли не совпадают." "ru-RU": "Пароли не совпадают."
}, },
"comment_is_too_long" : { "comment_is_too_long" : {
"en-US": "Comment is too long. (maximum 20 characters)", "en-US": "Comment is too long. (maximum 20 characters)",
"zh-CN": "备注长度超出限制。最长为20个字符", "zh-CN": "备注长度超出限制。最长为20个字符",
"ja-JP": "コメントは長すぎです。20文字まで",
"de-DE": "Kommentar zu lang. (maximal 20 Zeichen)", "de-DE": "Kommentar zu lang. (maximal 20 Zeichen)",
"ru-RU": "Комментарий слишком длинный. (максимум 20 символов)" "ru-RU": "Комментарий слишком длинный. (максимум 20 символов)"
}, },
"comment_contains_illegal_chars" : { "comment_contains_illegal_chars" : {
"en-US": "Comment contains illegal character(s).", "en-US": "Comment contains illegal character(s).",
"zh-CN": "备注包含不合法的字符。", "zh-CN": "备注包含不合法的字符。",
"ja-JP": "コメントに使えない文字が入っています。",
"de-DE": "Kommentar enthält ungültige Zeichen.", "de-DE": "Kommentar enthält ungültige Zeichen.",
"ru-RU": "Комментарий содержит недопустимые символы." "ru-RU": "Комментарий содержит недопустимые символы."
}, },
"project_name_is_required" : { "project_name_is_required" : {
"en-US": "Project name is required.", "en-US": "Project name is required.",
"zh-CN": "项目名称为必填项。", "zh-CN": "项目名称为必填项。",
"ja-JP": "プロジェクト名は必須です。",
"de-DE": "Projektname erforderlich.", "de-DE": "Projektname erforderlich.",
"ru-RU": "Необходимо ввести название Проекта." "ru-RU": "Необходимо ввести название Проекта."
}, },
"project_name_is_too_short" : { "project_name_is_too_short" : {
"en-US": "Project name is too short. (minimum 4 characters)", "en-US": "Project name is too short. (minimum 4 characters)",
"zh-CN": "项目名称至少要求 4个字符。", "zh-CN": "项目名称至少要求 4个字符。",
"ja-JP": "プロジェクト名は4文字以上です。",
"de-DE": "Projektname zu kurz. (mindestens 4 Zeichen)", "de-DE": "Projektname zu kurz. (mindestens 4 Zeichen)",
"ru-RU": "Название проекта слишком короткое. (миниму 4 символа)" "ru-RU": "Название проекта слишком короткое. (миниму 4 символа)"
}, },
"project_name_is_too_long" : { "project_name_is_too_long" : {
"en-US": "Project name is too long. (maximum 30 characters)", "en-US": "Project name is too long. (maximum 30 characters)",
"zh-CN": "项目名称长度超出限制。最长为30个字符", "zh-CN": "项目名称长度超出限制。最长为30个字符",
"ja-JP": "プロジェクト名は長すぎです。30文字まで",
"de-DE": "Projektname zu lang. (maximal 30 Zeichen)", "de-DE": "Projektname zu lang. (maximal 30 Zeichen)",
"ru-RU": "Название проекта слишком длинное (максимум 30 символов)" "ru-RU": "Название проекта слишком длинное (максимум 30 символов)"
}, },
"project_name_contains_illegal_chars" : { "project_name_contains_illegal_chars" : {
"en-US": "Project name contains illegal character(s).", "en-US": "Project name contains illegal character(s).",
"zh-CN": "项目名称包含不合法的字符。", "zh-CN": "项目名称包含不合法的字符。",
"ja-JP": "プロジェクト名に使えない文字が入っています。",
"de-DE": "Projektname enthält ungültige Zeichen.", "de-DE": "Projektname enthält ungültige Zeichen.",
"ru-RU": "Название проекта содержит недопустимые символы." "ru-RU": "Название проекта содержит недопустимые символы."
}, },
"project_exists" : { "project_exists" : {
"en-US": "Project exists.", "en-US": "Project exists.",
"zh-CN": "项目已存在。", "zh-CN": "项目已存在。",
"ja-JP": "プロジェクトはすでに存在しました。",
"de-DE": "Projekt existiert bereits.", "de-DE": "Projekt existiert bereits.",
"ru-RU": "Такой проект уже существует." "ru-RU": "Такой проект уже существует."
}, },
"delete_user" : { "delete_user" : {
"en-US": "Delete User", "en-US": "Delete User",
"zh-CN": "删除用户", "zh-CN": "删除用户",
"ja-JP": "ユーザを削除",
"de-DE": "Benutzer löschen", "de-DE": "Benutzer löschen",
"ru-RU": "Удалить пользователя" "ru-RU": "Удалить пользователя"
}, },
"are_you_sure_to_delete_user" : { "are_you_sure_to_delete_user" : {
"en-US": "Are you sure to delete ", "en-US": "Are you sure to delete ",
"zh-CN": "确认要删除用户 ", "zh-CN": "确认要删除用户 ",
"ja-JP": "ユーザを削除でよろしでしょうか ",
"de-DE": "Sind Sie sich sicher, dass Sie folgenden Benutzer löschen möchten: ", "de-DE": "Sind Sie sich sicher, dass Sie folgenden Benutzer löschen möchten: ",
"ru-RU": "Вы уверены что хотите удалить пользователя? " "ru-RU": "Вы уверены что хотите удалить пользователя? "
}, },
"input_your_username_and_password" : { "input_your_username_and_password" : {
"en-US": "Please input your username and password.", "en-US": "Please input your username and password.",
"zh-CN": "请输入用户名和密码。", "zh-CN": "请输入用户名和密码。",
"ja-JP": "ユーザ名とパスワードを入力してください。",
"de-DE": "Bitte geben Sie ihr Benutzername und Passwort ein.", "de-DE": "Bitte geben Sie ihr Benutzername und Passwort ein.",
"ru-RU": "Введите имя пользователя и пароль." "ru-RU": "Введите имя пользователя и пароль."
}, },
"check_your_username_or_password" : { "check_your_username_or_password" : {
"en-US": "Please check your username or password.", "en-US": "Please check your username or password.",
"zh-CN": "请输入正确的用户名或密码。", "zh-CN": "请输入正确的用户名或密码。",
"ja-JP": "正しいユーザ名とパスワードを入力してください。",
"de-DE": "Bitte überprüfen Sie ihren Benutzernamen und Passwort.", "de-DE": "Bitte überprüfen Sie ihren Benutzernamen und Passwort.",
"ru-RU": "Проверьте свои имя пользователя и пароль." "ru-RU": "Проверьте свои имя пользователя и пароль."
}, },
"title_login_failed" : { "title_login_failed" : {
"en-US": "Login Failed", "en-US": "Login Failed",
"zh-CN": "登录失败", "zh-CN": "登录失败",
"ja-JP": "ログインに失敗しました。",
"de-DE": "Anmeldung fehlgeschlagen", "de-DE": "Anmeldung fehlgeschlagen",
"ru-RU": "Ошибка входа" "ru-RU": "Ошибка входа"
}, },
"title_change_password" : { "title_change_password" : {
"en-US": "Change Password", "en-US": "Change Password",
"zh-CN": "修改密码", "zh-CN": "修改密码",
"ja-JP": "パスワードを変更します。",
"de-DE": "Passwort ändern", "de-DE": "Passwort ändern",
"ru-RU": "Сменить пароль" "ru-RU": "Сменить пароль"
}, },
"change_password_successfully" : { "change_password_successfully" : {
"en-US": "Password changed successfully.", "en-US": "Password changed successfully.",
"zh-CN": "密码已修改。", "zh-CN": "密码已修改。",
"ja-JP": "パスワードを変更しました。",
"de-DE": "Passwort erfolgreich geändert.", "de-DE": "Passwort erfolgreich geändert.",
"ru-RU": "Пароль успешно изменен." "ru-RU": "Пароль успешно изменен."
}, },
"title_forgot_password" : { "title_forgot_password" : {
"en-US": "Forgot Password", "en-US": "Forgot Password",
"zh-CN": "忘记密码", "zh-CN": "忘记密码",
"ja-JP": "パスワードをリセットします。",
"de-DE": "Passwort vergessen", "de-DE": "Passwort vergessen",
"ru-RU": "Забыли пароль?" "ru-RU": "Забыли пароль?"
}, },
"email_has_been_sent" : { "email_has_been_sent" : {
"en-US": "Email for resetting password has been sent.", "en-US": "Email for resetting password has been sent.",
"zh-CN": "重置密码邮件已发送。", "zh-CN": "重置密码邮件已发送。",
"ja-JP": "パスワードをリセットするメールを送信しました。",
"de-DE": "Eine E-Mail mit einem Wiederherstellungslink wurde an Sie gesendet.", "de-DE": "Eine E-Mail mit einem Wiederherstellungslink wurde an Sie gesendet.",
"ru-RU": "На ваш E-mail было выслано письмо с инструкциями по сбросу пароля." "ru-RU": "На ваш E-mail было выслано письмо с инструкциями по сбросу пароля."
}, },
"send_email_failed" : { "send_email_failed" : {
"en-US": "Failed to send Email for resetting password.", "en-US": "Failed to send Email for resetting password.",
"zh-CN": "重置密码邮件发送失败。", "zh-CN": "重置密码邮件发送失败。",
"ja-JP": "パスワードをリセットするメールを送信する際エラーが出ました",
"de-DE": "Fehler beim Senden der Wiederherstellungs-E-Mail.", "de-DE": "Fehler beim Senden der Wiederherstellungs-E-Mail.",
"ru-RU": "Ошибка отправки сообщения." "ru-RU": "Ошибка отправки сообщения."
}, },
"please_login_first" : { "please_login_first" : {
"en-US": "Please login first.", "en-US": "Please login first.",
"zh-CN": "请先登录。", "zh-CN": "请先登录。",
"ja-JP": "この先にログインが必要です。",
"de-DE": "Bitte melden Sie sich zuerst an.", "de-DE": "Bitte melden Sie sich zuerst an.",
"ru-RU": "Сначала выполните вход в систему." "ru-RU": "Сначала выполните вход в систему."
}, },
"old_password_is_not_correct" : { "old_password_is_not_correct" : {
"en-US": "Old password is not correct.", "en-US": "Old password is not correct.",
"zh-CN": "原密码输入不正确。", "zh-CN": "原密码输入不正确。",
"ja-JP": "現在のパスワードが正しく入力されていません。",
"de-DE": "Altes Passwort ist nicht korrekt.", "de-DE": "Altes Passwort ist nicht korrekt.",
"ru-RU": "Старый пароль введен неверно." "ru-RU": "Старый пароль введен неверно."
}, },
"please_input_new_password" : { "please_input_new_password" : {
"en-US": "Please input new password.", "en-US": "Please input new password.",
"zh-CN": "请输入新密码。", "zh-CN": "请输入新密码。",
"ja-JP": "あたらしいパスワードを入力してください",
"de-DE": "Bitte geben Sie ihr neues Passwort ein.", "de-DE": "Bitte geben Sie ihr neues Passwort ein.",
"ru-RU": "Пожалуйста, введите новый пароль." "ru-RU": "Пожалуйста, введите новый пароль."
}, },
"invalid_reset_url": { "invalid_reset_url": {
"en-US": "Invalid URL for resetting password.", "en-US": "Invalid URL for resetting password.",
"zh-CN": "无效密码重置链接。", "zh-CN": "无效密码重置链接。",
"ja-JP": "無効なパスワードをリセットするリンク。",
"de-DE": "Ungültige URL zum Passwort wiederherstellen.", "de-DE": "Ungültige URL zum Passwort wiederherstellen.",
"ru-RU": "Неверный URL для сброса пароля." "ru-RU": "Неверный URL для сброса пароля."
}, },
"reset_password_successfully" : { "reset_password_successfully" : {
"en-US": "Reset password successfully.", "en-US": "Reset password successfully.",
"zh-CN": "密码重置成功。", "zh-CN": "密码重置成功。",
"ja-JP": "パスワードをリセットしました。",
"de-DE": "Passwort erfolgreich wiederhergestellt.", "de-DE": "Passwort erfolgreich wiederhergestellt.",
"ru-RU": "Пароль успешно сброшен." "ru-RU": "Пароль успешно сброшен."
}, },
"internal_error": { "internal_error": {
"en-US": "Internal error.", "en-US": "Internal error.",
"zh-CN": "内部错误,请联系系统管理员。", "zh-CN": "内部错误,请联系系统管理员。",
"ja-JP": "エラーが出ました、管理者に連絡してください。",
"de-DE": "Interner Fehler.", "de-DE": "Interner Fehler.",
"ru-RU": "Внутренняя ошибка." "ru-RU": "Внутренняя ошибка."
}, },
"title_reset_password" : { "title_reset_password" : {
"en-US": "Reset Password", "en-US": "Reset Password",
"zh-CN": "重置密码", "zh-CN": "重置密码",
"ja-JP": "パスワードをリセットする",
"de-DE": "Passwort zurücksetzen", "de-DE": "Passwort zurücksetzen",
"ru-RU": "Сбросить пароль" "ru-RU": "Сбросить пароль"
}, },
"title_sign_up" : { "title_sign_up" : {
"en-US": "Sign Up", "en-US": "Sign Up",
"zh-CN": "注册", "zh-CN": "注册",
"ja-JP": "登録",
"de-DE": "Registrieren", "de-DE": "Registrieren",
"ru-RU": "Регистрация" "ru-RU": "Регистрация"
}, },
"title_add_user": { "title_add_user": {
"en-US": "Add User", "en-US": "Add User",
"zh-CN": "新增用户", "zh-CN": "新增用户",
"ja-JP": "ユーザを追加",
"de-DE": "Benutzer hinzufügen", "de-DE": "Benutzer hinzufügen",
"ru-RU": "Добавить пользователя" "ru-RU": "Добавить пользователя"
}, },
"registered_successfully": { "registered_successfully": {
"en-US": "Signed up successfully.", "en-US": "Signed up successfully.",
"zh-CN": "注册成功。", "zh-CN": "注册成功。",
"ja-JP": "登録しました。",
"de-DE": "Erfolgreich registriert.", "de-DE": "Erfolgreich registriert.",
"ru-RU": "Регистрация прошла успешно." "ru-RU": "Регистрация прошла успешно."
}, },
"registered_failed" : { "registered_failed" : {
"en-US": "Failed to sign up.", "en-US": "Failed to sign up.",
"zh-CN": "注册失败。", "zh-CN": "注册失败。",
"ja-JP": "登録でませんでした。",
"de-DE": "Registrierung fehlgeschlagen.", "de-DE": "Registrierung fehlgeschlagen.",
"ru-RU": "Ошибка регистрации." "ru-RU": "Ошибка регистрации."
}, },
"added_user_successfully": { "added_user_successfully": {
"en-US": "Added user successfully.", "en-US": "Added user successfully.",
"zh-CN": "新增用户成功。", "zh-CN": "新增用户成功。",
"ja-JP": "ユーザを追加しました。",
"de-DE": "Benutzer erfolgreich erstellt.", "de-DE": "Benutzer erfolgreich erstellt.",
"ru-RU": "Пользователь успешно добавлен." "ru-RU": "Пользователь успешно добавлен."
}, },
"added_user_failed": { "added_user_failed": {
"en-US": "Adding user failed.", "en-US": "Adding user failed.",
"zh-CN": "新增用户失败。", "zh-CN": "新增用户失败。",
"ja-JP": "ユーザを追加できませんでした。",
"de-DE": "Benutzer erstellen fehlgeschlagen.", "de-DE": "Benutzer erstellen fehlgeschlagen.",
"ru-RU": "Ошибка добавления пользователя." "ru-RU": "Ошибка добавления пользователя."
}, },
"projects": { "projects": {
"en-US": "Projects", "en-US": "Projects",
"zh-CN": "项目", "zh-CN": "项目",
"ja-JP": "プロジェクト",
"de-DE": "Projekte", "de-DE": "Projekte",
"ru-RU": "Проекты" "ru-RU": "Проекты"
}, },
"repositories" : { "repositories" : {
"en-US": "Repositories", "en-US": "Repositories",
"zh-CN": "镜像仓库", "zh-CN": "镜像仓库",
"ja-JP": "リポジトリ",
"de-DE": "Repositories", "de-DE": "Repositories",
"ru-RU": "Репозитории" "ru-RU": "Репозитории"
}, },
"no_repo_exists" : { "no_repo_exists" : {
"en-US": "No repositories found, please use 'docker push' to upload images.", "en-US": "No repositories found, please use 'docker push' to upload images.",
"zh-CN": "未发现镜像请用docker push命令上传镜像。", "zh-CN": "未发现镜像请用docker push命令上传镜像。",
"ja-JP": "イメージが見つかりませんでした。docker pushを利用しイメージをアップロードしてください。",
"de-DE": "Keine Repositories gefunden, bitte benutzen Sie 'docker push' um ein Image hochzuladen.", "de-DE": "Keine Repositories gefunden, bitte benutzen Sie 'docker push' um ein Image hochzuladen.",
"ru-RU": "Репозитории не найдены, используйте команду 'docker push' для добавления образов." "ru-RU": "Репозитории не найдены, используйте команду 'docker push' для добавления образов."
}, },
"tag" : { "tag" : {
"en-US": "Tag", "en-US": "Tag",
"zh-CN": "标签", "zh-CN": "标签",
"ja-JP": "タグ",
"de-DE": "Tag", "de-DE": "Tag",
"ru-RU": "Метка" "ru-RU": "Метка"
}, },
"pull_command": { "pull_command": {
"en-US": "Pull Command", "en-US": "Pull Command",
"zh-CN": "Pull 命令", "zh-CN": "Pull 命令",
"ja-JP": "Pull コマンド",
"de-DE": "Pull Befehl", "de-DE": "Pull Befehl",
"ru-RU": "Команда для скачивания образа" "ru-RU": "Команда для скачивания образа"
}, },
"image_details" : { "image_details" : {
"en-US": "Image Details", "en-US": "Image Details",
"zh-CN": "镜像详细信息", "zh-CN": "镜像详细信息",
"ja-JP": "イメージ詳細",
"de-DE": "Image Details", "de-DE": "Image Details",
"ru-RU": "Информация об образе" "ru-RU": "Информация об образе"
}, },
"add_members" : { "add_members" : {
"en-US": "Add Member", "en-US": "Add Member",
"zh-CN": "添加成员", "zh-CN": "添加成员",
"ja-JP": "メンバーを追加する",
"de-DE": "Mitglied hinzufügen", "de-DE": "Mitglied hinzufügen",
"ru-RU": "Добавить Участника" "ru-RU": "Добавить Участника"
}, },
"edit_members" : { "edit_members" : {
"en-US": "Edit Members", "en-US": "Edit Members",
"zh-CN": "编辑成员", "zh-CN": "编辑成员",
"ja-JP": "メンバーを編集する",
"de-DE": "Mitglieder bearbeiten", "de-DE": "Mitglieder bearbeiten",
"ru-RU": "Редактировать Участников" "ru-RU": "Редактировать Участников"
}, },
"add_member_failed" : { "add_member_failed" : {
"en-US": "Adding Member Failed", "en-US": "Adding Member Failed",
"zh-CN": "添加成员失败", "zh-CN": "添加成员失败",
"ja-JP": "メンバーを追加できません出した",
"de-DE": "Mitglied hinzufügen fehlgeschlagen", "de-DE": "Mitglied hinzufügen fehlgeschlagen",
"ru-RU": "Ошибка при добавлении нового участника" "ru-RU": "Ошибка при добавлении нового участника"
}, },
"please_input_username" : { "please_input_username" : {
"en-US": "Please input a username.", "en-US": "Please input a username.",
"zh-CN": "请输入用户名。", "zh-CN": "请输入用户名。",
"ja-JP": "ユーザ名を入力してください。",
"de-DE": "Bitte geben Sie einen Benutzernamen ein.", "de-DE": "Bitte geben Sie einen Benutzernamen ein.",
"ru-RU": "Пожалуйста, введите имя пользователя." "ru-RU": "Пожалуйста, введите имя пользователя."
}, },
"please_assign_a_role_to_user" : { "please_assign_a_role_to_user" : {
"en-US": "Please assign a role to the user.", "en-US": "Please assign a role to the user.",
"zh-CN": "请为用户分配角色。", "zh-CN": "请为用户分配角色。",
"ja-JP": "ユーザーに役割を割り当てるしてください。",
"de-DE": "Bitte weisen Sie dem Benutzer eine Rolle zu.", "de-DE": "Bitte weisen Sie dem Benutzer eine Rolle zu.",
"ru-RU": "Пожалуйста, назначьте роль пользователю." "ru-RU": "Пожалуйста, назначьте роль пользователю."
}, },
"user_id_exists" : { "user_id_exists" : {
"en-US": "User is already a member.", "en-US": "User is already a member.",
"zh-CN": "用户已经是成员。", "zh-CN": "用户已经是成员。",
"ja-JP": "すでにメンバーに登録しました。",
"de-DE": "Benutzer ist bereits Mitglied.", "de-DE": "Benutzer ist bereits Mitglied.",
"ru-RU": "Пользователь уже является участником." "ru-RU": "Пользователь уже является участником."
}, },
"user_id_does_not_exist" : { "user_id_does_not_exist" : {
"en-US": "User does not exist.", "en-US": "User does not exist.",
"zh-CN": "不存在此用户。", "zh-CN": "不存在此用户。",
"ja-JP": "ユーザが見つかりませんでした。",
"de-DE": "Benutzer existiert nicht.", "de-DE": "Benutzer existiert nicht.",
"ru-RU": "Пользователя с таким именем не существует." "ru-RU": "Пользователя с таким именем не существует."
}, },
"insufficient_privileges" : { "insufficient_privileges" : {
"en-US": "Insufficient privileges.", "en-US": "Insufficient privileges.",
"zh-CN": "权限不足。", "zh-CN": "权限不足。",
"ja-JP": "権限エラー。",
"de-DE": "Unzureichende Berechtigungen.", "de-DE": "Unzureichende Berechtigungen.",
"ru-RU": "Недостаточно прав." "ru-RU": "Недостаточно прав."
}, },
"operation_failed" : { "operation_failed" : {
"en-US": "Operation Failed", "en-US": "Operation Failed",
"zh-CN": "操作失败", "zh-CN": "操作失败",
"ja-JP": "操作に失敗しました。",
"de-DE": "Befehl fehlgeschlagen", "de-DE": "Befehl fehlgeschlagen",
"ru-RU": "Ошибка при выполнении данной операции" "ru-RU": "Ошибка при выполнении данной операции"
}, },
"button_on" : { "button_on" : {
"en-US": "On", "en-US": "On",
"zh-CN": "打开", "zh-CN": "打开",
"ja-JP": "オン",
"de-DE": "An", "de-DE": "An",
"ru-RU": "Вкл." "ru-RU": "Вкл."
}, },
"button_off" : { "button_off" : {
"en-US": "Off", "en-US": "Off",
"zh-CN": "关闭", "zh-CN": "关闭",
"ja-JP": "オフ",
"de-DE": "Aus", "de-DE": "Aus",
"ru-RU": "Откл." "ru-RU": "Откл."
} }

View File

@ -76,6 +76,7 @@ language_en-US = English
language_zh-CN = 中文 language_zh-CN = 中文
language_de-DE = Deutsch language_de-DE = Deutsch
language_ru-RU = Русский language_ru-RU = Русский
language_ja-JP = 日本語
copyright = Copyright copyright = Copyright
all_rights_reserved = Все права защищены. all_rights_reserved = Все права защищены.
index_desc = Проект Harbor представляет собой надежный сервер управления docker-образами корпоративного класса. Компании могут использовать данный сервер в своей инфарструктуе для повышения производительности и безопасности . Проект Harbor может использоваться как в среде разработки так и в продуктивной среде. index_desc = Проект Harbor представляет собой надежный сервер управления docker-образами корпоративного класса. Компании могут использовать данный сервер в своей инфарструктуе для повышения производительности и безопасности . Проект Harbor может использоваться как в среде разработки так и в продуктивной среде.

View File

@ -76,6 +76,7 @@ language_en-US = English
language_zh-CN = 中文 language_zh-CN = 中文
language_de-DE = Deutsch language_de-DE = Deutsch
language_ru-RU = Русский language_ru-RU = Русский
language_ja-JP = 日本語
copyright = 版权所有 copyright = 版权所有
all_rights_reserved = 保留所有权利。 all_rights_reserved = 保留所有权利。
index_desc = Harbor是可靠的企业级Registry服务器。企业用户可使用Harbor搭建私有容器Registry服务提高生产效率和安全度既可应用于生产环境也可以在开发环境中使用。 index_desc = Harbor是可靠的企业级Registry服务器。企业用户可使用Harbor搭建私有容器Registry服务提高生产效率和安全度既可应用于生产环境也可以在开发环境中使用。

View File

@ -70,7 +70,8 @@ var SUPPORT_LANGUAGES = {
"en-US": "English", "en-US": "English",
"zh-CN": "Chinese", "zh-CN": "Chinese",
"de-DE": "German", "de-DE": "German",
"ru-RU": "Russian" "ru-RU": "Russian",
"ja-JP": "Japanese"
}; };
var DEFAULT_LANGUAGE = "en-US"; var DEFAULT_LANGUAGE = "en-US";

View File

@ -62,7 +62,7 @@ jQuery(function(){
return; return;
} }
$.each(data, function(i, e){ $.each(data, function(i, e){
var targetId = e.replace(/\//g, "------"); var targetId = e.replace(/\//g, "------").replace(/\./g, "---");
var row = '<div class="panel panel-default" targetId="' + targetId + '">' + var row = '<div class="panel panel-default" targetId="' + targetId + '">' +
'<div class="panel-heading" role="tab" id="heading' + i + '"+ >' + '<div class="panel-heading" role="tab" id="heading' + i + '"+ >' +
'<h4 class="panel-title">' + '<h4 class="panel-title">' +
@ -105,7 +105,7 @@ jQuery(function(){
$('#accordionRepo').on('show.bs.collapse', function (e) { $('#accordionRepo').on('show.bs.collapse', function (e) {
$('#accordionRepo .in').collapse('hide'); $('#accordionRepo .in').collapse('hide');
var targetId = $(e.target).attr("targetId"); var targetId = $(e.target).attr("targetId");
var repoName = targetId.replace(/------/g, "/"); var repoName = targetId.replace(/[-]{6}/g, "/").replace(/[-]{3}/g, '.');
new AjaxUtil({ new AjaxUtil({
url: "/api/repositories/tags?repo_name=" + repoName, url: "/api/repositories/tags?repo_name=" + repoName,
type: "get", type: "get",
@ -113,7 +113,7 @@ jQuery(function(){
$('#' + targetId +' table tbody tr').remove(); $('#' + targetId +' table tbody tr').remove();
var row = []; var row = [];
for(var i in data){ for(var i in data){
var tagName = data[i] var tagName = data[i];
row.push('<tr><td><a href="#" imageId="' + tagName + '" repoName="' + repoName + '">' + tagName + '</a></td><td><input type="text" style="width:100%" readonly value=" docker pull '+ $("#harborRegUrl").val() +'/'+ repoName + ':' + tagName +'"></td></tr>'); row.push('<tr><td><a href="#" imageId="' + tagName + '" repoName="' + repoName + '">' + tagName + '</a></td><td><input type="text" style="width:100%" readonly value=" docker pull '+ $("#harborRegUrl").val() +'/'+ repoName + ':' + tagName +'"></td></tr>');
} }
$('#' + targetId +' table tbody').append(row.join("")); $('#' + targetId +' table tbody').append(row.join(""));

View File

@ -38,6 +38,7 @@
<li><a href="/language?lang=zh-CN">{{i18n .Lang "language_zh-CN"}}</a></li> <li><a href="/language?lang=zh-CN">{{i18n .Lang "language_zh-CN"}}</a></li>
<li><a href="/language?lang=de-DE">{{i18n .Lang "language_de-DE"}}</a></li> <li><a href="/language?lang=de-DE">{{i18n .Lang "language_de-DE"}}</a></li>
<li><a href="/language?lang=ru-RU">{{i18n .Lang "language_ru-RU"}}</a></li> <li><a href="/language?lang=ru-RU">{{i18n .Lang "language_ru-RU"}}</a></li>
<li><a href="/language?lang=ja-JP">{{i18n .Lang "language_ja-JP"}}</a></li>
</ul> </ul>
</li> </li>
</ul> </ul>