Merge pull request #272 from xiahaoshawn/new-version-of-ui

LGTM
This commit is contained in:
kun wang 2016-06-07 13:59:41 +08:00
commit bd0ef1eb3d
43 changed files with 615 additions and 253 deletions

View File

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

View File

@ -2,15 +2,9 @@ FROM library/ubuntu:14.04
# run logrotate hourly, disable imklog model, provides TCP/UDP syslog reception
RUN mv /etc/cron.daily/logrotate /etc/cron.hourly/ \
&& sed 's/$ModLoad imklog/#$ModLoad imklog/' -i /etc/rsyslog.conf \
&& sed 's/$KLogPermitNonKernelFacility on/#$KLogPermitNonKernelFacility on/' -i /etc/rsyslog.conf \
&& sed 's/#$ModLoad imudp/$ModLoad imudp/' -i /etc/rsyslog.conf \
&& sed 's/#$UDPServerRun 514/$UDPServerRun 514/' -i /etc/rsyslog.conf \
&& sed 's/#$ModLoad imtcp/$ModLoad imtcp/' -i /etc/rsyslog.conf \
&& sed 's/#$InputTCPServerRun 514/$InputTCPServerRun 514/' -i /etc/rsyslog.conf \
&& sed 's/$PrivDropToUser syslog/#$PrivDropToUser syslog/' -i /etc/rsyslog.conf \
&& sed 's/$PrivDropToGroup syslog/#$PrivDropToGroup syslog/' -i /etc/rsyslog.conf \
&& rm /etc/rsyslog.d/*
&& rm /etc/rsyslog.d/* \
&& rm /etc/rsyslog.conf
ADD rsyslog.conf /etc/rsyslog.conf
# logrotate configuration file for docker
ADD logrotate_docker.conf /etc/logrotate.d/
@ -23,4 +17,3 @@ VOLUME /var/log/docker/
EXPOSE 514
CMD cron && rsyslogd -n

60
Deploy/log/rsyslog.conf Normal file
View File

@ -0,0 +1,60 @@
# /etc/rsyslog.conf Configuration file for rsyslog.
#
# For more information see
# /usr/share/doc/rsyslog-doc/html/rsyslog_conf.html
#
# Default logging rules can be found in /etc/rsyslog.d/50-default.conf
#################
#### MODULES ####
#################
$ModLoad imuxsock # provides support for local system logging
#$ModLoad imklog # provides kernel logging support
#$ModLoad immark # provides --MARK-- message capability
# provides UDP syslog reception
$ModLoad imudp
$UDPServerRun 514
# provides TCP syslog reception
$ModLoad imtcp
$InputTCPServerRun 514
# Enable non-kernel facility klog messages
#$KLogPermitNonKernelFacility on
###########################
#### GLOBAL DIRECTIVES ####
###########################
#
# Use traditional timestamp format.
# To enable high precision timestamps, comment out the following line.
#
$ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat
# Filter duplicated messages
$RepeatedMsgReduction on
#
# Set the default permissions for all log files.
#
$FileOwner syslog
$FileGroup adm
$FileCreateMode 0640
$DirCreateMode 0755
$Umask 0022
#$PrivDropToUser syslog
#$PrivDropToGroup syslog
#
# Where to place spool and state files
#
$WorkDirectory /var/spool/rsyslog
#
# Include all config files in /etc/rsyslog.d/
#
$IncludeConfig /etc/rsyslog.d/*.conf

View File

@ -2,8 +2,9 @@ appname = registry
runmode = dev
[lang]
types = en-US|zh-CN
names = English|中文
types = en-US|zh-CN|de-DE|ru-RU|ja-JP
names = en-US|zh-CN|de-DE|ru-RU|ja-JP
[dev]
httpport = 80

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.
* **AD/LDAP support**: Harbor integrates with existing enterprise AD/LDAP for user authentication and management.
* **Auditing**: All the operations to the repositories are tracked.
* **Internationalization**: Already localized for English, Chinese, German and Russian. More languages can be added.
* **Internationalization**: Already localized for English, Chinese, German, Japanese and Russian. More languages can be added.
* **RESTful API**: RESTful APIs for most administrative operations, easing intergration with external management platforms.
### Getting Started
@ -67,7 +67,7 @@ Harbor is available under the [Apache 2 license](LICENSE).
&nbsp; &nbsp; <a href="https://www.caicloud.io" border="0"><img alt="CaiCloud" src="docs/img/caicloudLogoWeb.png"></a>
### 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
<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

@ -114,23 +114,10 @@ func (pma *ProjectMemberAPI) Get() {
// Post ...
func (pma *ProjectMemberAPI) Post() {
pid := pma.project.ProjectID
//userQuery := models.User{UserID: pma.currentUserID, RoleID: models.PROJECTADMIN}
rolelist, err := dao.GetUserProjectRoles(pma.currentUserID, pid)
if err != nil {
log.Errorf("Error occurred in GetUserProjectRoles, error: %v", err)
pma.CustomAbort(http.StatusInternalServerError, "Internal error.")
}
hasProjectAdminRole := false
for _, role := range rolelist {
if role.RoleID == models.PROJECTADMIN {
hasProjectAdminRole = true
break
}
}
if !hasProjectAdminRole {
log.Warningf("Current user, id: %d does not have project admin role for project, id:", pma.currentUserID, pid)
currentUserID := pma.currentUserID
projectID := pma.project.ProjectID
if !hasProjectAdminRole(currentUserID, projectID) {
log.Warningf("Current user, id: %d does not have project admin role for project, id:", currentUserID, projectID)
pma.RenderError(http.StatusForbidden, "")
return
}
@ -144,21 +131,21 @@ func (pma *ProjectMemberAPI) Post() {
pma.RenderError(http.StatusNotFound, "User does not exist")
return
}
rolelist, err = dao.GetUserProjectRoles(userID, pid)
rolelist, err := dao.GetUserProjectRoles(userID, projectID)
if err != nil {
log.Errorf("Error occurred in GetUserProjectRoles, error: %v", err)
pma.CustomAbort(http.StatusInternalServerError, "Internal error.")
}
if len(rolelist) > 0 {
log.Warningf("user is already added to project, user id: %d, project id: %d", userID, pid)
log.Warningf("user is already added to project, user id: %d, project id: %d", userID, projectID)
pma.RenderError(http.StatusConflict, "user is ready in project")
return
}
for _, rid := range req.Roles {
err = dao.AddProjectMember(pid, userID, int(rid))
err = dao.AddProjectMember(projectID, userID, int(rid))
if err != nil {
log.Errorf("Failed to update DB to add project user role, project id: %d, user id: %d, role id: %d", pid, userID, rid)
log.Errorf("Failed to update DB to add project user role, project id: %d, user id: %d, role id: %d", projectID, userID, rid)
pma.RenderError(http.StatusInternalServerError, "Failed to update data in database")
return
}
@ -167,27 +154,16 @@ func (pma *ProjectMemberAPI) Post() {
// Put ...
func (pma *ProjectMemberAPI) Put() {
currentUserID := pma.currentUserID
pid := pma.project.ProjectID
mid := pma.memberID
rolelist, err := dao.GetUserProjectRoles(pma.currentUserID, pid)
if err != nil {
log.Errorf("Error occurred in GetUserProjectRoles, error: %v", err)
pma.CustomAbort(http.StatusInternalServerError, "Internal error.")
}
hasProjectAdminRole := false
for _, role := range rolelist {
if role.RoleID == models.PROJECTADMIN {
hasProjectAdminRole = true
break
}
}
if !hasProjectAdminRole {
log.Warningf("Current user, id: %d does not have project admin role for project, id: %d", pma.currentUserID, pid)
if !hasProjectAdminRole(currentUserID, pid) {
log.Warningf("Current user, id: %d does not have project admin role for project, id:", currentUserID, pid)
pma.RenderError(http.StatusForbidden, "")
return
}
mid := pma.memberID
var req memberReq
pma.DecodeJSONReq(&req)
roleList, err := dao.GetUserProjectRoles(mid, pid)
@ -217,51 +193,20 @@ func (pma *ProjectMemberAPI) Put() {
// Delete ...
func (pma *ProjectMemberAPI) Delete() {
currentUserID := pma.currentUserID
pid := pma.project.ProjectID
mid := pma.memberID
rolelist, err := dao.GetUserProjectRoles(pma.currentUserID, pid)
hasProjectAdminRole := false
for _, role := range rolelist {
if role.RoleID == models.PROJECTADMIN {
hasProjectAdminRole = true
break
}
}
if !hasProjectAdminRole {
log.Warningf("Current user, id: %d does not have project admin role for project, id: %d", pma.currentUserID, pid)
if !hasProjectAdminRole(currentUserID, pid) {
log.Warningf("Current user, id: %d does not have project admin role for project, id:", currentUserID, pid)
pma.RenderError(http.StatusForbidden, "")
return
}
err = dao.DeleteProjectMember(pid, mid)
mid := pma.memberID
err := dao.DeleteProjectMember(pid, mid)
if err != nil {
log.Errorf("Failed to delete project roles for user, user id: %d, project id: %d, error: %v", mid, pid, err)
pma.RenderError(http.StatusInternalServerError, "Failed to update data in DB")
return
}
}
//sysadmin has all privileges to all projects
func listRoles(userID int, projectID int64) ([]models.Role, error) {
roles := make([]models.Role, 1)
isSysAdmin, err := dao.IsAdminRole(userID)
if err != nil {
return roles, err
}
if isSysAdmin {
role, err := dao.GetRoleByID(models.PROJECTADMIN)
if err != nil {
return roles, err
}
roles = append(roles, *role)
return roles, nil
}
rs, err := dao.GetUserProjectRoles(userID, projectID)
if err != nil {
return roles, err
}
roles = append(roles, rs...)
return roles, nil
}

View File

@ -43,7 +43,6 @@ const projectNameMaxLen int = 30
// Prepare validates the URL and the user
func (p *ProjectAPI) Prepare() {
p.userID = p.ValidateUser()
idStr := p.Ctx.Input.Param(":id")
if len(idStr) > 0 {
var err error
@ -65,6 +64,8 @@ func (p *ProjectAPI) Prepare() {
// Post ...
func (p *ProjectAPI) Post() {
p.userID = p.ValidateUser()
var req projectReq
var public int
p.DecodeJSONReq(&req)
@ -99,20 +100,52 @@ func (p *ProjectAPI) Post() {
// Head ...
func (p *ProjectAPI) Head() {
projectName := p.GetString("project_name")
result, err := dao.ProjectExists(projectName)
if len(projectName) == 0 {
p.CustomAbort(http.StatusBadRequest, "project_name is needed")
}
project, err := dao.GetProjectByName(projectName)
if err != nil {
log.Errorf("Error while communicating with DB, error: %v", err)
p.RenderError(http.StatusInternalServerError, "Error while communicating with DB")
log.Errorf("error occurred in GetProjectByName: %v", err)
p.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
// only public project can be Headed by user without login
if project != nil && project.Public == 1 {
return
}
if !result {
p.RenderError(http.StatusNotFound, "")
return
userID := p.ValidateUser()
if project == nil {
p.CustomAbort(http.StatusNotFound, http.StatusText(http.StatusNotFound))
}
if !checkProjectPermission(userID, project.ProjectID) {
p.CustomAbort(http.StatusForbidden, http.StatusText(http.StatusForbidden))
}
}
// Get ...
func (p *ProjectAPI) Get() {
project, err := dao.GetProjectByID(p.projectID)
if err != nil {
log.Errorf("failed to get project %d: %v", p.projectID, err)
p.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if project.Public == 0 {
userID := p.ValidateUser()
if !checkProjectPermission(userID, p.projectID) {
p.CustomAbort(http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized))
}
}
p.Data["json"] = project
p.ServeJSON()
}
// List ...
func (p *ProjectAPI) List() {
var projectList []models.Project
projectName := p.GetString("project_name")
if len(projectName) > 0 {
@ -132,6 +165,8 @@ func (p *ProjectAPI) Get() {
if public == 1 {
projectList, err = dao.GetPublicProjects(projectName)
} else {
//if the request is not for public projects, user must login or provide credential
p.userID = p.ValidateUser()
isAdmin, err = dao.IsAdminRole(p.userID)
if err != nil {
log.Errorf("Error occured in check admin, error: %v", err)
@ -164,6 +199,8 @@ func (p *ProjectAPI) Get() {
// Put ...
func (p *ProjectAPI) Put() {
p.userID = p.ValidateUser()
var req projectReq
var public int
@ -192,6 +229,7 @@ func (p *ProjectAPI) Put() {
// FilterAccessLog handles GET to /api/projects/{}/logs
func (p *ProjectAPI) FilterAccessLog() {
p.userID = p.ValidateUser()
var filter models.AccessLog
p.DecodeJSONReq(&filter)

View File

@ -19,6 +19,7 @@ import (
"encoding/json"
"net/http"
"os"
"sort"
"strconv"
"strings"
"time"
@ -29,6 +30,7 @@ import (
svc_utils "github.com/vmware/harbor/service/utils"
"github.com/vmware/harbor/utils/log"
"github.com/vmware/harbor/utils/registry"
"github.com/vmware/harbor/utils/registry/auth"
"github.com/vmware/harbor/utils/registry/errors"
)
@ -38,19 +40,13 @@ import (
// the security of registry
type RepositoryAPI struct {
BaseAPI
userID int
}
// Prepare will set a non existent user ID in case the request tries to view repositories under a project he doesn't has permission.
func (ra *RepositoryAPI) Prepare() {
ra.userID = ra.ValidateUser()
}
// Get ...
func (ra *RepositoryAPI) Get() {
projectID, err0 := ra.GetInt64("project_id")
if err0 != nil {
log.Errorf("Failed to get project id, error: %v", err0)
projectID, err := ra.GetInt64("project_id")
if err != nil {
log.Errorf("Failed to get project id, error: %v", err)
ra.RenderError(http.StatusBadRequest, "Invalid project id")
return
}
@ -64,9 +60,14 @@ func (ra *RepositoryAPI) Get() {
ra.RenderError(http.StatusNotFound, "")
return
}
if p.Public == 0 && !checkProjectPermission(ra.userID, projectID) {
ra.RenderError(http.StatusForbidden, "")
return
if p.Public == 0 {
userID := ra.ValidateUser()
if !checkProjectPermission(userID, projectID) {
ra.RenderError(http.StatusForbidden, "")
return
}
}
repoList, err := svc_utils.GetRepoFromCache()
@ -105,7 +106,7 @@ func (ra *RepositoryAPI) Delete() {
ra.CustomAbort(http.StatusBadRequest, "repo_name is nil")
}
rc, err := ra.initializeRepositoryClient(repoName)
rc, err := ra.initRepositoryClient(repoName)
if err != nil {
log.Errorf("error occurred while initializing repository client for %s: %v", repoName, err)
ra.CustomAbort(http.StatusInternalServerError, "internal error")
@ -164,7 +165,7 @@ func (ra *RepositoryAPI) GetTags() {
ra.CustomAbort(http.StatusBadRequest, "repo_name is nil")
}
rc, err := ra.initializeRepositoryClient(repoName)
rc, err := ra.initRepositoryClient(repoName)
if err != nil {
log.Errorf("error occurred while initializing repository client for %s: %v", repoName, err)
ra.CustomAbort(http.StatusInternalServerError, "internal error")
@ -185,6 +186,8 @@ func (ra *RepositoryAPI) GetTags() {
tags = append(tags, ts...)
sort.Strings(tags)
ra.Data["json"] = tags
ra.ServeJSON()
}
@ -198,7 +201,7 @@ func (ra *RepositoryAPI) GetManifests() {
ra.CustomAbort(http.StatusBadRequest, "repo_name or tag is nil")
}
rc, err := ra.initializeRepositoryClient(repoName)
rc, err := ra.initRepositoryClient(repoName)
if err != nil {
log.Errorf("error occurred while initializing repository client for %s: %v", repoName, err)
ra.CustomAbort(http.StatusInternalServerError, "internal error")
@ -238,16 +241,50 @@ func (ra *RepositoryAPI) GetManifests() {
ra.ServeJSON()
}
func (ra *RepositoryAPI) initializeRepositoryClient(repoName string) (r *registry.Repository, err error) {
u := models.User{
UserID: ra.userID,
func (ra *RepositoryAPI) initRepositoryClient(repoName string) (r *registry.Repository, err error) {
endpoint := os.Getenv("REGISTRY_URL")
username, password, ok := ra.Ctx.Request.BasicAuth()
if ok {
credential := auth.NewBasicAuthCredential(username, password)
return registry.NewRepositoryWithCredential(repoName, endpoint, credential)
}
user, err := dao.GetUser(u)
username, err = ra.getUsername()
if err != nil {
return nil, err
}
endpoint := os.Getenv("REGISTRY_URL")
return registry.NewRepositoryWithUsername(repoName, endpoint, user.Username)
return registry.NewRepositoryWithUsername(repoName, endpoint, username)
}
func (ra *RepositoryAPI) getUsername() (string, error) {
// get username from session
sessionUsername := ra.GetSession("username")
if sessionUsername != nil {
username, ok := sessionUsername.(string)
if ok {
return username, nil
}
}
// if username does not exist in session, try to get userId from sessiion
// and then get username from DB according to the userId
sessionUserID := ra.GetSession("userId")
if sessionUserID != nil {
userID, ok := sessionUserID.(int)
if ok {
u := models.User{
UserID: userID,
}
user, err := dao.GetUser(u)
if err != nil {
return "", err
}
return user.Username, nil
}
}
return "", nil
}

View File

@ -22,20 +22,52 @@ import (
)
func checkProjectPermission(userID int, projectID int64) bool {
exist, err := dao.IsAdminRole(userID)
roles, err := listRoles(userID, projectID)
if err != nil {
log.Errorf("Error occurred in IsAdminRole, error: %v", err)
log.Errorf("error occurred in getProjectPermission: %v", err)
return false
}
if exist {
return true
}
roleList, err := dao.GetUserProjectRoles(userID, projectID)
return len(roles) > 0
}
func hasProjectAdminRole(userID int, projectID int64) bool {
roles, err := listRoles(userID, projectID)
if err != nil {
log.Errorf("Error occurred in GetUserProjectRoles, error: %v", err)
log.Errorf("error occurred in getProjectPermission: %v", err)
return false
}
return len(roleList) > 0
for _, role := range roles {
if role.RoleID == models.PROJECTADMIN {
return true
}
}
return false
}
//sysadmin has all privileges to all projects
func listRoles(userID int, projectID int64) ([]models.Role, error) {
roles := make([]models.Role, 0, 1)
isSysAdmin, err := dao.IsAdminRole(userID)
if err != nil {
return roles, err
}
if isSysAdmin {
role, err := dao.GetRoleByID(models.PROJECTADMIN)
if err != nil {
return roles, err
}
roles = append(roles, *role)
return roles, nil
}
rs, err := dao.GetUserProjectRoles(userID, projectID)
if err != nil {
return roles, err
}
roles = append(roles, rs...)
return roles, nil
}
func checkUserExists(name string) int {

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.
func (c *CommonController) SwitchLanguage() {
lang := c.GetString("lang")
if lang == "en-US" || lang == "zh-CN" || lang == "de-DE" || lang == "ru-RU" {
if lang == "en-US" || lang == "zh-CN" || lang == "de-DE" || lang == "ru-RU" || lang == "ja-JP" {
c.SetSession("lang", lang)
c.Data["Lang"] = lang
}

View File

@ -8,10 +8,12 @@ import (
"github.com/vmware/harbor/utils/log"
)
// SignInController handles requests to /ng/sign_in
type SignInController struct {
BaseController
}
//Get renders sign_in page
func (sic *SignInController) Get() {
sessionUserID := sic.GetSession("userId")
var hasLoggedIn bool

View File

@ -249,7 +249,6 @@ func getProjects(public int, projectName string) ([]models.Project, error) {
}
sql += " order by name "
var projects []models.Project
log.Debugf("sql xxx", sql)
if _, err := o.Raw(sql, queryParam).QueryRows(&projects); err != nil {
return nil, err
}

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 is a module for migrating database schema between different version of project [harbor](https://github.com/vmware/harbor)
# Migration guide
Migration is a module for migrating database schema between different version of project [Harbor](https://github.com/vmware/harbor)
This module is for those machine running Harbor's old version, such as 0.1.0. If your Harbor' version is up to date, please ignore this module.
**WARNING!!** You must backup your data before migrating
###installation
- step 1: modify migration.cfg
###Installation
- step 1: change `db_username`, `db_password`, `db_port`, `db_name` in migration.cfg
- step 2: build image from dockerfile
```
cd harbor-migration
docker build -t your-image-name .
docker build -t migrate-tool .
```
###migration operation
- show instruction of harbor-migration
```docker run your-image-name help```
- test mysql connection in harbor-migration
```docker run -v /data/database:/var/lib/mysql your-image-name test```
- create backup file in `/path/to/backup`
```
docker run -ti -v /data/database:/var/lib/mysql -v /path/to/backup:/harbor-migration/backup your-image-name backup
```
- restore from backup file in `/path/to/backup`
```
docker run -ti -v /data/database:/var/lib/mysql -v /path/to/backup:/harbor-migration/backup your-image-name restore
```
- perform database schema upgrade
```docker run -ti -v /data/database:/var/lib/mysql your-image-name up head```
you can use `-v /etc/localtime:/etc/localtime` to sync container timezone with host timezone.
you may change `/data/database` to the mysql volumes path you set in docker-compose.yml.
###migration step
- step 1: stop and remove harbor service
###Migrate Step
- step 1: stop and remove Harbor service
```
docker-compose down
```
- step 2: perform migration operation
- step 3: rebuild newest harbor images and restart service
- step 2: create backup file in `/path/to/backup`
```
docker run -ti --rm -v /data/database:/var/lib/mysql -v /path/to/backup:/harbor-migration/backup migrate-tool backup
```
- step 3: perform database schema upgrade
```docker run -ti --rm -v /data/database:/var/lib/mysql migrate-tool up head```
- step 4: rebuild newest Harbor images and restart service
```
docker-compose build && docker-compose up -d
```
You may change `/data/database` to the mysql volumes path you set in docker-compose.yml.
###Migration operation reference
- You can use `help` to show instruction of Harbor migration
```docker run migrate-tool help```
- You can use `test` to test mysql connection in Harbor migration
```docker run --rm -v /data/database:/var/lib/mysql migrate-tool test```
- You can restore from backup file in `/path/to/backup`
```
docker run -ti --rm -v /data/database:/var/lib/mysql -v /path/to/backup:/harbor-migration/backup migrate-tool restore
```

View File

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

View File

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

View File

@ -46,10 +46,24 @@ func GetResourceActions(scopes []string) []*token.ResourceActions {
continue
}
items := strings.Split(s, ":")
length := len(items)
typee := items[0]
name := ""
if length > 1 {
name = items[1]
}
actions := []string{}
if length > 2 {
actions = strings.Split(items[2], ",")
}
res = append(res, &token.ResourceActions{
Type: items[0],
Name: items[1],
Actions: strings.Split(items[2], ","),
Type: typee,
Name: name,
Actions: actions,
})
}
return res

View File

@ -75,6 +75,7 @@ language_en-US = English
language_zh-CN = 中文
language_de-DE = Deutsch
language_ru-RU = Русский
language_ja-JP = 日本語
copyright = Copyright
all_rights_reserved = Alle Rechte vorbehalten.
index_desc = Project Harbor ist ein zuverlässiger Enterprise-Class Registry Server. Unternehmen können ihren eigenen Registry Server aufsetzen um die Produktivität und Sicherheit zu erhöhen. Project Harbor kann für Entwicklungs- wie auch Produktiv-Umgebungen genutzt werden.

View File

@ -76,6 +76,7 @@ language_en-US = English
language_zh-CN = 中文
language_de-DE = Deutsch
language_ru-RU = Русский
language_ja-JP = 日本語
copyright = Copyright
all_rights_reserved = All rights reserved.
index_desc = Project Harbor is to build an enterprise-class, reliable registry server. Enterprises can set up a private registry server in their own environment to improve productivity as well as security. Project Harbor can be used in both development and production environment.

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@
function roles() {
return [
{'id': '0', 'name': 'NA', 'roleName': 'NA'},
{'id': '1', 'name': 'Project Admin', 'roleName': 'projectAdmin'},
{'id': '2', 'name': 'Developer', 'roleName': 'developer'},
{'id': '3', 'name': 'Guest', 'roleName': 'guest'}
@ -25,7 +26,7 @@
for(var i = 0; i < r.length; i++) {
var role = r[i];
if(query.key === 'roleName' && role.roleName === query.value
|| query.key === 'roleId' && role.id === query.value) {
|| query.key === 'roleId' && role.id === String(query.value)) {
return role;
}
}

View File

@ -0,0 +1,4 @@
<h4 class="page-header title-color underlined">// 'summary' | tr //</h4>
<dl class="page-content dl-horizontal" ng-repeat="(key, value) in vm.statProjects">
<dt>// key | tr //:</dt><dd>//value//</dd>
</dl>

View File

@ -0,0 +1,40 @@
(function() {
'use strict';
angular
.module('harbor.summary')
.directive('projectSummary', projectSummary);
ProjectSummaryController.$inject = ['StatProjectService'];
function ProjectSummaryController(StatProjectService) {
var vm = this;
StatProjectService()
.success(statProjectSuccess)
.error(statProjectFailed);
function statProjectSuccess(data, status) {
vm.statProjects = data;
}
function statProjectFailed(status) {
console.log('Failed stat project:' + status);
}
}
function projectSummary() {
var directive = {
'restrict': 'E',
'templateUrl': '/static/ng/resources/js/components/summary/summary.directive.html',
'controller': ProjectSummaryController,
'scope' : true,
'controllerAs': 'vm',
'bindToController': true
};
return directive;
}
})();

View File

@ -0,0 +1,10 @@
(function() {
'use strict';
angular
.module('harbor.summary', [
'harbor.services.project'
]);
})();

View File

@ -27,6 +27,7 @@
'harbor.services.user',
'harbor.services.repository',
'harbor.services.project.member',
'harbor.summary',
'harbor.optional.menu',
'harbor.modal.dialog',
'harbor.sign.in',

View File

@ -6,28 +6,17 @@
.module('harbor.layout.dashboard')
.controller('DashboardController', DashboardController);
DashboardController.$inject = ['StatProjectService', 'ListTop10RepositoryService', 'ListIntegratedLogService'];
DashboardController.$inject = ['ListTop10RepositoryService', 'ListIntegratedLogService'];
function DashboardController(StatProjectService, ListTop10RepositoryService, ListIntegratedLogService) {
function DashboardController(ListTop10RepositoryService, ListIntegratedLogService) {
var vm = this;
StatProjectService()
.then(statProjectSuccess, statProjectFailed);
ListTop10RepositoryService()
.then(listTop10RepositorySuccess, listTop10RepositoryFailed);
ListIntegratedLogService()
.then(listIntegratedLogSuccess, listIntegratedLogFailed);
function statProjectSuccess(data) {
vm.statProjects = data;
}
function statProjectFailed(data) {
console.log('Failed stat project:' + data);
}
function listTop10RepositorySuccess(data) {
vm.top10Repositories = data;
}

View File

@ -4,7 +4,6 @@
angular
.module('harbor.layout.dashboard', [
'harbor.services.project',
'harbor.services.repository',
'harbor.services.log'
]);

View File

@ -6,9 +6,9 @@
.module('harbor.layout.project')
.controller('ProjectController', ProjectController);
ProjectController.$inject = ['$scope', 'ListProjectService', '$timeout', 'currentUser'];
ProjectController.$inject = ['$scope', 'ListProjectService', '$timeout', 'currentUser', 'getRole'];
function ProjectController($scope, ListProjectService, $timeout, currentUser) {
function ProjectController($scope, ListProjectService, $timeout, currentUser, getRole) {
var vm = this;
vm.isOpen = false;
@ -22,6 +22,7 @@
vm.togglePublicity = togglePublicity;
vm.user = currentUser.get();
vm.retrieve();
vm.getProjectRole = getProjectRole;
function retrieve() {
@ -34,6 +35,11 @@
vm.projects = data || [];
}
function getProjectRole(roleId) {
var role = getRole({'key': 'roleId', 'value': roleId});
return role.name;
}
function listProjectFailed(e) {
console.log('Failed to list Project:' + e);
}

View File

@ -4,6 +4,7 @@
angular
.module('harbor.layout.project', [
'harbor.project.member',
'harbor.services.project',
'harbor.services.user'
]);

View File

@ -45,15 +45,18 @@ var locale_messages = {
'comments': 'Comments',
'comment_is_too_long': 'Comment is too long. (maximum 20 characters)',
'forgot_password_description': 'Please input the Email used when you signed up, a reset password Email will be sent to you.',
'email_does_not_exist': 'Email does not exist',
'reset_password': 'Reset Password',
'summary': 'Summary',
'projects': 'Projects',
'public_projects': 'Public Projects',
'public': 'Public',
'total_projects': 'Total Projects',
'public_repositories': 'Public Repositories',
'total_repositories': 'Total Repositories',
'my_project_count': 'Projects',
'my_repo_count': 'Repositories',
'public_project_count': 'Public Projects',
'public_repo_count': 'Public Repositories',
'total_project_count': 'Total Projects',
'total_repo_count': 'Total Repositories',
'top_10_repositories': 'Top 10 Repositories',
'repository_name': 'Repository Name',
'size': 'Size',

View File

@ -45,15 +45,18 @@ var locale_messages = {
'comments': '备注',
'comment_is_too_long' : '备注长度超出限制。最长为20个字符',
'forgot_password_description': '重置邮件将发送到此邮箱。',
'email_does_not_exist': '邮箱不存在。',
'reset_password': '重置密码',
'summary': '摘要',
'projects': '项目',
'public_projects': '公开项目',
'public': '公开',
'total_projects': '全部项目',
'public_repositories': '公开镜像仓库',
'total_repositories': '全部镜像仓库',
'my_project_count': '项目',
'my_repo_count': '镜像仓库',
'public_project_count': '公开项目',
'public_repo_count': '公开镜像仓库',
'total_project_count': '全部项目',
'total_repo_count': '全部镜像仓库',
'top_10_repositories': 'Top 10 镜像仓库',
'repository_name': '镜像仓库名',
'size': '规格',

View File

@ -6,35 +6,18 @@
.module('harbor.services.project')
.factory('StatProjectService', StatProjectService);
StatProjectService.$inject = ['$http', '$q', '$timeout'];
StatProjectService.$inject = ['$http', '$log'];
function StatProjectService($http, $q, $timeout) {
function StatProjectService($http, $log) {
return StatProject;
var mockData = {
'projects': 30,
'public_projects': 50,
'total_projects': 120,
'repositories': 50,
'public_repositories': 40,
'total_repositories': 110
};
function async() {
var deferred = $q.defer();
$timeout(function() {
deferred.resolve(mockData);
}, 500);
return deferred.promise;
function StatProject() {
$log.info('statistics projects and repositories');
return $http
.get('/api/statistics');
}
return statProject;
function statProject() {
return async();
}
}
})();

View File

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

View File

@ -62,7 +62,7 @@ jQuery(function(){
return;
}
$.each(data, function(i, e){
var targetId = e.replace(/\//g, "------");
var targetId = e.replace(/\//g, "------").replace(/\./g, "---");
var row = '<div class="panel panel-default" targetId="' + targetId + '">' +
'<div class="panel-heading" role="tab" id="heading' + i + '"+ >' +
'<h4 class="panel-title">' +
@ -105,7 +105,7 @@ jQuery(function(){
$('#accordionRepo').on('show.bs.collapse', function (e) {
$('#accordionRepo .in').collapse('hide');
var targetId = $(e.target).attr("targetId");
var repoName = targetId.replace(/------/g, "/");
var repoName = targetId.replace(/[-]{6}/g, "/").replace(/[-]{3}/g, '.');
new AjaxUtil({
url: "/api/repositories/tags?repo_name=" + repoName,
type: "get",
@ -113,8 +113,8 @@ jQuery(function(){
$('#' + targetId +' table tbody tr').remove();
var row = [];
for(var i in data){
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>');
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>');
}
$('#' + targetId +' table tbody').append(row.join(""));
$('#' + targetId +' table tbody tr a').on("click", function(e){

View File

@ -53,6 +53,7 @@ func initRouters() {
//API:
beego.Router("/api/search", &api.SearchAPI{})
beego.Router("/api/projects/:pid/members/?:mid", &api.ProjectMemberAPI{})
beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List")
beego.Router("/api/projects/?:id", &api.ProjectAPI{})
beego.Router("/api/statistics", &api.StatisticAPI{})
beego.Router("/api/projects/:id/logs/filter", &api.ProjectAPI{}, "post:FilterAccessLog")

View File

@ -4,15 +4,7 @@
<div class="col-xs-4 col-md-4">
<div class="row">
<div class="up-section">
<h4 class="page-header title-color underlined">// 'summary' | tr //</h4>
<dl class="page-content dl-horizontal">
<dt>// 'projects' | tr //:</dt><dd>//vm.statProjects['projects']//</dd>
<dt>// 'public_projects' | tr //:</dt><dd>//vm.statProjects['public_projects']//</dd>
<dt>// 'total_projects' | tr //:</dt><dd>//vm.statProjects['total_projects']//</dd>
<dt>// 'repositories' | tr //:</dt><dd>//vm.statProjects['repositories']//</dd>
<dt>// 'public_repositories' | tr //:</dt><dd>//vm.statProjects['public_repositories']//</dd>
<dt>// 'total_repositories' | tr //:</dt><dd>//vm.statProjects['total_repositories']//</dd>
</dl>
<project-Summary></project-Summary>
</div>
</div>
</div>

View File

@ -35,8 +35,8 @@
</tr>
<tr ng-if="vm.projects.length > 0" ng-repeat="p in vm.projects">
<td><a href="/ng/repository#/repositories?project_id=//p.ProjectId//&is_public=//p.Public//">//p.Name//</a></td>
<td>N/A</td>
<td>N/A</td>
<td>//p.repo_count//</td>
<td>//vm.getProjectRole(p.role_id)//</td>
<td>//p.CreationTime | dateL : 'YYYY-MM-DD HH:mm:ss'//</td>
<td><publicity-button is-public="p.Public" owned="p.OwnerId == vm.user.UserId" project-id="p.ProjectId"></publicity-button></td>
</tr>

View File

@ -197,3 +197,6 @@
<script src="/static/ng/resources/js/components/log/log.config.js"></script>
<script src="/static/ng/resources/js/components/log/list-log.directive.js"></script>
<script src="/static/ng/resources/js/components/log/advanced-search.directive.js"></script>
<script src="/static/ng/resources/js/components/summary/summary.module.js"></script>
<script src="/static/ng/resources/js/components/summary/summary.directive.js"></script>

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=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=ja-JP">{{i18n .Lang "language_ja-JP"}}</a></li>
</ul>
</li>
</ul>