Merge remote-tracking branch 'upstream/job-service' into job-service

This commit is contained in:
Tan Jiang 2016-05-03 15:57:00 +08:00
commit f0a2bc879a
36 changed files with 487 additions and 164 deletions

View File

@ -94,6 +94,7 @@ create table access_log (
user_id int NOT NULL, user_id int NOT NULL,
project_id int NOT NULL, project_id int NOT NULL,
repo_name varchar (40), repo_name varchar (40),
repo_tag varchar (20),
GUID varchar(64), GUID varchar(64),
operation varchar(20) NOT NULL, operation varchar(20) NOT NULL,
op_time timestamp, op_time timestamp,

View File

@ -7,14 +7,16 @@ services:
ports: ports:
- 1514:514 - 1514:514
registry: registry:
image: library/registry:2.3.0 image: library/registry:2.4.0
volumes: volumes:
- /data/registry:/storage - /data/registry:/storage
- ./config/registry/:/etc/registry/ - ./config/registry/:/etc/registry/
environment:
- GODEBUG=netdns=cgo
ports: ports:
- 5001:5001 - 5001:5001
command: command:
/etc/registry/config.yml ["serve", "/etc/registry/config.yml"]
depends_on: depends_on:
- log - log
logging: logging:

View File

@ -30,6 +30,6 @@ notifications:
- name: harbor - name: harbor
disabled: false disabled: false
url: http://ui/service/notifications url: http://ui/service/notifications
timeout: 500 timeout: 500ms
threshold: 5 threshold: 5
backoff: 1000 backoff: 1s

View File

@ -2,8 +2,8 @@ appname = registry
runmode = dev runmode = dev
[lang] [lang]
types = en-US|zh-CN types = en-US|zh-CN|de-DE
names = en-US|zh-CN names = en-US|zh-CN|de-DE
[dev] [dev]
httpport = 80 httpport = 80

View File

@ -12,3 +12,4 @@ LDAP_URL=$ldap_url
LDAP_BASE_DN=$ldap_basedn LDAP_BASE_DN=$ldap_basedn
SELF_REGISTRATION=$self_registration SELF_REGISTRATION=$self_registration
LOG_LEVEL=debug LOG_LEVEL=debug
GODEBUG=netdns=cgo

View File

@ -13,7 +13,7 @@ Project Harbor is an enterprise-class registry server. It extends the open sourc
* **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 AD/LDAP of the enterprise for user authentication and management. * **AD/LDAP support**: Harbor integrates with existing AD/LDAP of the enterprise for user authentication and management.
* **Auditing**: All the operations to the repositories are tracked and can be used for auditing purpose. * **Auditing**: All the operations to the repositories are tracked and can be used for auditing purpose.
* **Internationalization**: Localized for English and Chinese languages. More languages can be added. * **Internationalization**: Localized for English, Chinese and German languages. More languages can be added.
* **RESTful API**: RESTful APIs are provided for most administrative operations of Harbor. The integration with other management softwares becomes easy. * **RESTful API**: RESTful APIs are provided for most administrative operations of Harbor. The integration with other management softwares becomes easy.
### Getting Started ### Getting Started

View File

@ -86,3 +86,11 @@ func (b *BaseAPI) ValidateUser() int {
} }
return userID return userID
} }
// Redirect does redirection to resource URI with http header status code.
func (b *BaseAPI) Redirect(statusCode int, resouceID string) {
requestURI := b.Ctx.Request.RequestURI
resoucreURI := requestURI + "/" + resouceID
b.Ctx.Redirect(statusCode, resoucreURI)
}

View File

@ -87,11 +87,13 @@ func (p *ProjectAPI) Post() {
return return
} }
project := models.Project{OwnerID: p.userID, Name: projectName, CreationTime: time.Now(), Public: public} project := models.Project{OwnerID: p.userID, Name: projectName, CreationTime: time.Now(), Public: public}
err = dao.AddProject(project) projectID, err := dao.AddProject(project)
if err != nil { if err != nil {
log.Errorf("Failed to add project, error: %v", err) log.Errorf("Failed to add project, error: %v", err)
p.RenderError(http.StatusInternalServerError, "Failed to add project") p.RenderError(http.StatusInternalServerError, "Failed to add project")
} }
p.Redirect(http.StatusCreated, strconv.FormatInt(projectID, 10))
} }
// Head ... // Head ...
@ -183,6 +185,7 @@ func (p *ProjectAPI) FilterAccessLog() {
p.CustomAbort(http.StatusInternalServerError, "Internal error.") p.CustomAbort(http.StatusInternalServerError, "Internal error.")
} }
p.Data["json"] = accessLogList p.Data["json"] = accessLogList
p.ServeJSON() p.ServeJSON()
} }

View File

@ -204,25 +204,12 @@ type tag struct {
Tags []string `json:"tags"` Tags []string `json:"tags"`
} }
type histroyItem struct {
V1Compatibility string `json:"v1Compatibility"`
}
type manifest struct {
Name string `json:"name"`
Tag string `json:"tag"`
Architecture string `json:"architecture"`
SchemaVersion int `json:"schemaVersion"`
History []histroyItem `json:"history"`
}
// GetTags handles GET /api/repositories/tags // GetTags handles GET /api/repositories/tags
func (ra *RepositoryAPI) GetTags() { func (ra *RepositoryAPI) GetTags() {
var tags []string var tags []string
repoName := ra.GetString("repo_name") repoName := ra.GetString("repo_name")
tags, err := ra.registry.ListTag(repoName) tags, err := ra.registry.ListTag(repoName)
if err != nil { if err != nil {
e, ok := errors.ParseError(err) e, ok := errors.ParseError(err)
@ -257,8 +244,7 @@ func (ra *RepositoryAPI) GetManifests() {
ra.CustomAbort(http.StatusInternalServerError, "internal error") ra.CustomAbort(http.StatusInternalServerError, "internal error")
} }
} }
mani := models.Manifest{}
mani := manifest{}
err = json.Unmarshal(payload, &mani) err = json.Unmarshal(payload, &mani)
if err != nil { if err != nil {
log.Errorf("Failed to decode json from response for manifests, repo name: %s, tag: %s, error: %v", repoName, tag, err) log.Errorf("Failed to decode json from response for manifests, repo name: %s, tag: %s, error: %v", repoName, tag, err)

View File

@ -158,13 +158,14 @@ func (ua *UserAPI) Post() {
user := models.User{} user := models.User{}
ua.DecodeJSONReq(&user) ua.DecodeJSONReq(&user)
_, err := dao.Register(user) userID, err := dao.Register(user)
if err != nil { if err != nil {
log.Errorf("Error occurred in Register: %v", err) log.Errorf("Error occurred in Register: %v", err)
ua.RenderError(http.StatusInternalServerError, "Internal error.") ua.RenderError(http.StatusInternalServerError, "Internal error.")
return return
} }
ua.Redirect(http.StatusCreated, strconv.FormatInt(userID, 10))
} }
// Delete ... // Delete ...

View File

@ -76,31 +76,25 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
scope := openldap.LDAP_SCOPE_SUBTREE // LDAP_SCOPE_BASE, LDAP_SCOPE_ONELEVEL, LDAP_SCOPE_SUBTREE scope := openldap.LDAP_SCOPE_SUBTREE // LDAP_SCOPE_BASE, LDAP_SCOPE_ONELEVEL, LDAP_SCOPE_SUBTREE
filter := "objectClass=*" filter := "objectClass=*"
attributes := []string{"cn", "mail", "uid"} attributes := []string{"mail"}
result, err := ldap.SearchAll(baseDn, scope, filter, attributes) result, err := ldap.SearchAll(baseDn, scope, filter, attributes)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(result.Entries()) != 1 {
log.Warningf("Found more than one entry.")
return nil, nil
}
en := result.Entries()[0]
u := models.User{} u := models.User{}
if len(result.Entries()) == 1 {
en := result.Entries()[0]
for _, attr := range en.Attributes() { for _, attr := range en.Attributes() {
val := attr.Values()[0] val := attr.Values()[0]
switch attr.Name() { if attr.Name() == "mail" {
case "uid":
u.Username = val
case "mail":
u.Email = val u.Email = val
case "cn": }
u.Realname = val
} }
} }
log.Debug("username:", u.Username, ",email:", u.Email, ",realname:", u.Realname) u.Username = m.Principal
log.Debug("username:", u.Username, ",email:", u.Email)
exist, err := dao.UserExists(u, "username") exist, err := dao.UserExists(u, "username")
if err != nil { if err != nil {
@ -114,6 +108,7 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
} }
u.UserID = currentUser.UserID u.UserID = currentUser.UserID
} else { } else {
u.Realname = m.Principal
u.Password = "12345678AbC" u.Password = "12345678AbC"
u.Comment = "registered from LDAP." u.Comment = "registered from LDAP."
userID, err := dao.Register(u) userID, err := dao.Register(u)

View File

@ -0,0 +1,68 @@
version: '2'
services:
log:
image: daocloud.io/harbor/deploy_log:latest
volumes:
- /var/log/harbor/:/var/log/docker/
ports:
- 1514:514
registry:
image: daocloud.io/library/registry:2.3.0
volumes:
- /data/registry:/storage
- ./config/registry/:/etc/registry/
ports:
- 5001:5001
command:
/etc/registry/config.yml
depends_on:
- log
logging:
driver: "syslog"
options:
syslog-address: "tcp://127.0.0.1:1514"
syslog-tag: "registry"
mysql:
image: daocloud.io/harbor/deploy_mysql:latest
volumes:
- /data/database:/var/lib/mysql
env_file:
- ./config/db/env
depends_on:
- log
logging:
driver: "syslog"
options:
syslog-address: "tcp://127.0.0.1:1514"
syslog-tag: "mysql"
ui:
image: daocloud.io/harbor/deploy_ui:latest
env_file:
- ./config/ui/env
volumes:
- ./config/ui/app.conf:/etc/ui/app.conf
- ./config/ui/private_key.pem:/etc/ui/private_key.pem
depends_on:
- log
logging:
driver: "syslog"
options:
syslog-address: "tcp://127.0.0.1:1514"
syslog-tag: "ui"
proxy:
image: daocloud.io/library/nginx:1.9
volumes:
- ./config/nginx:/etc/nginx
ports:
- 80:80
- 443:443
depends_on:
- mysql
- registry
- ui
- log
logging:
driver: "syslog"
options:
syslog-address: "tcp://127.0.0.1:1514"
syslog-tag: "proxy"

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" { if lang == "en-US" || lang == "zh-CN" || lang == "de-DE" {
c.SetSession("lang", lang) c.SetSession("lang", lang)
c.Data["Lang"] = lang c.Data["Lang"] = lang
} }

View File

@ -19,6 +19,7 @@ import (
"strings" "strings"
"github.com/vmware/harbor/models" "github.com/vmware/harbor/models"
"github.com/vmware/harbor/utils/log"
"github.com/astaxie/beego/orm" "github.com/astaxie/beego/orm"
) )
@ -27,14 +28,14 @@ import (
func AddAccessLog(accessLog models.AccessLog) error { func AddAccessLog(accessLog models.AccessLog) error {
o := orm.NewOrm() o := orm.NewOrm()
p, err := o.Raw(`insert into access_log p, err := o.Raw(`insert into access_log
(user_id, project_id, repo_name, guid, operation, op_time) (user_id, project_id, repo_name, repo_tag, guid, operation, op_time)
values (?, ?, ?, ?, ?, now())`).Prepare() values (?, ?, ?, ?, ?, ?, now())`).Prepare()
if err != nil { if err != nil {
return err return err
} }
defer p.Close() defer p.Close()
_, err = p.Exec(accessLog.UserID, accessLog.ProjectID, accessLog.RepoName, accessLog.GUID, accessLog.Operation) _, err = p.Exec(accessLog.UserID, accessLog.ProjectID, accessLog.RepoName, accessLog.RepoTag, accessLog.GUID, accessLog.Operation)
return err return err
} }
@ -43,7 +44,7 @@ func AddAccessLog(accessLog models.AccessLog) error {
func GetAccessLogs(accessLog models.AccessLog) ([]models.AccessLog, error) { func GetAccessLogs(accessLog models.AccessLog) ([]models.AccessLog, error) {
o := orm.NewOrm() o := orm.NewOrm()
sql := `select a.log_id, u.username, a.repo_name, a.operation, a.op_time sql := `select a.log_id, u.username, a.repo_name, a.repo_tag, a.operation, a.op_time
from access_log a left join user u on a.user_id = u.user_id from access_log a left join user u on a.user_id = u.user_id
where a.project_id = ? ` where a.project_id = ? `
queryParam := make([]interface{}, 1) queryParam := make([]interface{}, 1)
@ -96,12 +97,15 @@ func GetAccessLogs(accessLog models.AccessLog) ([]models.AccessLog, error) {
} }
// AccessLog ... // AccessLog ...
func AccessLog(username, projectName, repoName, action string) error { func AccessLog(username, projectName, repoName, repoTag, action string) error {
o := orm.NewOrm() o := orm.NewOrm()
sql := "insert into access_log (user_id, project_id, repo_name, operation, op_time) " + sql := "insert into access_log (user_id, project_id, repo_name, repo_tag, operation, op_time) " +
"select (select user_id as user_id from user where username=?), " + "select (select user_id as user_id from user where username=?), " +
"(select project_id as project_id from project where name=?), ?, ?, now() " "(select project_id as project_id from project where name=?), ?, ?, ?, now() "
_, err := o.Raw(sql, username, projectName, repoName, action).Exec() _, err := o.Raw(sql, username, projectName, repoName, repoTag, action).Exec()
if err != nil {
log.Errorf("error in AccessLog: %v ", err)
}
return err return err
} }

View File

@ -372,7 +372,7 @@ func TestAddProject(t *testing.T) {
OwnerName: currentUser.Username, OwnerName: currentUser.Username,
} }
err := AddProject(project) _, err := AddProject(project)
if err != nil { if err != nil {
t.Errorf("Error occurred in AddProject: %v", err) t.Errorf("Error occurred in AddProject: %v", err)
} }

View File

@ -29,41 +29,41 @@ import (
//TODO:transaction, return err //TODO:transaction, return err
// AddProject adds a project to the database along with project roles information and access log records. // AddProject adds a project to the database along with project roles information and access log records.
func AddProject(project models.Project) error { func AddProject(project models.Project) (int64, error) {
if isIllegalLength(project.Name, 4, 30) { if isIllegalLength(project.Name, 4, 30) {
return errors.New("project name is illegal in length. (greater than 4 or less than 30)") return 0, errors.New("project name is illegal in length. (greater than 4 or less than 30)")
} }
if isContainIllegalChar(project.Name, []string{"~", "-", "$", "\\", "[", "]", "{", "}", "(", ")", "&", "^", "%", "*", "<", ">", "\"", "'", "/", "?", "@"}) { if isContainIllegalChar(project.Name, []string{"~", "-", "$", "\\", "[", "]", "{", "}", "(", ")", "&", "^", "%", "*", "<", ">", "\"", "'", "/", "?", "@"}) {
return errors.New("project name contains illegal characters") return 0, errors.New("project name contains illegal characters")
} }
o := orm.NewOrm() o := orm.NewOrm()
p, err := o.Raw("insert into project (owner_id, name, creation_time, update_time, deleted, public) values (?, ?, ?, ?, ?, ?)").Prepare() p, err := o.Raw("insert into project (owner_id, name, creation_time, update_time, deleted, public) values (?, ?, ?, ?, ?, ?)").Prepare()
if err != nil { if err != nil {
return err return 0, err
} }
now := time.Now() now := time.Now()
r, err := p.Exec(project.OwnerID, project.Name, now, now, project.Deleted, project.Public) r, err := p.Exec(project.OwnerID, project.Name, now, now, project.Deleted, project.Public)
if err != nil { if err != nil {
return err return 0, err
} }
projectID, err := r.LastInsertId() projectID, err := r.LastInsertId()
if err != nil { if err != nil {
return err return 0, err
} }
if err = AddProjectMember(projectID, project.OwnerID, models.PROJECTADMIN); err != nil { if err = AddProjectMember(projectID, project.OwnerID, models.PROJECTADMIN); err != nil {
return err return projectID, err
} }
accessLog := models.AccessLog{UserID: project.OwnerID, ProjectID: projectID, RepoName: project.Name + "/", GUID: "N/A", Operation: "create", OpTime: time.Now()} accessLog := models.AccessLog{UserID: project.OwnerID, ProjectID: projectID, RepoName: project.Name + "/", RepoTag: "N/A", GUID: "N/A", Operation: "create", OpTime: time.Now()}
err = AddAccessLog(accessLog) err = AddAccessLog(accessLog)
return err return projectID, err
} }
// IsProjectPublic ... // IsProjectPublic ...

View File

@ -74,17 +74,14 @@ func validate(user models.User) error {
return errors.New("Username already exists.") return errors.New("Username already exists.")
} }
if len(user.Email) > 0 {
if m, _ := regexp.MatchString(`^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, user.Email); !m { if m, _ := regexp.MatchString(`^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, user.Email); !m {
return errors.New("Email with illegal format.") return errors.New("Email with illegal format.")
} }
if isIllegalLength(user.Email, 0, -1) {
return errors.New("Email cannot empty.")
}
if exist, _ := UserExists(models.User{Email: user.Email}, "email"); exist { if exist, _ := UserExists(models.User{Email: user.Email}, "email"); exist {
return errors.New("Email already exists.") return errors.New("Email already exists.")
} }
}
if isIllegalLength(user.Realname, 0, 20) { if isIllegalLength(user.Realname, 0, 20) {
return errors.New("Realname with illegal length.") return errors.New("Realname with illegal length.")

View File

@ -22,6 +22,10 @@ From time to time, you may need to mannually test Harbor REST API. You can deplo
```sh ```sh
vi prepare-swagger.sh vi prepare-swagger.sh
``` ```
* Change the SCHEME to the protocol scheme of your Harbor server.
```sh
SCHEME=<HARBOR_SERVER_SCHEME>
```
* Change the SERVER_IP to the IP address of your Harbor server. * Change the SERVER_IP to the IP address of your Harbor server.
```sh ```sh
SERVER_ID=<HARBOR_SERVER_DOMAIN> SERVER_ID=<HARBOR_SERVER_DOMAIN>

View File

@ -0,0 +1,60 @@
#Customize Harbor token service with your key and certificate
Harbor requires Docker client to access the Harbor registry with a token. The procedure to generate a token is like [Docker Registry v2 authentication](https://github.com/docker/distribution/blob/master/docs/spec/auth/token.md). Firstly, you should make a request to the token service for a token. The token is signed by the private key. After that, you make a new request with the token to the Harbor registry, Harbor registry will verify the token with the public key in the rootcert bundle. Then Harbor registry will authorize the Docker client to push/pull images.
By default, Harbor uses default private key and certificate in authentication. Also, you can customize your configuration with your own key and certificate with the following steps:
1.If you already have a certificate, go to step 3.
2.If not, you can generate a root certificate using openSSL with following commands:
**1)Generate a private key:**
```sh
$ openssl genrsa -out private_key.pem 4096
```
**2)Generate a certificate:**
```sh
$ openssl req -new -x509 -key private_key.pem -out root.crt -days 3650
```
You are about to be asked to enter information that will be incorporated into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank. Following are what you're asked to enter.
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (eg, server FQDN or YOUR name) []:
Email Address []:
After you execute these two commands, you will see private_key.pem and root.crt in the **current directory**, just type "ls", you'll see them.
3.Refer to [Installation Guide](https://github.com/vmware/harbor/blob/master/docs/installation_guide.md) to install Harbor, After you execute ./prepare, Harbor generates several config files. We need to replace the original private key and certificate with your own key and certificate.
4.Replace the default key and certificate. Assume that you key and certificate are in the directory /root/cert, following are what you should do:
```
$ cd config/ui
$ cp /root/cert/private_key.pem private_key.pem
$ cp /root/cert/root.crt ../registry/root.crt
```
5.After these, go back to the Deploy directory, you can start Harbor using following command:
```
$ docker-compose up -d
```
6.Then you can push/pull images to see if your own certificate works. Please refer [User Guide](https://github.com/vmware/harbor/blob/master/docs/user_guide.md) for more info.

View File

@ -0,0 +1,11 @@
### A faster way to pull images for Chinese Harbor users
By default, Harbor not only build images according to Dockerfile but also pull images from Docker Hub. For the reason we all know, it is difficult for Chinese Harbor users to pull images from the Docker Hub. We put images on daocloud.io platform, we'll put images on other platforms later. If you have difficulty to pull images from Docker Hub, or you think it wastes too much time to build images. We recommend you to use the following way to accelerate the pulling procedure(make sure you're in the harbor diectory):
```
$ cd contrib
$ cp docker-compose.yml.daocloud ../Deploy
$ cd ../Deploy
$ mv docker-compose.yml docker-compose.yml.bak
$ mv docker-compose.yml.daocloud docker-compose.yml
$ docker-compose up -d
```
Then you'll see docker pulling imges faster than before.

View File

@ -1,5 +1,6 @@
#!/bin/bash #!/bin/bash
SERVER_IP=10.117.170.65 SCHEME=http
SERVER_IP=reg.mydomain.com
set -e set -e
echo "Doing some clean up..." echo "Doing some clean up..."
rm -f *.tar.gz rm -f *.tar.gz
@ -8,9 +9,10 @@ wget https://github.com/swagger-api/swagger-ui/archive/v2.1.4.tar.gz -O swagger.
echo "Untarring Swagger UI package to the static file path..." echo "Untarring Swagger UI package to the static file path..."
tar -C ../static/vendors -zxf swagger.tar.gz swagger-ui-2.1.4/dist tar -C ../static/vendors -zxf swagger.tar.gz swagger-ui-2.1.4/dist
echo "Executing some processes..." echo "Executing some processes..."
sed -i 's/http:\/\/petstore\.swagger\.io\/v2\/swagger\.json/http:\/\/'$SERVER_IP'\/static\/resources\/yaml\/swagger\.yaml/g' \ sed -i 's/http:\/\/petstore\.swagger\.io\/v2\/swagger\.json/'$SCHEME':\/\/'$SERVER_IP'\/static\/resources\/yaml\/swagger\.yaml/g' \
../static/vendors/swagger-ui-2.1.4/dist/index.html ../static/vendors/swagger-ui-2.1.4/dist/index.html
mkdir -p ../static/resources/yaml mkdir -p ../static/resources/yaml
cp swagger.yaml ../static/resources/yaml cp swagger.yaml ../static/resources/yaml
sed -i 's/host: localhost/host: '$SERVER_IP'/g' ../static/resources/yaml/swagger.yaml sed -i 's/host: localhost/host: '$SERVER_IP'/g' ../static/resources/yaml/swagger.yaml
sed -i 's/ \- http$/ \- '$SCHEME'/g' ../static/resources/yaml/swagger.yaml
echo "Finish preparation for the Swagger UI." echo "Finish preparation for the Swagger UI."

View File

@ -25,6 +25,7 @@ type AccessLog struct {
UserID int `orm:"column(user_id)" json:"UserId"` UserID int `orm:"column(user_id)" json:"UserId"`
ProjectID int64 `orm:"column(project_id)" json:"ProjectId"` ProjectID int64 `orm:"column(project_id)" json:"ProjectId"`
RepoName string `orm:"column(repo_name)"` RepoName string `orm:"column(repo_name)"`
RepoTag string `orm:"column(repo_tag)"`
GUID string `orm:"column(GUID)" json:"Guid"` GUID string `orm:"column(GUID)" json:"Guid"`
Operation string `orm:"column(operation)"` Operation string `orm:"column(operation)"`
OpTime time.Time `orm:"column(op_time)"` OpTime time.Time `orm:"column(op_time)"`

View File

@ -40,6 +40,7 @@ type Target struct {
Digest string Digest string
Repository string Repository string
URL string `json:"Url"` URL string `json:"Url"`
Tag string
} }
// Actor holds information about actor. // Actor holds information about actor.

View File

@ -42,3 +42,21 @@ type Tag struct {
Version string `json:"version"` Version string `json:"version"`
ImageID string `json:"image_id"` ImageID string `json:"image_id"`
} }
// Manifest ...
type Manifest struct {
SchemaVersion int `json:"schemaVersion"`
Name string `json:"name"`
Tag string `json:"tag"`
Architecture string `json:"architecture"`
FsLayers []blobSumItem `json:"fsLayers"`
History []histroyItem `json:"history"`
}
type histroyItem struct {
V1Compatibility string `json:"v1Compatibility"`
}
type blobSumItem struct {
BlobSum string `json:"blobSum"`
}

View File

@ -46,7 +46,7 @@ func (n *NotificationHandler) Post() {
log.Errorf("error while decoding json: %v", err) log.Errorf("error while decoding json: %v", err)
return return
} }
var username, action, repo, project string var username, action, repo, project, repoTag string
var matched bool var matched bool
for _, e := range notification.Events { for _, e := range notification.Events {
matched, err = regexp.MatchString(manifestPattern, e.Target.MediaType) matched, err = regexp.MatchString(manifestPattern, e.Target.MediaType)
@ -58,13 +58,16 @@ func (n *NotificationHandler) Post() {
username = e.Actor.Name username = e.Actor.Name
action = e.Action action = e.Action
repo = e.Target.Repository repo = e.Target.Repository
repoTag = e.Target.Tag
log.Debugf("repo tag is : %v ", repoTag)
if strings.Contains(repo, "/") { if strings.Contains(repo, "/") {
project = repo[0:strings.LastIndex(repo, "/")] project = repo[0:strings.LastIndex(repo, "/")]
} }
if username == "" { if username == "" {
username = "anonymous" username = "anonymous"
} }
go dao.AccessLog(username, project, repo, action) go dao.AccessLog(username, project, repo, repoTag, action)
if action == "push" { if action == "push" {
go func() { go func() {
err2 := svc_utils.RefreshCatalogCache() err2 := svc_utils.RefreshCatalogCache()

View File

@ -0,0 +1,86 @@
page_title_index = Harbor
page_title_sign_in = Anmelden - Harbor
page_title_project = Projekt - Harbor
page_title_item_details = Details - Harbor
page_title_registration = Registrieren - Harbor
page_title_add_user = Benutzer anlegen - Harbor
page_title_forgot_password = Passwort vergessen - Harbor
title_forgot_password = Passwort vergessen
page_title_reset_password = Passwort zurücksetzen - Harbor
title_reset_password = Passwort zurücksetzen
page_title_change_password = Passwort ändern - Harbor
title_change_password = Passwort ändern
page_title_search = Suche - Harbor
sign_in = Anmelden
sign_up = Registrieren
add_user = Benutzer anlegen
log_out = Abmelden
search_placeholder = Projekte oder Repositories
change_password = Passwort ändern
username_email = Benutzername/E-Mail
password = Passwort
forgot_password = Passwort vergessen
welcome = Willkommen
my_projects = Meine Projekte
public_projects = Öffentliche Projekte
admin_options = Admin Optionen
project_name = Projektname
creation_time = Erstellungsdatum
publicity = Öffentlich
add_project = Projekt hinzufügen
check_for_publicity = öffentliches Projekt
button_save = Speichern
button_cancel = Abbrechen
button_submit = Absenden
username = Benutzername
email = E-Mail
system_admin = System Admininistrator
dlg_button_ok = OK
dlg_button_cancel = Abbrechen
registration = Registrieren
username_description = Dies wird Ihr Benutzername sein.
email_description = Die E-Mail Adresse wird für das Zurücksetzen des Passworts genutzt.
full_name = Sollständiger Name
full_name_description = Vor- und Nachname.
password_description = Mindestens sieben Zeichen bestehend aus einem Kleinbuchstaben, einem Großbuchstaben und einer Zahl
confirm_password = Passwort bestätigen
note_to_the_admin = Kommentar
old_password = Altes Passwort
new_password = Neues Passwort
forgot_password_description = Bitte gebe die E-Mail Adresse ein, die du zur Registrierung verwendet hast. Ein Link zur Wiederherstellung wird dir per E-Mail an diese Adresse geschickt.
projects = Projekte
repositories = Repositories
search = Suche
home = Home
project = Projekt
owner = Besitzer
repo = Repositories
user = Benutzer
logs = Logs
repo_name = Repository
add_members = Benutzer hinzufügen
operation = Aktion
advance = erweiterte Suche
all = Alle
others = Andere
start_date = Start Datum
end_date = End Datum
timestamp = Zeitstempel
role = Rolle
reset_email_hint = Bitte klicke auf diesen Link um dein Passwort zurückzusetzen
reset_email_subject = Passwort zurücksetzen
language = Deutsch
language_en-US = English
language_zh-CN = 中文
language_de-DE = Deutsch
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.
index_desc_0 = Vorteile:
index_desc_1 = 1. Sicherheit: Halten Sie ihr geistiges Eigentum innerhalb der Organisation.
index_desc_2 = 2. Effizienz: Ein privater Registry Server innerhalb des Netzwerks ihrer Organisation kann den Traffic zu öffentlichen Services im Internet signifikant reduzieren.
index_desc_3 = 3. Zugriffskontrolle: RBAC (Role Based Access Control) wird zur Verfügung gestellt. Benutzerverwaltung kann mit bestehenden Identitätsservices wie AD/LDAP integriert werden.
index_desc_4 = 4. Audit: Jeglicher Zugriff auf die Registry wird protokolliert und kann für ein Audit verwendet werden.
index_desc_5 = 5. GUI: Benutzerfreundliche Verwaltung über eine einzige Management-Konsole
index_title = Ein Enterprise-Class Registry Server

View File

@ -59,6 +59,7 @@ repo = Repositories
user = Users user = Users
logs = Logs logs = Logs
repo_name = Repository Name repo_name = Repository Name
repo_tag = Tag
add_members = Add Members add_members = Add Members
operation = Operation operation = Operation
advance = Advanced Search advance = Advanced Search
@ -73,6 +74,7 @@ reset_email_subject = Reset your password
language = English language = English
language_en-US = English language_en-US = English
language_zh-CN = 中文 language_zh-CN = 中文
language_de-DE = Deutsch
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

@ -15,254 +15,317 @@
var global_messages = { var global_messages = {
"username_is_required" : { "username_is_required" : {
"en-US": "Username is required.", "en-US": "Username is required.",
"zh-CN": "用户名为必填项。" "zh-CN": "用户名为必填项。",
"de-DE": "Benutzername erforderlich."
}, },
"username_has_been_taken" : { "username_has_been_taken" : {
"en-US": "Username has been taken.", "en-US": "Username has been taken.",
"zh-CN": "用户名已被占用。" "zh-CN": "用户名已被占用。",
"de-DE": "Benutzername bereits vergeben."
}, },
"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个字符",
"de-DE": "Benutzername ist zu lang. (maximal 20 Zeichen)"
}, },
"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": "用户名包含不合法的字符。",
"de-DE": "Benutzername enthält ungültige Zeichen."
}, },
"email_is_required" : { "email_is_required" : {
"en-US": "Email is required.", "en-US": "Email is required.",
"zh-CN": "邮箱为必填项。" "zh-CN": "邮箱为必填项。",
"de-DE": "E-Mail Adresse erforderlich."
}, },
"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": "邮箱包含不合法的字符。",
"de-DE": "E-Mail Adresse enthält ungültige Zeichen."
}, },
"email_has_been_taken" : { "email_has_been_taken" : {
"en-US": "Email has been taken.", "en-US": "Email has been taken.",
"zh-CN": "邮箱已被占用。" "zh-CN": "邮箱已被占用。",
"de-DE": "E-Mail Adresse wird bereits verwendet."
}, },
"email_content_illegal" : { "email_content_illegal" : {
"en-US": "Email format is illegal.", "en-US": "Email format is illegal.",
"zh-CN": "邮箱格式不合法。" "zh-CN": "邮箱格式不合法。",
"de-DE": "Format der E-Mail Adresse ist ungültig."
}, },
"email_does_not_exist" : { "email_does_not_exist" : {
"en-US": "Email does not exist.", "en-US": "Email does not exist.",
"zh-CN": "邮箱不存在。" "zh-CN": "邮箱不存在。",
"de-DE": "E-Mail Adresse existiert nicht."
}, },
"realname_is_required" : { "realname_is_required" : {
"en-US": "Full name is required.", "en-US": "Full name is required.",
"zh-CN": "全名为必填项。" "zh-CN": "全名为必填项。",
"de-DE": "Vollständiger Name erforderlich."
}, },
"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个字符",
"de-DE": "Vollständiger Name zu lang. (maximal 20 Zeichen)"
}, },
"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": "全名包含不合法的字符。",
"de-DE": "Vollständiger Name enthält ungültige Zeichen."
}, },
"password_is_required" : { "password_is_required" : {
"en-US": "Password is required.", "en-US": "Password is required.",
"zh-CN": "密码为必填项。" "zh-CN": "密码为必填项。",
"de-DE": "Passwort erforderlich."
}, },
"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个数字。",
"de-DE": "Passwort ungültig. Mindestens sieben Zeichen bestehend aus einem Kleinbuchstaben, einem Großbuchstaben und einer Zahl"
}, },
"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个字符",
"de-DE": "Passwort zu lang. (maximal 20 Zeichen)"
}, },
"password_does_not_match" : { "password_does_not_match" : {
"en-US": "Passwords do not match.", "en-US": "Passwords do not match.",
"zh-CN": "两次密码输入不一致。" "zh-CN": "两次密码输入不一致。",
"de-DE": "Passwörter stimmen nicht überein."
}, },
"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个字符",
"de-DE": "Kommentar zu lang. (maximal 20 Zeichen)"
}, },
"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": "备注包含不合法的字符。",
"de-DE": "Kommentar enthält ungültige Zeichen."
}, },
"project_name_is_required" : { "project_name_is_required" : {
"en-US": "Project name is required.", "en-US": "Project name is required.",
"zh-CN": "项目名称为必填项。" "zh-CN": "项目名称为必填项。",
"de-DE": "Projektname erforderlich."
}, },
"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个字符。",
"de-DE": "Projektname zu kurz. (mindestens 4 Zeichen)"
}, },
"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个字符",
"de-DE": "Projektname zu lang. (maximal 30 Zeichen)"
}, },
"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": "项目名称包含不合法的字符。",
"de-DE": "Projektname enthält ungültige Zeichen."
}, },
"project_exists" : { "project_exists" : {
"en-US": "Project exists.", "en-US": "Project exists.",
"zh-CN": "项目已存在。" "zh-CN": "项目已存在。",
"de-DE": "Projekt existiert bereits."
}, },
"delete_user" : { "delete_user" : {
"en-US": "Delete User", "en-US": "Delete User",
"zh-CN": "删除用户" "zh-CN": "删除用户",
"de-DE": "Benutzer löschen"
}, },
"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": "确认要删除用户 ",
"de-DE": "Sind Sie sich sicher, dass Sie folgenden Benutzer löschen möchten: "
}, },
"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": "请输入用户名和密码。",
"de-DE": "Bitte geben Sie ihr Benutzername und Passwort ein."
}, },
"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": "请输入正确的用户名或密码。",
"de-DE": "Bitte überprüfen Sie ihren Benutzernamen und Passwort."
}, },
"title_login_failed" : { "title_login_failed" : {
"en-US": "Login Failed", "en-US": "Login Failed",
"zh-CN": "登录失败" "zh-CN": "登录失败",
"de-DE": "Anmeldung fehlgeschlagen"
}, },
"title_change_password" : { "title_change_password" : {
"en-US": "Change Password", "en-US": "Change Password",
"zh-CN": "修改密码" "zh-CN": "修改密码",
"de-DE": "Passwort ändern"
}, },
"change_password_successfully" : { "change_password_successfully" : {
"en-US": "Password changed successfully.", "en-US": "Password changed successfully.",
"zh-CN": "密码已修改。" "zh-CN": "密码已修改。",
"de-DE": "Passwort erfolgreich geändert."
}, },
"title_forgot_password" : { "title_forgot_password" : {
"en-US": "Forgot Password", "en-US": "Forgot Password",
"zh-CN": "忘记密码" "zh-CN": "忘记密码",
"de-DE": "Passwort vergessen"
}, },
"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": "重置密码邮件已发送。",
"de-DE": "Eine E-Mail mit einem Wiederherstellungslink wurde an Sie gesendet."
}, },
"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": "重置密码邮件发送失败。",
"de-DE": "Fehler beim Senden der Wiederherstellungs-E-Mail."
}, },
"please_login_first" : { "please_login_first" : {
"en-US": "Please login first.", "en-US": "Please login first.",
"zh-CN": "请先登录。" "zh-CN": "请先登录。",
"de-DE": "Bitte melden Sie sich zuerst an."
}, },
"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": "原密码输入不正确。",
"de-DE": "Altes Passwort ist nicht korrekt."
}, },
"please_input_new_password" : { "please_input_new_password" : {
"en-US": "Please input new password.", "en-US": "Please input new password.",
"zh-CN": "请输入新密码。" "zh-CN": "请输入新密码。",
"de-DE": "Bitte geben Sie ihr neues Passwort ein."
}, },
"invalid_reset_url": { "invalid_reset_url": {
"en-US": "Invalid URL for resetting password.", "en-US": "Invalid URL for resetting password.",
"zh-CN": "无效密码重置链接。" "zh-CN": "无效密码重置链接。",
"de-DE": "Ungültige URL zum Passwort wiederherstellen."
}, },
"reset_password_successfully" : { "reset_password_successfully" : {
"en-US": "Reset password successfully.", "en-US": "Reset password successfully.",
"zh-CN": "密码重置成功。" "zh-CN": "密码重置成功。",
"de-DE": "Passwort erfolgreich wiederhergestellt."
}, },
"internal_error": { "internal_error": {
"en-US": "Internal error.", "en-US": "Internal error.",
"zh-CN": "内部错误,请联系系统管理员。" "zh-CN": "内部错误,请联系系统管理员。",
"de-DE": "Interner Fehler."
}, },
"title_reset_password" : { "title_reset_password" : {
"en-US": "Reset Password", "en-US": "Reset Password",
"zh-CN": "重置密码" "zh-CN": "重置密码",
"de-DE": "Passwort zurücksetzen"
}, },
"title_sign_up" : { "title_sign_up" : {
"en-US": "Sign Up", "en-US": "Sign Up",
"zh-CN": "注册" "zh-CN": "注册",
"de-DE": "Registrieren"
}, },
"title_add_user": { "title_add_user": {
"en-US": "Add User", "en-US": "Add User",
"zh-CN": "新增用户" "zh-CN": "新增用户",
"de-DE": "Benutzer hinzufügen"
}, },
"registered_successfully": { "registered_successfully": {
"en-US": "Signed up successfully.", "en-US": "Signed up successfully.",
"zh-CN": "注册成功。" "zh-CN": "注册成功。",
"de-DE": "Erfolgreich registriert."
}, },
"registered_failed" : { "registered_failed" : {
"en-US": "Failed to sign up.", "en-US": "Failed to sign up.",
"zh-CN": "注册失败。" "zh-CN": "注册失败。",
"de-DE": "Registrierung fehlgeschlagen."
}, },
"added_user_successfully": { "added_user_successfully": {
"en-US": "Added user successfully.", "en-US": "Added user successfully.",
"zh-CN": "新增用户成功。" "zh-CN": "新增用户成功。",
"de-DE": "Benutzer erfolgreich erstellt."
}, },
"added_user_failed": { "added_user_failed": {
"en-US": "Added user failed.", "en-US": "Adding user failed.",
"zh-CN": "新增用户失败。" "zh-CN": "新增用户失败。",
"de-DE": "Benutzer erstellen fehlgeschlagen."
}, },
"projects": { "projects": {
"en-US": "Projects", "en-US": "Projects",
"zh-CN": "项目" "zh-CN": "项目",
"de-DE": "Projekte"
}, },
"repositories" : { "repositories" : {
"en-US": "Repositories", "en-US": "Repositories",
"zh-CN": "镜像仓库" "zh-CN": "镜像仓库",
"de-DE": "Repositories"
}, },
"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命令上传镜像。",
"de-DE": "Keine Repositories gefunden, bitte benutzen Sie 'docker push' um ein Image hochzuladen."
}, },
"tag" : { "tag" : {
"en-US": "Tag", "en-US": "Tag",
"zh-CN": "标签" "zh-CN": "标签",
"de-DE": "Tag"
}, },
"pull_command": { "pull_command": {
"en-US": "Pull Command", "en-US": "Pull Command",
"zh-CN": "Pull 命令" "zh-CN": "Pull 命令",
"de-DE": "Pull Befehl"
}, },
"image_details" : { "image_details" : {
"en-US": "Image Details", "en-US": "Image Details",
"zh-CN": "镜像详细信息" "zh-CN": "镜像详细信息",
"de-DE": "Image Details"
}, },
"add_members" : { "add_members" : {
"en-US": "Add Member", "en-US": "Add Member",
"zh-CN": "添加成员" "zh-CN": "添加成员",
"de-DE": "Mitglied hinzufügen"
}, },
"edit_members" : { "edit_members" : {
"en-US": "Edit Member", "en-US": "Edit Members",
"zh-CN": "编辑成员" "zh-CN": "编辑成员",
"de-DE": "Mitglieder bearbeiten"
}, },
"add_member_failed" : { "add_member_failed" : {
"en-US": "Adding Member Failed", "en-US": "Adding Member Failed",
"zh-CN": "添加成员失败" "zh-CN": "添加成员失败",
"de-DE": "Mitglied hinzufügen fehlgeschlagen"
}, },
"please_input_username" : { "please_input_username" : {
"en-US": "Please input a username.", "en-US": "Please input a username.",
"zh-CN": "请输入用户名。" "zh-CN": "请输入用户名。",
"de-DE": "Bitte geben Sie einen Benutzernamen ein."
}, },
"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": "请为用户分配角色。",
"de-DE": "Bitte weisen Sie dem Benutzer eine Rolle zu."
}, },
"user_id_exists" : { "user_id_exists" : {
"en-US": "User is already a member.", "en-US": "User is already a member.",
"zh-CN": "用户已经是成员。" "zh-CN": "用户已经是成员。",
"de-DE": "Benutzer ist bereits Mitglied."
}, },
"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": "不存在此用户。",
"de-DE": "Benutzer existiert nicht."
}, },
"insufficient_privileges" : { "insufficient_privileges" : {
"en-US": "Insufficient privileges.", "en-US": "Insufficient privileges.",
"zh-CN": "权限不足。" "zh-CN": "权限不足。",
"de-DE": "Unzureichende Berechtigungen."
}, },
"operation_failed" : { "operation_failed" : {
"en-US": "Operation Failed", "en-US": "Operation Failed",
"zh-CN": "操作失败" "zh-CN": "操作失败",
"de-DE": "Befehl fehlgeschlagen"
}, },
"button_on" : { "button_on" : {
"en-US": "On", "en-US": "On",
"zh-CN": "打开" "zh-CN": "打开",
"de-DE": "An"
}, },
"button_off" : { "button_off" : {
"en-US": "Off", "en-US": "Off",
"zh-CN": "关闭" "zh-CN": "关闭",
"de-DE": "Aus"
} }
}; };

View File

@ -59,6 +59,7 @@ repo = 镜像仓库
user = 用户 user = 用户
logs = 日志 logs = 日志
repo_name = 镜像名称 repo_name = 镜像名称
repo_tag = 镜像标签
add_members = 添加成员 add_members = 添加成员
operation = 操作 operation = 操作
advance = 高级检索 advance = 高级检索
@ -73,6 +74,7 @@ reset_email_subject = 重置您的密码
language = 中文 language = 中文
language_en-US = English language_en-US = English
language_zh-CN = 中文 language_zh-CN = 中文
language_de-DE = Deutsch
copyright = 版权所有 copyright = 版权所有
all_rights_reserved = 保留所有权利。 all_rights_reserved = 保留所有权利。
index_desc = Harbor是可靠的企业级Registry服务器。企业用户可使用Harbor搭建私有容器Registry服务提高生产效率和安全度既可应用于生产环境也可以在开发环境中使用。 index_desc = Harbor是可靠的企业级Registry服务器。企业用户可使用Harbor搭建私有容器Registry服务提高生产效率和安全度既可应用于生产环境也可以在开发环境中使用。

View File

@ -68,7 +68,8 @@ AjaxUtil.prototype.exec = function(){
var SUPPORT_LANGUAGES = { var SUPPORT_LANGUAGES = {
"en-US": "English", "en-US": "English",
"zh-CN": "Chinese" "zh-CN": "Chinese",
"de-DE": "German"
}; };
var DEFAULT_LANGUAGE = "en-US"; var DEFAULT_LANGUAGE = "en-US";

View File

@ -286,6 +286,7 @@ jQuery(function(){
'<tr>' + '<tr>' +
'<td>' + e.Username + '</td>' + '<td>' + e.Username + '</td>' +
'<td>' + e.RepoName + '</td>' + '<td>' + e.RepoName + '</td>' +
'<td>' + e.RepoTag + '</td>' +
'<td>' + e.Operation + '</td>' + '<td>' + e.Operation + '</td>' +
'<td>' + moment(new Date(e.OpTime)).format("YYYY-MM-DD HH:mm:ss") + '</td>' + '<td>' + moment(new Date(e.OpTime)).format("YYYY-MM-DD HH:mm:ss") + '</td>' +
'</tr>'); '</tr>');

View File

@ -161,8 +161,8 @@ jQuery(function(){
$("#tblUser tbody tr").remove(); $("#tblUser tbody tr").remove();
$.each(data || [], function(i, e){ $.each(data || [], function(i, e){
var row = '<tr>' + var row = '<tr>' +
'<td style="vertical-align: middle;">' + e.Username + '</td>' + '<td style="vertical-align: middle;">' + e.username + '</td>' +
'<td style="vertical-align: middle;">' + e.Email + '</td>'; '<td style="vertical-align: middle;">' + e.email + '</td>';
if(e.HasAdminRole == 1){ if(e.HasAdminRole == 1){
row += '<td style="padding-left: 30px;"><button type="button" class="btn btn-success" userid="' + e.UserId + '">' + i18n.getMessage("button_on") + '</button></td>'; row += '<td style="padding-left: 30px;"><button type="button" class="btn btn-success" userid="' + e.UserId + '">' + i18n.getMessage("button_on") + '</button></td>';
} else { } else {

View File

@ -57,7 +57,7 @@ jQuery(function(){
}, },
complete: function(xhr, status){ complete: function(xhr, status){
$("#btnPageSignUp").prop("disabled", false); $("#btnPageSignUp").prop("disabled", false);
if(xhr && xhr.status == 200){ if(xhr && xhr.status == 201){
$("#dlgModal") $("#dlgModal")
.dialogModal({ .dialogModal({
"title": isAdmin == "true" ? i18n.getMessage("title_add_user") : i18n.getMessage("title_sign_up"), "title": isAdmin == "true" ? i18n.getMessage("title_add_user") : i18n.getMessage("title_sign_up"),

View File

@ -159,10 +159,11 @@
<table id="tblAccessLog" class="table table-hover" > <table id="tblAccessLog" class="table table-hover" >
<thead> <thead>
<tr> <tr>
<th width="20%">{{i18n .Lang "username"}}</th> <th width="15%">{{i18n .Lang "username"}}</th>
<th width="40%">{{i18n .Lang "repo_name"}}</th> <th width="30%">{{i18n .Lang "repo_name"}}</th>
<th width="20%">{{i18n .Lang "operation"}}</th> <th width="15%">{{i18n .Lang "repo_tag"}}</th>
<th width="20%">{{i18n .Lang "timestamp"}}</th> <th width="15%">{{i18n .Lang "operation"}}</th>
<th width="15%">{{i18n .Lang "timestamp"}}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@ -65,9 +65,9 @@
<table id="tblUser" class="table table-hover"> <table id="tblUser" class="table table-hover">
<thead> <thead>
<tr> <tr>
<th>{{i18n .Lang "username"}}</th> <th width="35%">{{i18n .Lang "username"}}</th>
<th>{{i18n .Lang "email"}}</th> <th width="45%">{{i18n .Lang "email"}}</th>
<th>{{i18n .Lang "system_admin"}}</th> <th width="20%">{{i18n .Lang "system_admin"}}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>

View File

@ -36,6 +36,7 @@
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="/language?lang=en-US">{{i18n .Lang "language_en-US"}}</a></li> <li><a href="/language?lang=en-US">{{i18n .Lang "language_en-US"}}</a></li>
<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>
</ul> </ul>
</li> </li>
</ul> </ul>