diff --git a/Deploy/config/nginx/nginx.https.conf b/Deploy/config/nginx/nginx.https.conf index c802943c1..e7ee49ede 100644 --- a/Deploy/config/nginx/nginx.https.conf +++ b/Deploy/config/nginx/nginx.https.conf @@ -89,6 +89,6 @@ http { server { listen 80; server_name harbordomain.com; - rewrite ^/(.*) https://$server_name$1 permanent; + rewrite ^/(.*) https://$server_name/$1 permanent; } } diff --git a/Deploy/db/registry.sql b/Deploy/db/registry.sql index 1fd0eafa3..870d2d032 100644 --- a/Deploy/db/registry.sql +++ b/Deploy/db/registry.sql @@ -39,7 +39,7 @@ insert into role (role_code, name) values create table user ( user_id int NOT NULL AUTO_INCREMENT, username varchar(15), - email varchar(30), + email varchar(128), password varchar(40) NOT NULL, realname varchar (20) NOT NULL, comment varchar (30), diff --git a/Deploy/templates/ui/app.conf b/Deploy/templates/ui/app.conf index a5cf4dfdf..090bcdc47 100644 --- a/Deploy/templates/ui/app.conf +++ b/Deploy/templates/ui/app.conf @@ -2,8 +2,8 @@ appname = registry runmode = dev [lang] -types = en-US|zh-CN|de-DE -names = en-US|zh-CN|de-DE +types = en-US|zh-CN|de-DE|ru-RU +names = en-US|zh-CN|de-DE|ru-RU [dev] httpport = 80 diff --git a/README.md b/README.md index 06dbb8cf9..950891463 100644 --- a/README.md +++ b/README.md @@ -6,22 +6,21 @@ > Project Harbor is initiated by VMware China R&D as a Cloud Application Accelerator (CAA) project. CAA provides a set of tools to improve the productivity of cloud developers in China and other countries. CAA includes tools like registry server, mirror server, decentralized image distributor, etc. -Project Harbor is an enterprise-class registry server. It extends the open source Docker Registry server by adding more functionalities usually required by an enterprise. Harbor is designed to be deployed in a private environment of an organization. A private registry is important for organizations who care much about security. In addition, a private registry improves productivity by eliminating the need to download images from the public network. This is very helpful to container users who do not have a good network to the Internet. +Project Harbor is an enterprise-class registry server, which extends the open source Docker Registry server by adding the functionality usually required by an enterprise, such as security, control, and management. Harbor is primarily designed to be a private registry - providing the needed security and control that enterprises require. It also helps minimize bandwidth usage, which is helpful to both improve productivity (local network access) as well as performance (for those with poor internet connectivity). ### Features * **Role Based Access Control**: Users and docker repositories are organized via "projects", a user can have different permission for images under a namespace. * **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. -* **Auditing**: All the operations to the repositories are tracked and can be used for auditing purpose. -* **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. +* **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 and German. More languages can be added. +* **RESTful API**: RESTful APIs for most administrative operations, easing intergration with external management platforms. ### Getting Started -Harbor is self-contained and can be easily deployed via docker-compose. The below are quick-start steps. Refer to the [Installation and Configuration Guide](docs/installation_guide.md) for detail information. +Harbor is self-contained and can be easily deployed via docker-compose (Quick-Start steps below). Refer to the [Installation and Configuration Guide](docs/installation_guide.md) for detailed information. **System requirements:** -Harbor only works with docker 1.10+ and docker-compose 1.6.0+ . -The host must be connected to the Internet. +Harbor only works with docker 1.10+ and docker-compose 1.6.0+, and an internet-connected host 1. Get the source code: @@ -31,7 +30,7 @@ The host must be connected to the Internet. 2. Edit the file **Deploy/harbor.cfg**, make necessary configuration changes such as hostname, admin password and mail server. Refer to [Installation and Configuration Guide](docs/installation_guide.md) for more info. -3. Install Harbor by the following commands. It may take a while for the docker-compose process to finish. +3. Install Harbor with the following commands. Note that the docker-compose process can take a while! ```sh $ cd Deploy @@ -44,21 +43,21 @@ The host must be connected to the Internet. $ docker-compose up ``` -If everything works fine, you can open a browser to visit the admin portal at http://reg.yourdomain.com . The default administrator username and password are admin/Harbor12345 . +_If everything worked properly, you should be able to open a browser to visit the admin portal at http://reg.yourdomain.com . Note that the default administrator username/password are admin/Harbor12345 ._ -Log in to the admin portal and create a new project, e.g. myproject. You can then use docker commands to login and push images. The default port of Harbor registry server is 80: +Log in to the admin portal and create a new project, e.g. `myproject`. You can then use docker commands to login and push images (By default, the registry server listens on port 80): ```sh $ docker login reg.yourdomain.com $ docker push reg.yourdomain.com/myproject/myrepo ``` **NOTE:** -To simplify the installation process, a pre-built installation package of Harbor is provided so that you don't need to clone the source code. By using this package, you can even install Harbor onto a host that is not connected to the Internet. For details on how to download and use this installation package, please refer to [Installation and Configuration Guide](docs/installation_guide.md) . +For those who don't want to clone the source, or need to install Harbor on a server not connected to the Internet - there is a pre-built installation package available. For details on how to download and use this installation package, please refer to [Installation and Configuration Guide](docs/installation_guide.md) . For information on how to use Harbor, please see [User Guide](docs/user_guide.md) . ### Deploy Harbor on Kubernetes -Detailed instruction about deploying Harbor on Kubernetes is described [here](docs/kubernetes_deployment.md). +Detailed instruction about deploying Harbor on Kubernetes is available [here](docs/kubernetes_deployment.md). ### Contribution We welcome contributions from the community. If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a pull request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq). diff --git a/api/repository.go b/api/repository.go index b0361af3a..a827d58bc 100644 --- a/api/repository.go +++ b/api/repository.go @@ -29,7 +29,6 @@ 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" ) @@ -39,22 +38,12 @@ import ( // the security of registry type RepositoryAPI struct { BaseAPI - userID int - username string + 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() { - userID, ok := ra.GetSession("userId").(int) - if !ok { - userID = dao.NonExistUserID - } - ra.userID = userID - - username, ok := ra.GetSession("username").(string) - if ok { - ra.username = username - } + ra.userID = ra.ValidateUser() } // Get ... @@ -250,29 +239,15 @@ func (ra *RepositoryAPI) GetManifests() { } func (ra *RepositoryAPI) initializeRepositoryClient(repoName string) (r *registry.Repository, err error) { + u := models.User{ + UserID: ra.userID, + } + user, err := dao.GetUser(u) + if err != nil { + return nil, err + } + endpoint := os.Getenv("REGISTRY_URL") - //no session, use basic auth - if ra.userID == dao.NonExistUserID { - username, password, _ := ra.Ctx.Request.BasicAuth() - credential := auth.NewBasicAuthCredential(username, password) - - return registry.NewRepositoryWithCredential(repoName, endpoint, credential) - - } - - //session exists, use username - if len(ra.username) == 0 { - u := models.User{ - UserID: ra.userID, - } - user, err := dao.GetUser(u) - if err != nil { - return nil, err - } - - ra.username = user.Username - } - - return registry.NewRepositoryWithUsername(repoName, endpoint, ra.username) + return registry.NewRepositoryWithUsername(repoName, endpoint, user.Username) } diff --git a/controllers/login.go b/controllers/login.go index 42d324f08..2bfb29983 100644 --- a/controllers/login.go +++ b/controllers/login.go @@ -69,7 +69,7 @@ func (c *CommonController) Login() { // SwitchLanguage handles UI request to switch between different languages and re-render template based on language. func (c *CommonController) SwitchLanguage() { lang := c.GetString("lang") - if lang == "en-US" || lang == "zh-CN" || lang == "de-DE" { + if lang == "en-US" || lang == "zh-CN" || lang == "de-DE" || lang == "ru-RU" { c.SetSession("lang", lang) c.Data["Lang"] = lang } diff --git a/docs/installation_guide.md b/docs/installation_guide.md index cfc0a2c01..a94c24d10 100644 --- a/docs/installation_guide.md +++ b/docs/installation_guide.md @@ -1,21 +1,28 @@ -# Installation and Configuration Guide of Harbor -Harbor can be installed by two approaches: +# Installation and Configuration Guide +Harbor can be installed in one of two ways: -1. Installing from the source code, which goes through a full build process. Internet connection is required. -2. Installing via a pre-built installation package, which saves time for building the code. Further, it provides a way to install Harbor to a host that is isolated from the Internet (offline installation). +1. From source code - This goes through a full build process, _and requires an Internet connection_. +2. Pre-built installation package - This can save time (no building necessary!) as well as allows for installation on a host that is _not_ connected to the Internet. -This guide describes both approaches and their usage. +This guide describes both of these approaches -## Prerequisites of the target host -Harbor is deployed as several Docker containers. Hence, it can be deployed on any Linux distribution that supports Docker. -Before deploying Harbor, the target host requires Python, Docker, Docker Compose to be installed. -* Python should be version 2.7 or higher. Some Linux distributions (Gentoo, Arch) may not have a Python interpreter installed by default. On those systems, you need to install Python manually. -* The Docker engine should be version 1.10 or higher. For the details to install Docker engine, please refer to: https://docs.docker.com/engine/installation/ -* The Docker Compose needs to be version 1.6.0 or higher. For the details to install Docker compose, please refer to: https://docs.docker.com/compose/install/ +## Prerequisites for the target host +Harbor is deployed as several Docker containers, and, therefore, can be deployed on any Linux distribution that supports Docker. +The target host requires Python, Docker, and Docker Compose to be installed. +* Python should be version 2.7 or higher. Note that you may have to install Python on Linux distributions (Gentoo, Arch) that do not come with a Python interpreter installed by default +* Docker engine should be version 1.10 or higher. For installation instructions, please refer to: https://docs.docker.com/engine/installation/ +* Docker Compose needs to be version 1.6.0 or higher. For installation instructions, please refer to: https://docs.docker.com/compose/install/ -## Installing Harbor from the source code +## Installation from source code + +_Note: To install from source, the target host must be connected to the Internet!_ +The steps boil down to the following + +1. Get the source code +2. Configure **harbor.cfg** +3. **prepare** the configuration files +4. Start Harbor with Docker Compose -To install from the source, the target host must be connected to the Internet. #### Getting the source code: ```sh @@ -23,30 +30,28 @@ $ git clone https://github.com/vmware/harbor ``` #### Configuring Harbor -Before installing Harbor, you should configure the parameters in the file **harbor.cfg**. You then execute the **prepare** script to generate configuration files for Harbor's containers. Finally, you use Docker Compose to start Harbor. +Configuration parameters are located in the file **harbor.cfg**. +The parameters are described below - note that at the very least, you will need to change the **hostname** attribute. -At minimum, you need to change the **hostname** attribute in **harbor.cfg**. The description of each attribute is as follows: +* **hostname**: The target host's hostname, which is used to access the UI and the registry service. It should be the IP address or the fully qualified domain name (FQDN) of your target machine, e.g., `192.168.1.10` or `reg.yourdomain.com`. _Do NOT use `localhost` or `127.0.0.1` for the hostname - the registry service needs to be accessible by external clients!_ +* **ui_url_protocol**: (**http** or **https**. Default is **http**) The protocol used to access the UI and the token/notification service. By default, this is _http_. To set up the https protocol, refer to [Configuring Harbor with HTTPS Access](configure_https.md). +* **Email settings**: These parameters are needed for Harbor to be able to send a user a "password reset" email, and are only necessary if that functionality is needed. Also, do mnote that by default SSL connectivity is _not_ enabled - if your SMTP server requires SSL, but does _not_ support STARTTLS, then you should enable SSL by setting **email_ssl = true**. + * email_server = smtp.mydomain.com + * email_server_port = 25 + * email_username = sample_admin@mydomain.com + * email_password = abc + * email_from = admin + * email_ssl = false -**hostname**: The hostname for a user to access the user interface and the registry service. It should be the IP address or the fully qualified domain name (FQDN) of your target machine, for example 192.168.1.10 or reg.yourdomain.com . Do NOT use localhost or 127.0.0.1 for the hostname because the registry service needs to be accessed by external clients. -**ui_url_protocol**: The protocol for accessing the user interface and the token/notification service, by default it is http. To set up the https protocol, refer to [Configuring Harbor with HTTPS Access](configure_https.md). -**Email settings**: the following 6 attributes are used to send an email to reset a user's password, they are not mandatory unless the password reset function is needed in Harbor. By default SSL connection is not enabled, if your smtp server(such as exmail.qq.com) requires SSL connection and doesn't support STARTTLS, then you should enable it by set **email_ssl = true**. -* email_server = smtp.mydomain.com -* email_server_port = 25 -* email_username = sample_admin@mydomain.com -* email_password = abc -* email_from = admin -* email_ssl = false - -**harbor_admin_password**: The password for the administrator of Harbor, by default the password is Harbor12345, the user name is admin. -**auth_mode**: The authentication mode of Harbor. By default it is *db_auth*, i.e. the credentials are stored in a database. Please set it to *ldap_auth* if you want to verify user's credentials against an LDAP server. -**ldap_url**: The URL for LDAP endpoint, for example ldaps://ldap.mydomain.com. It is only used when **auth_mode** is set to *ldap_auth*. -**ldap_basedn**: The basedn template for verifying the user's credentials against LDAP, for example uid=%s,ou=people,dc=mydomain,dc=com. It is only used when **auth_mode** is set to *ldap_auth*. -**db_password**: The password of root user of mySQL database. Change this password for any production use. -**self_registration**: The flag to turn on or off the user self-registration function. If this flag is turned off, only an admin user can create new users in Harbor. The default value is on. -NOTE: When **auth_mode** is *ldap_auth*, the self-registration feature is always disabled, therefore, this flag is ignored. +* **harbor_admin_password**: The adminstrator's password. _Note that the default username/password are **admin/Harbor12345** ._ +* **auth_mode**: The type of authentication that is used. By default it is **db_auth**, i.e. the credentials are stored in a database. For LDAP authentication, set this to **ldap_auth**. +* **ldap_url**: The LDAP endpoint URL (e.g. `ldaps://ldap.mydomain.com`). _Only used when **auth_mode** is set to *ldap_auth* ._ +* **ldap_basedn**: The basedn template for verifying the user's credentials against LDAP (e.g. `uid=%s,ou=people,dc=mydomain,dc=com`). _Only used when **auth_mode** is set to *ldap_auth* ._ +* **db_password**: The root password for the mySQL database used for **db_auth**. _Change this password for any production use!!_ +* **self_registration**: (**on** or **off**. Default is **on**) Enable / Disable the ability for a user to register themselves. When disabled, new users can only be created by the Admin user, only an admin user can create new users in Harbor. _NOTE: When **auth_mode** is set to **ldap_auth**, self-registration feature is **always** disabled, and this flag is ignored. #### Building and starting Harbor -After configuring harbor.cfg, build and start Harbor by the following commands. Because it requires downloading necessary files from the Internet, it may take a while for the docker-compose process to finish. +Once **harbord.cfg** is configured, build and start Harbor as follows. Note that Note that the docker-compose process can take a while! ```sh $ cd Deploy @@ -61,29 +66,32 @@ After configuring harbor.cfg, build and start Harbor by the following commands. $ sudo docker-compose up -d ``` -If everything works fine, you can open a browser to visit the admin portal at http://reg.yourdomain.com . The default administrator username and password are admin/Harbor12345 . +_If everything worked properly, you should be able to open a browser to visit the admin portal at http://reg.yourdomain.com . Note that the default administrator username/password are admin/Harbor12345 ._ -Log in to the admin portal and create a new project, e.g. myproject. You can then use docker commands to login and push images. The default port of Harbor registry server is 80: +Log in to the admin portal and create a new project, e.g. `myproject`. You can then use docker commands to login and push images (By default, the registry server listens on port 80): ```sh $ docker login reg.yourdomain.com $ docker push reg.yourdomain.com/myproject/myrepo ``` -**NOTE:** The default installation of Harbor uses HTTP protocol, you should add the option "--insecure-registry" to your client's Docker daemon and restart Docker service. +**NOTE:** The default installation of Harbor uses _HTTP_ - as such, you will need to add the option `--insecure-registry` to your client's Docker daemon and restart the Docker service. For information on how to use Harbor, please refer to [User Guide of Harbor](user_guide.md) . #### Configuring Harbor with HTTPS Access -Because Harbor does not ship with any certificates, it uses HTTP by default to serve registry requests. This makes it relatively simple to configure, especially for a development or testing environment. However, it is highly recommended that security be enabled for any production environment. Refer to [Configuring Harbor with HTTPS Access](configure_https.md) if you want to enable HTTPS access to Harbor. +Harbor does not ship with any certificates, and, by default, uses HTTP to serve requests. While this makes it relatively simple to set-up and run - especially for a development or testing environment - it is **not** recommended for a production environment. To enable HTTPS, please refer to [Configuring Harbor with HTTPS Access](configure_https.md) -## Installing Harbor via a pre-built installation package +## Installation from a pre-built package -A pre-built installation package of each release can be downloaded from the [release page](https://github.com/vmware/harbor/releases). After downloading the package file **harbor-<version>.tgz** , extract files in the package. +Pre-built installation packages of each release are available at [release page](https://github.com/vmware/harbor/releases). +Download the package file **harbor-<version>.tgz** , and then extract the files. ``` $ tar -xzvf harbor-0.1.1.tgz $ cd harbor ``` -Then configure Harbor by following instructions in Section [Configuring Harbor](#configuring-harbor). Next, run **prepare** script to generate config files and use docker compose to build Harbor's container images and eventually spin it up. +Next, configure Harbor as described earlier in [Configuring Harbor](#configuring-harbor). + +Finally, run the **prepare** script to generate config files, and use docker compose to build / start Harbor. ``` @@ -98,11 +106,24 @@ $ sudo docker-compose up -d ...... ``` -### Deploying Harbor to a host which does not have Internet access -When you run *docker-compose up* to start Harbor, it will pull base images from Docker Hub and build new images for the containers. This process requires accessing the Internet. If you want to deploy Harbor to a host that is not connected to the Internet, you need to prepare Harbor on a machine that has access to the Internet. After that, you export the images as tgz files and transfer them to the target machine. Then load the tgz file into Docker's local image repo. +### Deploying Harbor on a host which does not have Internet access +*docker-compose up* pulls the base images from Docker Hub and builds new images for the containers, which, necessarily, requires internet access. To deploy Harbor on a host that is not connected to the Internet +1. Prepare Harbor on a machine that has access to the Internet. +2. Export the images as tgz files +3. Transfer them to the target host. +4. Load the tgz file into Docker's local image repo on the host. + +THese steps are detailed below #### Building and saving images for offline installation -On a machine that is connected to the Internet, extract files from the pre-built installation package. Then run command "docker-compose build" to build the images and use the script *save_image.sh* to export them as tar files. The tar files will be stored in *images/* directory. Next, package everything in the directory *harbor/* into a tgz file and transfer it to the target machine. This can be done by executing the following commands: +On a machine that is connected to the Internet, +1. Extract the files from the pre-built installation package. +2. Then, run `docker-compose build` to build the images. +3. Use the script `save_image.sh` to export these images as tar files. Note that the tar files will be stored in the `images/` directory. +4. Package everything in the directory `harbor/` into a tgz file +5. Transfer this tgz file to the target machine. + +The commands, in detail, are as follows ``` $ cd harbor @@ -123,10 +144,9 @@ $ cd ../ $ tar -cvzf harbor_offline-0.1.1.tgz harbor ``` -The file **harbor_offline-0.1.1.tgz** contains the images saved by previous steps and the other files required to start Harbor. -You can use tools such as scp to transfer the file **harbor_offline-0.1.1.tgz** to the target machine that does not have Internet connection. -On the target machine, you can execute the following commands to start Harbor. Again, before running the **prepare** script, -be sure to update **harbor.cfg** to reflect the right configuration of the target machine. (Refer to Section [Configuring Harbor](#configuring-harbor) .) +The file `harbor_offline-0.1.1.tgz` contains the images and other files required to start Harbor. You can use tools such as `rsync` or `scp` to transfer the this file to the target host. +On the target host, execute the following commands to start Harbor. _Note that before running the **prepare** script, you **must** update **harbor.cfg** to reflect the right configuration of the target machine!!_ (Refer to Section [Configuring Harbor](#configuring-harbor) + ``` $ tar -xzvf harbor_offline-0.1.1.tgz $ cd harbor @@ -155,9 +175,9 @@ $ sudo docker-compose up -d ``` ### Managing Harbor's lifecycle -Harbor is composed of a few containers which are deployed via docker-compose, you can use docker-compose to manage the lifecycle of the containers. Below are a few useful commands: +You can use docker-compose to manage the container lifecycle of the containers. A few useful commands are listed below: -Build and start Harbor: +*Build and start Harbor:* ``` $ sudo docker-compose up -d Creating harbor_log_1 @@ -166,7 +186,7 @@ Creating harbor_registry_1 Creating harbor_ui_1 Creating harbor_proxy_1 ``` -Stop Harbor: +*Stop Harbor:* ``` $ sudo docker-compose stop Stopping harbor_proxy_1 ... done @@ -175,7 +195,7 @@ Stopping harbor_registry_1 ... done Stopping harbor_mysql_1 ... done Stopping harbor_log_1 ... done ``` -Restart Harbor after stopping +*Restart Harbor after stopping* ``` $ sudo docker-compose start Starting harbor_log_1 @@ -184,7 +204,7 @@ Starting harbor_registry_1 Starting harbor_ui_1 Starting harbor_proxy_1 ```` -Remove Harbor's containers while keeping the image data and Harbor's database files on the file system: +*Remove Harbor's containers while keeping the image data and Harbor's database files on the file system: * ``` $ sudo docker-compose rm Going to remove harbor_proxy_1, harbor_ui_1, harbor_registry_1, harbor_mysql_1, harbor_log_1 @@ -195,19 +215,20 @@ Removing harbor_registry_1 ... done Removing harbor_mysql_1 ... done ``` -Remove Harbor's database and image data (for a clean re-installation): +*Remove Harbor's database and image data (for a clean re-installation):* ```sh $ rm -r /data/database $ rm -r /data/registry ``` -[Docker Compose command-line reference](https://docs.docker.com/compose/reference/) describes the usage information for the docker-compose subcommands. +Please check the [Docker Compose command-line reference](https://docs.docker.com/compose/reference/) for more on docker-compose ### Persistent data and log files -By default, the data of database and image files in the registry are persisted in the directory **/data/** of the target machine. When Harbor's containers are removed and recreated, the data remain unchanged. Harbor leverages rsyslog to collect the logs of each container, by default the log files are stored in the directory **/var/log/harbor/** on Harbor's host. +By default, registry data is persisted in the target host's `/data/` of directory. This data remains unchanged even when Harbor's containers are removed and/or recreated. +In addition, Harbor users `rsyslog` to collect the logs of each container. By default, these log files are stored in the directory `/var/log/harbor/` on the target host. ##Troubleshooting -1.When setting up Harbor behind another nginx proxy or elastic load balancing, remove the below line if the proxy already has similar settings. Be sure to edit Deploy/config/nginx/nginx.conf and remove the line under these 3 sections: "location /", "location /v2/" and "location /service/". +1.When setting up Harbor behind an nginx proxy or elastic load balancing, look for the line below, in `Deploy/config/nginx/nginx.conf` and remove it from the sections: `location /`, `location /v2/` and `location /service/`. ``` proxy_set_header X-Forwarded-Proto $scheme; ``` diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 20307cc49..38d7da892 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -574,7 +574,7 @@ paths: description: Retrieved tags from a relevant repository successfully. 500: description: Unexpected internal errors. - /repositories/manifest: + /repositories/manifests: get: summary: Get manifests of a relevant repository. description: | diff --git a/migration/Dockerfile b/migration/Dockerfile index 507342170..5579bd95f 100644 --- a/migration/Dockerfile +++ b/migration/Dockerfile @@ -16,8 +16,6 @@ WORKDIR /harbor-migration COPY ./ ./ -COPY ./migration.cfg ./ - RUN ./prepare.sh ENTRYPOINT ["./run.sh"] diff --git a/migration/README.md b/migration/README.md index 1d013e47f..dde8e00c9 100644 --- a/migration/README.md +++ b/migration/README.md @@ -16,6 +16,10 @@ Migration is a module for migrating database schema between different version of - 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` @@ -33,15 +37,14 @@ Migration is a module for migrating database schema between different version of ```docker run -ti -v /data/database:/var/lib/mysql your-image-name up head``` -- perform database schema downgrade(downgrade has been disabled) - - ```docker run -v /data/database:/var/lib/mysql your-image-name down base``` +you can use `-v /etc/localtime:/etc/localtime` to sync container timezone with host timezone. +you may change `/data/database` to the mysql volumes path you set in docker-compose.yml. ###migration step - step 1: stop and remove harbor service ``` - docker-compose stop && docker-compose rm -f + docker-compose down ``` - step 2: perform migration operation - step 3: rebuild newest harbor images and restart service diff --git a/migration/db_meta.py b/migration/db_meta.py new file mode 100644 index 000000000..e20dd924c --- /dev/null +++ b/migration/db_meta.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import sqlalchemy as sa +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker, relationship + +Base = declarative_base() + +class User(Base): + __tablename__ = 'user' + + user_id = sa.Column(sa.Integer, primary_key=True) + username = sa.Column(sa.String(15), unique=True) + email = sa.Column(sa.String(30), unique=True) + password = sa.Column(sa.String(40), nullable=False) + realname = sa.Column(sa.String(20), nullable=False) + comment = sa.Column(sa.String(30)) + deleted = sa.Column(sa.Integer, nullable=False, server_default=sa.text("'0'")) + 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) + +class Properties(Base): + __tablename__ = 'properties' + + k = sa.Column(sa.String(64), primary_key = True) + v = sa.Column(sa.String(128), nullable = False) + +class ProjectMember(Base): + __tablename__ = 'project_member' + + 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) + sa.ForeignKeyConstraint(['project_id'], [u'project.project_id'], ), + sa.ForeignKeyConstraint(['role'], [u'role.role_id'], ), + sa.ForeignKeyConstraint(['user_id'], [u'user.user_id'], ), + +class UserProjectRole(Base): + __tablename__ = 'user_project_role' + + upr_id = sa.Column(sa.Integer(), primary_key = True) + user_id = sa.Column(sa.Integer(), sa.ForeignKey('user.user_id')) + pr_id = sa.Column(sa.Integer(), sa.ForeignKey('project_role.pr_id')) + project_role = relationship("ProjectRole") + +class ProjectRole(Base): + __tablename__ = 'project_role' + + pr_id = sa.Column(sa.Integer(), primary_key = True) + project_id = sa.Column(sa.Integer(), nullable = False) + role_id = sa.Column(sa.Integer(), nullable = False) + sa.ForeignKeyConstraint(['role_id'], [u'role.role_id']) + sa.ForeignKeyConstraint(['project_id'], [u'project.project_id']) + +class Access(Base): + __tablename__ = 'access' + + access_id = sa.Column(sa.Integer(), primary_key = True) + access_code = sa.Column(sa.String(1)) + comment = sa.Column(sa.String(30)) + +class Role(Base): + __tablename__ = 'role' + + role_id = sa.Column(sa.Integer, primary_key=True) + role_mask = sa.Column(sa.Integer, nullable=False, server_default=sa.text("'0'")) + role_code = sa.Column(sa.String(20)) + name = sa.Column(sa.String(20)) + +class Project(Base): + __tablename__ = 'project' + + 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) + deleted = sa.Column(sa.Integer, nullable=False, server_default=sa.text("'0'")) + public = sa.Column(sa.Integer, nullable=False, server_default=sa.text("'0'")) + owner = relationship(u'User') diff --git a/migration/migration_harbor/versions/0_1_1.py b/migration/migration_harbor/versions/0_1_1.py index 0f21b5436..746446f39 100644 --- a/migration/migration_harbor/versions/0_1_1.py +++ b/migration/migration_harbor/versions/0_1_1.py @@ -27,58 +27,11 @@ branch_labels = None depends_on = None from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import mysql -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker, relationship from datetime import datetime +from db_meta import * Session = sessionmaker() -Base = declarative_base() - -class Properties(Base): - __tablename__ = 'properties' - - k = sa.Column(sa.String(64), primary_key = True) - v = sa.Column(sa.String(128), nullable = False) - -class ProjectMember(Base): - __tablename__ = 'project_member' - - 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) - sa.ForeignKeyConstraint(['project_id'], [u'project.project_id'], ), - sa.ForeignKeyConstraint(['role'], [u'role.role_id'], ), - sa.ForeignKeyConstraint(['user_id'], [u'user.user_id'], ), - -class UserProjectRole(Base): - __tablename__ = 'user_project_role' - - upr_id = sa.Column(sa.Integer(), primary_key = True) - user_id = sa.Column(sa.Integer(), sa.ForeignKey('user.user_id')) - pr_id = sa.Column(sa.Integer(), sa.ForeignKey('project_role.pr_id')) - project_role = relationship("ProjectRole") - -class ProjectRole(Base): - __tablename__ = 'project_role' - - pr_id = sa.Column(sa.Integer(), primary_key = True) - project_id = sa.Column(sa.Integer(), nullable = False) - role_id = sa.Column(sa.Integer(), nullable = False) - sa.ForeignKeyConstraint(['role_id'], [u'role.role_id']) - sa.ForeignKeyConstraint(['project_id'], [u'project.project_id']) - -class Access(Base): - __tablename__ = 'access' - - access_id = sa.Column(sa.Integer(), primary_key = True) - access_code = sa.Column(sa.String(1)) - comment = sa.Column(sa.String(30)) - def upgrade(): """ update schema&data @@ -86,39 +39,60 @@ def upgrade(): bind = op.get_bind() session = Session(bind=bind) - #delete M from table access - acc = session.query(Access).filter_by(access_id=1).first() - session.delete(acc) - #create table property Properties.__table__.create(bind) 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('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()}) + + #init all sysadmin_flag = 0 + session.query(User).update({User.sysadmin_flag: 0}) + #create table project_member ProjectMember.__table__.create(bind) - #fill data + #fill data into project_member and user join_result = session.query(UserProjectRole).join(UserProjectRole.project_role).all() 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())) + #update sysadmin_flag + sys_admin_result = session.query(UserProjectRole).\ + join(UserProjectRole.project_role).filter(ProjectRole.role_id ==1).all() + for result in sys_admin_result: + session.query(User).filter(User.user_id == result.user_id).update({User.sysadmin_flag: 1}) + + #add column to table role + op.add_column('role', sa.Column('role_mask', sa.Integer(), server_default=sa.text(u"'0'"), nullable=False)) + #drop user_project_role table before drop project_role #because foreign key constraint op.drop_table('user_project_role') op.drop_table('project_role') + #delete sysadmin from table role + role = session.query(Role).filter_by(role_id=1).first() + session.delete(role) + session.query(Role).update({Role.role_id: Role.role_id - 1}) + + #delete M from table access + acc = session.query(Access).filter_by(access_id=1).first() + 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 role - op.add_column('role', sa.Column('role_mask', sa.Integer(), server_default=sa.text(u"'0'"), nullable=False)) - - #add column to table user - op.add_column('user', sa.Column('creation_time', sa.DateTime(), 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 project + session.query(Project).update({Project.update_time: datetime.now()}) session.commit() def downgrade(): diff --git a/migration/run.sh b/migration/run.sh index 806378ff5..d3a5a5b8e 100755 --- a/migration/run.sh +++ b/migration/run.sh @@ -1,5 +1,7 @@ #!/bin/bash +export PYTHONPATH=$PYTHONPATH:/harbor-migration + source ./migration.cfg WAITTIME=60 @@ -14,6 +16,7 @@ if [[ $1 = "help" || $1 = "h" || $# = 0 ]]; then echo "backup perform database backup" echo "restore perform database restore" echo "up, upgrade perform database schema upgrade" + echo "test test database connection" echo "h, help usage help" exit 0 fi @@ -36,7 +39,7 @@ fi echo 'Trying to start mysql server...' DBRUN=0 -nohup mysqld 2>&1 > ./nohup.log& +nohup mysqld 2>&1 > ./mysqld.log& for i in $(seq 1 $WAITTIME); do echo "$(/usr/sbin/service mysql status)" if [[ "$(/usr/sbin/service mysql status)" =~ "not running" ]]; then @@ -47,11 +50,19 @@ for i in $(seq 1 $WAITTIME); do fi done -if [[ $DBRUN -eq 0 ]]; then +if [[ $DBRUN -eq 0 ]]; then echo "timeout. Can't run mysql server." + if [[ $1 = "test" ]]; then + echo "test failed." + fi exit 1 fi +if [[ $1 = "test" ]]; then + echo "test passed." + exit 0 +fi + key="$1" case $key in up|upgrade) diff --git a/static/i18n/locale_de-DE.ini b/static/i18n/locale_de-DE.ini index 67a926c72..79d08dded 100644 --- a/static/i18n/locale_de-DE.ini +++ b/static/i18n/locale_de-DE.ini @@ -74,6 +74,7 @@ language = Deutsch language_en-US = English language_zh-CN = 中文 language_de-DE = Deutsch +language_ru-RU = Русский copyright = Copyright all_rights_reserved = Alle Rechte vorbehalten. index_desc = Project Harbor ist ein zuverlässiger Enterprise-Class Registry Server. Unternehmen können ihren eigenen Registry Server aufsetzen um die Produktivität und Sicherheit zu erhöhen. Project Harbor kann für Entwicklungs- wie auch Produktiv-Umgebungen genutzt werden. diff --git a/static/i18n/locale_en-US.ini b/static/i18n/locale_en-US.ini index 30f7009e0..414e6c647 100644 --- a/static/i18n/locale_en-US.ini +++ b/static/i18n/locale_en-US.ini @@ -75,6 +75,7 @@ language = English language_en-US = English language_zh-CN = 中文 language_de-DE = Deutsch +language_ru-RU = Русский copyright = Copyright all_rights_reserved = All rights reserved. index_desc = Project Harbor is to build an enterprise-class, reliable registry server. Enterprises can set up a private registry server in their own environment to improve productivity as well as security. Project Harbor can be used in both development and production environment. diff --git a/static/i18n/locale_messages.js b/static/i18n/locale_messages.js index d2eb902a4..6ba513b7b 100644 --- a/static/i18n/locale_messages.js +++ b/static/i18n/locale_messages.js @@ -16,316 +16,379 @@ var global_messages = { "username_is_required" : { "en-US": "Username is required.", "zh-CN": "用户名为必填项。", - "de-DE": "Benutzername erforderlich." + "de-DE": "Benutzername erforderlich.", + "ru-RU": "Требуется ввести имя пользователя." }, "username_has_been_taken" : { "en-US": "Username has been taken.", "zh-CN": "用户名已被占用。", - "de-DE": "Benutzername bereits vergeben." + "de-DE": "Benutzername bereits vergeben.", + "ru-RU": "Имя пользователя уже используется." }, "username_is_too_long" : { "en-US": "Username is too long. (maximum 20 characters)", "zh-CN": "用户名长度超出限制。(最长为20个字符)", - "de-DE": "Benutzername ist zu lang. (maximal 20 Zeichen)" + "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": "用户名包含不合法的字符。", - "de-DE": "Benutzername enthält ungültige Zeichen." + "de-DE": "Benutzername enthält ungültige Zeichen.", + "ru-RU": "Имя пользователя содержит недопустимые символы." }, "email_is_required" : { "en-US": "Email is required.", "zh-CN": "邮箱为必填项。", - "de-DE": "E-Mail Adresse erforderlich." + "de-DE": "E-Mail Adresse erforderlich.", + "ru-RU": "Требуется ввести E-mail адрес." }, "email_contains_illegal_chars" : { "en-US": "Email contains illegal character(s).", "zh-CN": "邮箱包含不合法的字符。", - "de-DE": "E-Mail Adresse enthält ungültige Zeichen." + "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": "邮箱已被占用。", - "de-DE": "E-Mail Adresse wird bereits verwendet." + "de-DE": "E-Mail Adresse wird bereits verwendet.", + "ru-RU": "Такой E-mail адрес уже используется." }, "email_content_illegal" : { "en-US": "Email format is illegal.", "zh-CN": "邮箱格式不合法。", - "de-DE": "Format der E-Mail Adresse ist ungültig." + "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": "邮箱不存在。", - "de-DE": "E-Mail Adresse existiert nicht." + "de-DE": "E-Mail Adresse existiert nicht.", + "ru-RU": "E-mail адрес не существует." }, "realname_is_required" : { "en-US": "Full name is required.", "zh-CN": "全名为必填项。", - "de-DE": "Vollständiger Name erforderlich." + "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个字符)", - "de-DE": "Vollständiger Name zu lang. (maximal 20 Zeichen)" + "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": "全名包含不合法的字符。", - "de-DE": "Vollständiger Name enthält ungültige Zeichen." + "de-DE": "Vollständiger Name enthält ungültige Zeichen.", + "ru-RU": "Полное имя содержит недопустимые символы." }, "password_is_required" : { "en-US": "Password is required.", "zh-CN": "密码为必填项。", - "de-DE": "Passwort erforderlich." + "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个数字。", - "de-DE": "Passwort ungültig. Mindestens sieben Zeichen bestehend aus einem Kleinbuchstaben, einem Großbuchstaben und einer Zahl" + "de-DE": "Passwort ungültig. Mindestens sieben Zeichen bestehend aus einem Kleinbuchstaben, einem Großbuchstaben und einer Zahl", + "ru-RU": "Такой пароль недопустим. Парольл должен содержать Минимум 7 символов, в которых будет присутствовать по меньшей мере 1 буква нижнего регистра, 1 буква верхнего регистра и 1 цифра" }, "password_is_too_long" : { "en-US": "Password is too long. (maximum 20 characters)", "zh-CN": "密码长度超出限制。(最长为20个字符)", - "de-DE": "Passwort zu lang. (maximal 20 Zeichen)" + "de-DE": "Passwort zu lang. (maximal 20 Zeichen)", + "ru-RU": "Пароль слишком длинный (максимум 20 символов)" }, "password_does_not_match" : { "en-US": "Passwords do not match.", "zh-CN": "两次密码输入不一致。", - "de-DE": "Passwörter stimmen nicht überein." + "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个字符)", - "de-DE": "Kommentar zu lang. (maximal 20 Zeichen)" + "de-DE": "Kommentar zu lang. (maximal 20 Zeichen)", + "ru-RU": "Комментарий слишком длинный. (максимум 20 символов)" }, "comment_contains_illegal_chars" : { "en-US": "Comment contains illegal character(s).", "zh-CN": "备注包含不合法的字符。", - "de-DE": "Kommentar enthält ungültige Zeichen." + "de-DE": "Kommentar enthält ungültige Zeichen.", + "ru-RU": "Комментарий содержит недопустимые символы." }, "project_name_is_required" : { "en-US": "Project name is required.", "zh-CN": "项目名称为必填项。", - "de-DE": "Projektname erforderlich." + "de-DE": "Projektname erforderlich.", + "ru-RU": "Необходимо ввести название Проекта." }, "project_name_is_too_short" : { "en-US": "Project name is too short. (minimum 4 characters)", "zh-CN": "项目名称至少要求 4个字符。", - "de-DE": "Projektname zu kurz. (mindestens 4 Zeichen)" + "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个字符)", - "de-DE": "Projektname zu lang. (maximal 30 Zeichen)" + "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": "项目名称包含不合法的字符。", - "de-DE": "Projektname enthält ungültige Zeichen." + "de-DE": "Projektname enthält ungültige Zeichen.", + "ru-RU": "Название проекта содержит недопустимые символы." }, "project_exists" : { "en-US": "Project exists.", "zh-CN": "项目已存在。", - "de-DE": "Projekt existiert bereits." + "de-DE": "Projekt existiert bereits.", + "ru-RU": "Такой проект уже существует." }, "delete_user" : { "en-US": "Delete User", "zh-CN": "删除用户", - "de-DE": "Benutzer löschen" + "de-DE": "Benutzer löschen", + "ru-RU": "Удалить пользователя" }, "are_you_sure_to_delete_user" : { "en-US": "Are you sure to delete ", "zh-CN": "确认要删除用户 ", - "de-DE": "Sind Sie sich sicher, dass Sie folgenden Benutzer löschen möchten: " + "de-DE": "Sind Sie sich sicher, dass Sie folgenden Benutzer löschen möchten: ", + "ru-RU": "Вы уверены что хотите удалить пользователя? " }, "input_your_username_and_password" : { "en-US": "Please input your username and password.", "zh-CN": "请输入用户名和密码。", - "de-DE": "Bitte geben Sie ihr Benutzername und Passwort ein." + "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": "请输入正确的用户名或密码。", - "de-DE": "Bitte überprüfen Sie ihren Benutzernamen und Passwort." + "de-DE": "Bitte überprüfen Sie ihren Benutzernamen und Passwort.", + "ru-RU": "Проверьте свои имя пользователя и пароль." }, "title_login_failed" : { "en-US": "Login Failed", "zh-CN": "登录失败", - "de-DE": "Anmeldung fehlgeschlagen" + "de-DE": "Anmeldung fehlgeschlagen", + "ru-RU": "Ошибка входа" }, "title_change_password" : { "en-US": "Change Password", "zh-CN": "修改密码", - "de-DE": "Passwort ändern" + "de-DE": "Passwort ändern", + "ru-RU": "Сменить пароль" }, "change_password_successfully" : { "en-US": "Password changed successfully.", "zh-CN": "密码已修改。", - "de-DE": "Passwort erfolgreich geändert." + "de-DE": "Passwort erfolgreich geändert.", + "ru-RU": "Пароль успешно изменен." }, "title_forgot_password" : { "en-US": "Forgot Password", "zh-CN": "忘记密码", - "de-DE": "Passwort vergessen" + "de-DE": "Passwort vergessen", + "ru-RU": "Забыли пароль?" }, "email_has_been_sent" : { "en-US": "Email for resetting password has been sent.", "zh-CN": "重置密码邮件已发送。", - "de-DE": "Eine E-Mail mit einem Wiederherstellungslink wurde an Sie gesendet." + "de-DE": "Eine E-Mail mit einem Wiederherstellungslink wurde an Sie gesendet.", + "ru-RU": "На ваш E-mail было выслано письмо с инструкциями по сбросу пароля." }, "send_email_failed" : { "en-US": "Failed to send Email for resetting password.", "zh-CN": "重置密码邮件发送失败。", - "de-DE": "Fehler beim Senden der Wiederherstellungs-E-Mail." + "de-DE": "Fehler beim Senden der Wiederherstellungs-E-Mail.", + "ru-RU": "Ошибка отправки сообщения." }, "please_login_first" : { "en-US": "Please login first.", "zh-CN": "请先登录。", - "de-DE": "Bitte melden Sie sich zuerst an." + "de-DE": "Bitte melden Sie sich zuerst an.", + "ru-RU": "Сначала выполните вход в систему." }, "old_password_is_not_correct" : { "en-US": "Old password is not correct.", "zh-CN": "原密码输入不正确。", - "de-DE": "Altes Passwort ist nicht korrekt." + "de-DE": "Altes Passwort ist nicht korrekt.", + "ru-RU": "Старый пароль введен неверно." }, "please_input_new_password" : { "en-US": "Please input new password.", "zh-CN": "请输入新密码。", - "de-DE": "Bitte geben Sie ihr neues Passwort ein." + "de-DE": "Bitte geben Sie ihr neues Passwort ein.", + "ru-RU": "Пожалуйста, введите новый пароль." }, "invalid_reset_url": { "en-US": "Invalid URL for resetting password.", "zh-CN": "无效密码重置链接。", - "de-DE": "Ungültige URL zum Passwort wiederherstellen." + "de-DE": "Ungültige URL zum Passwort wiederherstellen.", + "ru-RU": "Неверный URL для сброса пароля." }, "reset_password_successfully" : { "en-US": "Reset password successfully.", "zh-CN": "密码重置成功。", - "de-DE": "Passwort erfolgreich wiederhergestellt." + "de-DE": "Passwort erfolgreich wiederhergestellt.", + "ru-RU": "Пароль успешно сброшен." }, "internal_error": { "en-US": "Internal error.", "zh-CN": "内部错误,请联系系统管理员。", - "de-DE": "Interner Fehler." + "de-DE": "Interner Fehler.", + "ru-RU": "Внутренняя ошибка." }, "title_reset_password" : { "en-US": "Reset Password", "zh-CN": "重置密码", - "de-DE": "Passwort zurücksetzen" + "de-DE": "Passwort zurücksetzen", + "ru-RU": "Сбросить пароль" }, "title_sign_up" : { "en-US": "Sign Up", "zh-CN": "注册", - "de-DE": "Registrieren" + "de-DE": "Registrieren", + "ru-RU": "Регистрация" }, "title_add_user": { "en-US": "Add User", "zh-CN": "新增用户", - "de-DE": "Benutzer hinzufügen" + "de-DE": "Benutzer hinzufügen", + "ru-RU": "Добавить пользователя" }, "registered_successfully": { "en-US": "Signed up successfully.", "zh-CN": "注册成功。", - "de-DE": "Erfolgreich registriert." + "de-DE": "Erfolgreich registriert.", + "ru-RU": "Регистрация прошла успешно." }, "registered_failed" : { "en-US": "Failed to sign up.", "zh-CN": "注册失败。", - "de-DE": "Registrierung fehlgeschlagen." + "de-DE": "Registrierung fehlgeschlagen.", + "ru-RU": "Ошибка регистрации." }, "added_user_successfully": { "en-US": "Added user successfully.", "zh-CN": "新增用户成功。", - "de-DE": "Benutzer erfolgreich erstellt." + "de-DE": "Benutzer erfolgreich erstellt.", + "ru-RU": "Пользователь успешно добавлен." }, "added_user_failed": { "en-US": "Adding user failed.", "zh-CN": "新增用户失败。", - "de-DE": "Benutzer erstellen fehlgeschlagen." + "de-DE": "Benutzer erstellen fehlgeschlagen.", + "ru-RU": "Ошибка добавления пользователя." }, "projects": { "en-US": "Projects", "zh-CN": "项目", - "de-DE": "Projekte" + "de-DE": "Projekte", + "ru-RU": "Проекты" }, "repositories" : { "en-US": "Repositories", "zh-CN": "镜像仓库", - "de-DE": "Repositories" + "de-DE": "Repositories", + "ru-RU": "Репозитории" }, "no_repo_exists" : { "en-US": "No repositories found, please use 'docker push' to upload images.", "zh-CN": "未发现镜像,请用‘docker push’命令上传镜像。", - "de-DE": "Keine Repositories gefunden, bitte benutzen Sie 'docker push' um ein Image hochzuladen." + "de-DE": "Keine Repositories gefunden, bitte benutzen Sie 'docker push' um ein Image hochzuladen.", + "ru-RU": "Репозитории не найдены, используйте команду 'docker push' для добавления образов." }, "tag" : { "en-US": "Tag", "zh-CN": "标签", - "de-DE": "Tag" + "de-DE": "Tag", + "ru-RU": "Метка" }, "pull_command": { "en-US": "Pull Command", "zh-CN": "Pull 命令", - "de-DE": "Pull Befehl" + "de-DE": "Pull Befehl", + "ru-RU": "Команда для скачивания образа" }, "image_details" : { "en-US": "Image Details", "zh-CN": "镜像详细信息", - "de-DE": "Image Details" + "de-DE": "Image Details", + "ru-RU": "Информация об образе" }, "add_members" : { "en-US": "Add Member", "zh-CN": "添加成员", - "de-DE": "Mitglied hinzufügen" + "de-DE": "Mitglied hinzufügen", + "ru-RU": "Добавить Участника" }, "edit_members" : { "en-US": "Edit Members", "zh-CN": "编辑成员", - "de-DE": "Mitglieder bearbeiten" + "de-DE": "Mitglieder bearbeiten", + "ru-RU": "Редактировать Участников" }, "add_member_failed" : { "en-US": "Adding Member Failed", "zh-CN": "添加成员失败", - "de-DE": "Mitglied hinzufügen fehlgeschlagen" + "de-DE": "Mitglied hinzufügen fehlgeschlagen", + "ru-RU": "Ошибка при добавлении нового участника" }, "please_input_username" : { "en-US": "Please input a username.", "zh-CN": "请输入用户名。", - "de-DE": "Bitte geben Sie einen Benutzernamen ein." + "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": "请为用户分配角色。", - "de-DE": "Bitte weisen Sie dem Benutzer eine Rolle zu." + "de-DE": "Bitte weisen Sie dem Benutzer eine Rolle zu.", + "ru-RU": "Пожалуйста, назначьте роль пользователю." }, "user_id_exists" : { "en-US": "User is already a member.", "zh-CN": "用户已经是成员。", - "de-DE": "Benutzer ist bereits Mitglied." + "de-DE": "Benutzer ist bereits Mitglied.", + "ru-RU": "Пользователь уже является участником." }, "user_id_does_not_exist" : { "en-US": "User does not exist.", "zh-CN": "不存在此用户。", - "de-DE": "Benutzer existiert nicht." + "de-DE": "Benutzer existiert nicht.", + "ru-RU": "Пользователя с таким именем не существует." }, "insufficient_privileges" : { "en-US": "Insufficient privileges.", "zh-CN": "权限不足。", - "de-DE": "Unzureichende Berechtigungen." + "de-DE": "Unzureichende Berechtigungen.", + "ru-RU": "Недостаточно прав." }, "operation_failed" : { "en-US": "Operation Failed", "zh-CN": "操作失败", - "de-DE": "Befehl fehlgeschlagen" + "de-DE": "Befehl fehlgeschlagen", + "ru-RU": "Ошибка при выполнении данной операции" }, "button_on" : { "en-US": "On", "zh-CN": "打开", - "de-DE": "An" + "de-DE": "An", + "ru-RU": "Вкл." }, "button_off" : { "en-US": "Off", "zh-CN": "关闭", - "de-DE": "Aus" + "de-DE": "Aus", + "ru-RU": "Откл." } }; diff --git a/static/i18n/locale_ru-RU.ini b/static/i18n/locale_ru-RU.ini new file mode 100644 index 000000000..731584abb --- /dev/null +++ b/static/i18n/locale_ru-RU.ini @@ -0,0 +1,88 @@ +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 = Логин/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 = Email +system_admin = Системный администратор +dlg_button_ok = OK +dlg_button_cancel = Отмена +registration = Регистрация +username_description = Ваше имя пользователя. +email_description = Email адрес, который будет использоваться для сброса пароля. +full_name = Полное Имя +full_name_description = Имя и Фамилия. +password_description = Минимум 7 символов, в которых будет присутствовать по меньшей мере 1 буква нижнего регистра, 1 буква верхнего регистра и 1 цифра. +confirm_password = Подтвердить Пароль +note_to_the_admin = Комментарии +old_password = Старый Пароль +new_password = Новый Пароль +forgot_password_description = Введите Email, который вы использовали для регистрации, вам будет выслано письмо для сброса пароля. + +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 = Русский +copyright = Copyright +all_rights_reserved = Все права защищены. +index_desc = Проект Harbor представляет собой надежный сервер управления docker-образами корпоративного класса. Компании могут использовать данный сервер в своей инфарструктуе для повышения производительности и безопасности . Проект Harbor может использоваться как в среде разработки так и в продуктивной среде. +index_desc_0 = Основные преимущества данного решения: +index_desc_1 = 1. Безопасность: Хранение интеллектуальной собственности внутри организации. +index_desc_2 = 2. Эффективность: сервер хранения docker образов устанавливается в рамках внутренней сети организации, и может значительно сократить расход Интернет траффика +index_desc_3 = 3. Управление доступом: реализована модель RBAC (Ролевая модель управление доступом). Управление пользователями может быть интегрировано с существующими корпоративными сервисами идентификациями такими как AD/LDAP. +index_desc_4 = 4. Аудит: Любой доступ к хранилищу логируется и может быть использован для последующего анализа. +index_desc_5 = 5. GUI-интерфейс: удобная, единая консоль управления. +index_title = Сервер управления docker-образами корпоративного класса diff --git a/static/i18n/locale_zh-CN.ini b/static/i18n/locale_zh-CN.ini index 7f92b463f..69c34886b 100644 --- a/static/i18n/locale_zh-CN.ini +++ b/static/i18n/locale_zh-CN.ini @@ -75,6 +75,7 @@ language = 中文 language_en-US = English language_zh-CN = 中文 language_de-DE = Deutsch +language_ru-RU = Русский copyright = 版权所有 all_rights_reserved = 保留所有权利。 index_desc = Harbor是可靠的企业级Registry服务器。企业用户可使用Harbor搭建私有容器Registry服务,提高生产效率和安全度,既可应用于生产环境,也可以在开发环境中使用。 diff --git a/static/resources/js/common.js b/static/resources/js/common.js index 3b63c7f0f..7c7397d6b 100644 --- a/static/resources/js/common.js +++ b/static/resources/js/common.js @@ -69,7 +69,8 @@ AjaxUtil.prototype.exec = function(){ var SUPPORT_LANGUAGES = { "en-US": "English", "zh-CN": "Chinese", - "de-DE": "German" + "de-DE": "German", + "ru-RU": "Russian" }; var DEFAULT_LANGUAGE = "en-US"; diff --git a/utils/registry/auth/authorizer.go b/utils/registry/auth/authorizer.go index cea731246..26ea177a5 100644 --- a/utils/registry/auth/authorizer.go +++ b/utils/registry/auth/authorizer.go @@ -46,8 +46,8 @@ func NewRequestAuthorizer(handlers []Handler, challenges []au.Challenge) *Reques // ModifyRequest adds authorization to the request func (r *RequestAuthorizer) ModifyRequest(req *http.Request) error { - for _, handler := range r.handlers { - for _, challenge := range r.challenges { + for _, challenge := range r.challenges { + for _, handler := range r.handlers { if handler.Scheme() == challenge.Scheme { if err := handler.AuthorizeRequest(req, challenge.Parameters); err != nil { return err diff --git a/utils/registry/auth/tokenhandler.go b/utils/registry/auth/tokenhandler.go index f546bac0c..9d075b25d 100644 --- a/utils/registry/auth/tokenhandler.go +++ b/utils/registry/auth/tokenhandler.go @@ -22,6 +22,7 @@ import ( "net/http" "net/url" "strings" + "sync" "time" token_util "github.com/vmware/harbor/service/token" @@ -48,6 +49,7 @@ type tokenHandler struct { cache string // cached token expiresIn int // The duration in seconds since the token was issued that it will remain valid issuedAt *time.Time // The RFC3339-serialized UTC standard time at which a given token was issued + sync.Mutex } // Scheme returns the scheme that the handler can handle @@ -77,8 +79,10 @@ func (t *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]str expired := true - if t.expiresIn != 0 && t.issuedAt != nil { - expired = t.issuedAt.Add(time.Duration(t.expiresIn) * time.Second).Before(time.Now().UTC()) + cachedToken, cachedExpiredIn, cachedIssuedAt := t.getCachedToken() + + if len(cachedToken) != 0 && cachedExpiredIn != 0 && cachedIssuedAt != nil { + expired = cachedIssuedAt.Add(time.Duration(cachedExpiredIn) * time.Second).Before(time.Now().UTC()) } if expired || hasFrom { @@ -93,13 +97,11 @@ func (t *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]str token = to if !hasFrom { - t.cache = token - t.expiresIn = expiresIn - t.issuedAt = issuedAt + t.updateCachedToken(to, expiresIn, issuedAt) log.Debug("add token to cache") } } else { - token = t.cache + token = cachedToken log.Debug("get token from cache") } @@ -109,6 +111,20 @@ func (t *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]str return nil } +func (t *tokenHandler) getCachedToken() (string, int, *time.Time) { + t.Lock() + defer t.Unlock() + return t.cache, t.expiresIn, t.issuedAt +} + +func (t *tokenHandler) updateCachedToken(token string, expiresIn int, issuedAt *time.Time) { + t.Lock() + defer t.Unlock() + t.cache = token + t.expiresIn = expiresIn + t.issuedAt = issuedAt +} + // Implements interface Handler type standardTokenHandler struct { tokenHandler @@ -168,6 +184,7 @@ func (s *standardTokenHandler) generateToken(realm, service string, scopes []str if resp.StatusCode != http.StatusOK { err = registry_errors.Error{ StatusCode: resp.StatusCode, + StatusText: resp.Status, Message: string(b), } return diff --git a/utils/registry/errors/error.go b/utils/registry/errors/error.go index 60b8d6ce5..7a1311b00 100644 --- a/utils/registry/errors/error.go +++ b/utils/registry/errors/error.go @@ -23,12 +23,13 @@ import ( // an Error instance will be returned type Error struct { StatusCode int + StatusText string Message string } // Error ... func (e Error) Error() string { - return fmt.Sprintf("%d %s", e.StatusCode, e.Message) + return fmt.Sprintf("%d %s %s", e.StatusCode, e.StatusText, e.Message) } // ParseError parses err, if err is type Error, convert it to Error diff --git a/utils/registry/registry.go b/utils/registry/registry.go index 1ee01892e..baaf70e91 100644 --- a/utils/registry/registry.go +++ b/utils/registry/registry.go @@ -89,6 +89,10 @@ func (r *Registry) Catalog() ([]string, error) { resp, err := r.client.Do(req) if err != nil { + ok, e := isUnauthorizedError(err) + if ok { + return repos, e + } return repos, err } @@ -115,6 +119,7 @@ func (r *Registry) Catalog() ([]string, error) { return repos, errors.Error{ StatusCode: resp.StatusCode, + StatusText: resp.Status, Message: string(b), } } diff --git a/utils/registry/repository.go b/utils/registry/repository.go index 507634415..ac49e04f8 100644 --- a/utils/registry/repository.go +++ b/utils/registry/repository.go @@ -72,6 +72,9 @@ func NewRepositoryWithCredential(name, endpoint string, credential auth.Credenti } client, err := newClient(endpoint, "", credential, "repository", name, "pull", "push") + if err != nil { + return nil, err + } repository := &Repository{ Name: name, @@ -108,6 +111,17 @@ func NewRepositoryWithUsername(name, endpoint, username string) (*Repository, er return repository, nil } +// try to convert err to errors.Error if it is +func isUnauthorizedError(err error) (bool, error) { + if strings.Contains(err.Error(), http.StatusText(http.StatusUnauthorized)) { + return true, errors.Error{ + StatusCode: http.StatusUnauthorized, + StatusText: http.StatusText(http.StatusUnauthorized), + } + } + return false, err +} + // ListTag ... func (r *Repository) ListTag() ([]string, error) { tags := []string{} @@ -118,6 +132,10 @@ func (r *Repository) ListTag() ([]string, error) { resp, err := r.client.Do(req) if err != nil { + ok, e := isUnauthorizedError(err) + if ok { + return tags, e + } return tags, err } @@ -141,9 +159,9 @@ func (r *Repository) ListTag() ([]string, error) { return tags, nil } - return tags, errors.Error{ StatusCode: resp.StatusCode, + StatusText: resp.Status, Message: string(b), } @@ -161,6 +179,11 @@ func (r *Repository) ManifestExist(reference string) (digest string, exist bool, resp, err := r.client.Do(req) if err != nil { + ok, e := isUnauthorizedError(err) + if ok { + err = e + return + } return } @@ -183,6 +206,7 @@ func (r *Repository) ManifestExist(reference string) (digest string, exist bool, err = errors.Error{ StatusCode: resp.StatusCode, + StatusText: resp.Status, Message: string(b), } return @@ -201,6 +225,11 @@ func (r *Repository) PullManifest(reference string, acceptMediaTypes []string) ( resp, err := r.client.Do(req) if err != nil { + ok, e := isUnauthorizedError(err) + if ok { + err = e + return + } return } @@ -219,6 +248,7 @@ func (r *Repository) PullManifest(reference string, acceptMediaTypes []string) ( err = errors.Error{ StatusCode: resp.StatusCode, + StatusText: resp.Status, Message: string(b), } @@ -236,6 +266,11 @@ func (r *Repository) PushManifest(reference, mediaType string, payload []byte) ( resp, err := r.client.Do(req) if err != nil { + ok, e := isUnauthorizedError(err) + if ok { + err = e + return + } return } @@ -253,6 +288,7 @@ func (r *Repository) PushManifest(reference, mediaType string, payload []byte) ( err = errors.Error{ StatusCode: resp.StatusCode, + StatusText: resp.Status, Message: string(b), } @@ -268,6 +304,10 @@ func (r *Repository) DeleteManifest(digest string) error { resp, err := r.client.Do(req) if err != nil { + ok, e := isUnauthorizedError(err) + if ok { + return e + } return err } @@ -284,6 +324,7 @@ func (r *Repository) DeleteManifest(digest string) error { return errors.Error{ StatusCode: resp.StatusCode, + StatusText: resp.Status, Message: string(b), } } @@ -298,6 +339,7 @@ func (r *Repository) DeleteTag(tag string) error { if !exist { return errors.Error{ StatusCode: http.StatusNotFound, + StatusText: http.StatusText(http.StatusNotFound), } } @@ -313,6 +355,10 @@ func (r *Repository) BlobExist(digest string) (bool, error) { resp, err := r.client.Do(req) if err != nil { + ok, e := isUnauthorizedError(err) + if ok { + return false, e + } return false, err } @@ -333,6 +379,7 @@ func (r *Repository) BlobExist(digest string) (bool, error) { return false, errors.Error{ StatusCode: resp.StatusCode, + StatusText: resp.Status, Message: string(b), } } @@ -346,6 +393,11 @@ func (r *Repository) PullBlob(digest string) (size int64, data []byte, err error resp, err := r.client.Do(req) if err != nil { + ok, e := isUnauthorizedError(err) + if ok { + err = e + return + } return } @@ -367,6 +419,7 @@ func (r *Repository) PullBlob(digest string) (size int64, data []byte, err error err = errors.Error{ StatusCode: resp.StatusCode, + StatusText: resp.Status, Message: string(b), } @@ -379,6 +432,11 @@ func (r *Repository) initiateBlobUpload(name string) (location, uploadUUID strin resp, err := r.client.Do(req) if err != nil { + ok, e := isUnauthorizedError(err) + if ok { + err = e + return + } return } @@ -397,6 +455,7 @@ func (r *Repository) initiateBlobUpload(name string) (location, uploadUUID strin err = errors.Error{ StatusCode: resp.StatusCode, + StatusText: resp.Status, Message: string(b), } @@ -411,6 +470,10 @@ func (r *Repository) monolithicBlobUpload(location, digest string, size int64, d resp, err := r.client.Do(req) if err != nil { + ok, e := isUnauthorizedError(err) + if ok { + return e + } return err } @@ -427,6 +490,7 @@ func (r *Repository) monolithicBlobUpload(location, digest string, size int64, d return errors.Error{ StatusCode: resp.StatusCode, + StatusText: resp.Status, Message: string(b), } } @@ -460,6 +524,10 @@ func (r *Repository) DeleteBlob(digest string) error { resp, err := r.client.Do(req) if err != nil { + ok, e := isUnauthorizedError(err) + if ok { + return e + } return err } @@ -476,6 +544,7 @@ func (r *Repository) DeleteBlob(digest string) error { return errors.Error{ StatusCode: resp.StatusCode, + StatusText: resp.Status, Message: string(b), } } diff --git a/utils/registry/repository_test.go b/utils/registry/repository_test.go new file mode 100644 index 000000000..07b46237b --- /dev/null +++ b/utils/registry/repository_test.go @@ -0,0 +1,176 @@ +/* + Copyright (c) 2016 VMware, Inc. All Rights Reserved. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package registry + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + "time" + + //"github.com/vmware/harbor/utils/log" + "github.com/vmware/harbor/utils/registry/auth" + "github.com/vmware/harbor/utils/registry/errors" +) + +var ( + username = "user" + password = "P@ssw0rd" + repo = "samalba/my-app" + tags = tagResp{Tags: []string{"1.0", "2.0", "3.0"}} + validToken = "valid_token" + invalidToken = "invalid_token" + credential auth.Credential + registryServer *httptest.Server + tokenServer *httptest.Server + repositoryClient *Repository +) + +type tagResp struct { + Tags []string `json:"tags"` +} + +func TestMain(m *testing.M) { + //log.SetLevel(log.DebugLevel) + credential = auth.NewBasicAuthCredential(username, password) + + tokenServer = initTokenServer() + defer tokenServer.Close() + + registryServer = initRegistryServer() + defer registryServer.Close() + + os.Exit(m.Run()) +} + +func initRegistryServer() *httptest.Server { + mux := http.NewServeMux() + mux.HandleFunc("/v2/", servePing) + mux.HandleFunc(fmt.Sprintf("/v2/%s/tags/list", repo), serveTaglisting) + + return httptest.NewServer(mux) +} + +//response ping request: http://registry/v2 +func servePing(w http.ResponseWriter, r *http.Request) { + if !isTokenValid(r) { + challenge(w) + return + } +} + +func serveTaglisting(w http.ResponseWriter, r *http.Request) { + if !isTokenValid(r) { + challenge(w) + return + } + + if err := json.NewEncoder(w).Encode(tags); err != nil { + w.Write([]byte(err.Error())) + w.WriteHeader(http.StatusInternalServerError) + return + } + +} + +func isTokenValid(r *http.Request) bool { + valid := false + auth := r.Header.Get(http.CanonicalHeaderKey("Authorization")) + if len(auth) != 0 { + auth = strings.TrimSpace(auth) + index := strings.Index(auth, "Bearer") + token := auth[index+6:] + token = strings.TrimSpace(token) + if token == validToken { + valid = true + } + } + return valid +} + +func challenge(w http.ResponseWriter) { + challenge := "Bearer realm=\"" + tokenServer.URL + "/service/token\",service=\"token-service\"" + w.Header().Set("Www-Authenticate", challenge) + w.WriteHeader(http.StatusUnauthorized) + return +} + +func initTokenServer() *httptest.Server { + mux := http.NewServeMux() + mux.HandleFunc("/service/token", serveToken) + + return httptest.NewServer(mux) +} + +func serveToken(w http.ResponseWriter, r *http.Request) { + u, p, ok := r.BasicAuth() + if !ok || u != username || p != password { + w.WriteHeader(http.StatusUnauthorized) + return + } + + result := make(map[string]interface{}) + result["token"] = validToken + result["expires_in"] = 300 + result["issued_at"] = time.Now().Format(time.RFC3339) + + encoder := json.NewEncoder(w) + if err := encoder.Encode(result); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } +} + +func TestListTag(t *testing.T) { + client, err := NewRepositoryWithCredential(repo, registryServer.URL, credential) + if err != nil { + t.Error(err) + } + + list, err := client.ListTag() + if err != nil { + t.Error(err) + return + } + if len(list) != len(tags.Tags) { + t.Errorf("expected length: %d, actual length: %d", len(tags.Tags), len(list)) + return + } + +} + +func TestListTagWithInvalidCredential(t *testing.T) { + credential := auth.NewBasicAuthCredential(username, "wrong_password") + client, err := NewRepositoryWithCredential(repo, registryServer.URL, credential) + if err != nil { + t.Error(err) + } + + _, err = client.ListTag() + if err != nil { + e, ok := errors.ParseError(err) + if ok && e.StatusCode == http.StatusUnauthorized { + return + } + t.Error(err) + return + } +} diff --git a/views/segment/header-content.tpl b/views/segment/header-content.tpl index 368a64d8c..2dc434c60 100644 --- a/views/segment/header-content.tpl +++ b/views/segment/header-content.tpl @@ -37,6 +37,7 @@
  • {{i18n .Lang "language_en-US"}}
  • {{i18n .Lang "language_zh-CN"}}
  • {{i18n .Lang "language_de-DE"}}
  • +
  • {{i18n .Lang "language_ru-RU"}}