Merge pull request #1135 from vmware/dev

dev -> master for 0.5.0rc1
This commit is contained in:
Daniel Jiang 2016-11-21 17:06:37 +08:00 committed by GitHub
commit 99e1b5cd85
135 changed files with 2309 additions and 724 deletions

8
.gitignore vendored
View File

@ -1,11 +1,5 @@
harbor
make/common/config/registry/config.yml
make/common/config/ui/env
make/common/config/ui/app.conf
make/common/config/db/env
make/common/config/jobservice/env
make/common/config/nginx/nginx.conf
make/common/config/nginx/cert/*
make/common/config/*
make/dev/ui/harbor_ui
make/dev/jobservice/harbor_jobservice
src/ui/ui

View File

@ -5,10 +5,14 @@
# all: prepare env, compile binarys, build images and install images
# prepare: prepare env
# compile: compile ui and jobservice code
# compile_buildgolangimage:
# compile local building golang image
# forexample : make compile_buildgolangimage -e \
# GOBUILDIMAGE=harborgo:1.6.2
# compile_golangimage:
# compile from golang image
# for example: make compile_golangimage -e GOBUILDIMAGE= \
# reg-bj.eng.vmware.com/harborrelease/harborgo:1.6.2
# harborgo:1.6.2
# compile_ui, compile_jobservice: compile specific binary
#
# build: build Harbor docker images (defuault: build_photon)
@ -52,8 +56,6 @@
# cleanversiontag:
# cleanpackageremove specific version tag
# cleanpackage: remove online/offline install package
#
# all: install
#
# other example:
# clean specific version binarys and images:
@ -106,6 +108,7 @@ GOBUILDPATH_JOBSERVICE=$(GOBUILDPATH)/src/jobservice
GOBUILDMAKEPATH=$(GOBUILDPATH)/make
GOBUILDMAKEPATH_UI=$(GOBUILDMAKEPATH)/dev/ui
GOBUILDMAKEPATH_JOBSERVICE=$(GOBUILDMAKEPATH)/dev/jobservice
GOLANGDOCKERFILENAME=Dockerfile.golang
# binary
UISOURCECODE=$(SRCPATH)/ui
@ -170,7 +173,7 @@ REGISTRYUSER=user
REGISTRYPASSWORD=default
version:
if [ "$(DEVFLAG)" = "false" ] ; then \
@if [ "$(DEVFLAG)" = "false" ] ; then \
$(SEDCMD) -i 's/version=\"{{.Version}}\"/version=\"$(VERSIONTAG)\"/' -i $(VERSIONFILEPATH)/$(VERSIONFILENAME) ; \
fi
@ -179,40 +182,41 @@ check_environment:
compile_ui:
@echo "compiling binary for ui..."
$(GOBUILD) -o $(UIBINARYPATH)/$(UIBINARYNAME) $(UISOURCECODE)
@$(GOBUILD) -o $(UIBINARYPATH)/$(UIBINARYNAME) $(UISOURCECODE)
@echo "Done."
compile_jobservice:
@echo "compiling binary for jobservice..."
$(GOBUILD) -o $(JOBSERVICEBINARYPATH)/$(JOBSERVICEBINARYNAME) $(JOBSERVICESOURCECODE)
@$(GOBUILD) -o $(JOBSERVICEBINARYPATH)/$(JOBSERVICEBINARYNAME) $(JOBSERVICESOURCECODE)
@echo "Done."
compile_normal: compile_ui compile_jobservice
compile_golangimage:
@echo "pulling golang build base image"
$(DOCKERPULL) $(GOBUILDIMAGE)
compile_buildgolangimage:
@echo "compiling golang image for harbor ..."
@$(DOCKERBUILD) -t $(GOBUILDIMAGE) -f $(TOOLSPATH)/$(GOLANGDOCKERFILENAME) .
@echo "Done."
compile_golangimage:
@echo "compiling binary for ui (golang image)..."
@echo $(GOBASEPATH)
@echo $(GOBUILDPATH)
$(DOCKERCMD) run --rm -v $(BUILDPATH):$(GOBUILDPATH) -w $(GOBUILDPATH_UI) $(GOBUILDIMAGE) $(GOIMAGEBUILD) -v -o $(GOBUILDMAKEPATH_UI)/$(UIBINARYNAME)
@$(DOCKERCMD) run --rm -v $(BUILDPATH):$(GOBUILDPATH) -w $(GOBUILDPATH_UI) $(GOBUILDIMAGE) $(GOIMAGEBUILD) -v -o $(GOBUILDMAKEPATH_UI)/$(UIBINARYNAME)
@echo "Done."
@echo "compiling binary for jobservice (golang image)..."
$(DOCKERCMD) run --rm -v $(BUILDPATH):$(GOBUILDPATH) -w $(GOBUILDPATH_JOBSERVICE) $(GOBUILDIMAGE) $(GOIMAGEBUILD) -v -o $(GOBUILDMAKEPATH_JOBSERVICE)/$(JOBSERVICEBINARYNAME)
@$(DOCKERCMD) run --rm -v $(BUILDPATH):$(GOBUILDPATH) -w $(GOBUILDPATH_JOBSERVICE) $(GOBUILDIMAGE) $(GOIMAGEBUILD) -v -o $(GOBUILDMAKEPATH_JOBSERVICE)/$(JOBSERVICEBINARYNAME)
@echo "Done."
compile:check_environment $(COMPILETAG)
prepare:
@echo "preparing..."
$(MAKEPATH)/$(PREPARECMD) -conf $(CONFIGPATH)/$(CONFIGFILE)
@$(MAKEPATH)/$(PREPARECMD) -conf $(CONFIGPATH)/$(CONFIGFILE)
build_common: version
@echo "buildging db container for photon..."
cd $(DOCKERFILEPATH_DB) && $(DOCKERBUILD) -f $(DOCKERFILENAME_DB) -t $(DOCKERIMAGENAME_DB):$(VERSIONTAG) .
@cd $(DOCKERFILEPATH_DB) && $(DOCKERBUILD) -f $(DOCKERFILENAME_DB) -t $(DOCKERIMAGENAME_DB):$(VERSIONTAG) .
@echo "Done."
build_photon: build_common
@ -224,13 +228,13 @@ build_ubuntu: build_common
build: build_$(BASEIMAGE)
modify_composefile:
@echo "preparing tag:$(VERSIONTAG) docker-compose file..."
@echo "preparing docker-compose file..."
@cp $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSETPLFILENAME) $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSEFILENAME)
@$(SEDCMD) -i 's/image\: vmware.*/&:$(VERSIONTAG)/g' $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSEFILENAME)
install: compile build prepare modify_composefile
@echo "loading harbor images..."
$(DOCKERCOMPOSECMD) -f $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSEFILENAME) up -d
@$(DOCKERCOMPOSECMD) -f $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSEFILENAME) up -d
@echo "Install complete. You can visit harbor now."
package_online: modify_composefile
@ -238,12 +242,13 @@ package_online: modify_composefile
@cp -r make $(HARBORPKG)
@if [ -n "$(REGISTRYSERVER)" ] ; then \
$(SEDCMD) -i 's/image\: vmware/image\: $(REGISTRYSERVER)\/$(REGISTRYPROJECTNAME)/' \
$(HARBORPKG)/docker-compose.$(VERSIONTAG).yml ; \
$(HARBORPKG)/docker-compose.yml ; \
fi
@cp LICENSE $(HARBORPKG)/LICENSE
@cp NOTICE $(HARBORPKG)/NOTICE
@$(TARCMD) -zcvf harbor-online-installer-$(VERSIONTAG).tgz \
--exclude=$(HARBORPKG)/common/db --exclude=$(HARBORPKG)/ubuntu \
--exclude=$(HARBORPKG)/common/db --exclude=$(HARBORPKG)/common/config\
--exclude=$(HARBORPKG)/common/log --exclude=$(HARBORPKG)/ubuntu \
--exclude=$(HARBORPKG)/photon --exclude=$(HARBORPKG)/kubernetes \
--exclude=$(HARBORPKG)/dev --exclude=$(DOCKERCOMPOSETPLFILENAME) \
--exclude=$(HARBORPKG)/checkenv.sh \
@ -262,11 +267,11 @@ package_offline: compile build modify_composefile
@cp NOTICE $(HARBORPKG)/NOTICE
@echo "pulling nginx and registry..."
$(DOCKERPULL) registry:2.5.0
$(DOCKERPULL) nginx:1.11.5
@$(DOCKERPULL) registry:2.5.0
@$(DOCKERPULL) nginx:1.11.5
@echo "saving harbor docker image"
$(DOCKERSAVE) -o $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tgz \
@$(DOCKERSAVE) -o $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tgz \
$(DOCKERIMAGENAME_UI):$(VERSIONTAG) \
$(DOCKERIMAGENAME_LOG):$(VERSIONTAG) \
$(DOCKERIMAGENAME_DB):$(VERSIONTAG) \
@ -274,7 +279,8 @@ package_offline: compile build modify_composefile
nginx:1.11.5 registry:2.5.0
@$(TARCMD) -zcvf harbor-offline-installer-$(VERSIONTAG).tgz \
--exclude=$(HARBORPKG)/common/db --exclude=$(HARBORPKG)/ubuntu \
--exclude=$(HARBORPKG)/common/db --exclude=$(HARBORPKG)/common/config\
--exclude=$(HARBORPKG)/common/log --exclude=$(HARBORPKG)/ubuntu \
--exclude=$(HARBORPKG)/photon --exclude=$(HARBORPKG)/kubernetes \
--exclude=$(HARBORPKG)/dev --exclude=$(DOCKERCOMPOSETPLFILENAME) \
--exclude=$(HARBORPKG)/checkenv.sh \
@ -287,10 +293,10 @@ package_offline: compile build modify_composefile
pushimage:
@echo "pushing harbor images ..."
$(DOCKERTAG) $(DOCKERIMAGENAME_UI):$(VERSIONTAG) $(REGISTRYSERVER)$(DOCKERIMAGENAME_UI):$(VERSIONTAG)
$(PUSHSCRIPTPATH)/$(PUSHSCRIPTNAME) $(REGISTRYSERVER)$(DOCKERIMAGENAME_UI):$(VERSIONTAG) \
@$(DOCKERTAG) $(DOCKERIMAGENAME_UI):$(VERSIONTAG) $(REGISTRYSERVER)$(DOCKERIMAGENAME_UI):$(VERSIONTAG)
@$(PUSHSCRIPTPATH)/$(PUSHSCRIPTNAME) $(REGISTRYSERVER)$(DOCKERIMAGENAME_UI):$(VERSIONTAG) \
$(REGISTRYUSER) $(REGISTRYPASSWORD) $(REGISTRYSERVER)
$(DOCKERRMIMAGE) $(REGISTRYSERVER)$(DOCKERIMAGENAME_UI):$(VERSIONTAG)
@$(DOCKERRMIMAGE) $(REGISTRYSERVER)$(DOCKERIMAGENAME_UI):$(VERSIONTAG)
@$(DOCKERTAG) $(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG) $(REGISTRYSERVER)$(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG)
@$(PUSHSCRIPTPATH)/$(PUSHSCRIPTNAME) $(REGISTRYSERVER)$(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG) \
@ -309,7 +315,7 @@ pushimage:
start:
@echo "loading harbor images..."
@$(DOCKERCOMPOSECMD) -f $(DOCKERCOMPOSEFILEPATH)/docker-compose.$(VERSIONTAG).yml up -d
@$(DOCKERCOMPOSECMD) -f $(DOCKERCOMPOSEFILEPATH)/docker-compose.yml up -d
@echo "Start complete. You can visit harbor now."
down:
@ -328,12 +334,12 @@ cleanimage:
- $(DOCKERRMIMAGE) -f $(DOCKERIMAGENAME_DB):$(VERSIONTAG)
- $(DOCKERRMIMAGE) -f $(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG)
- $(DOCKERRMIMAGE) -f $(DOCKERIMAGENAME_LOG):$(VERSIONTAG)
#- $(DOCKERRMIMAGE) -f registry:2.5.0
#- $(DOCKERRMIMAGE) -f nginx:1.11.5
# - $(DOCKERRMIMAGE) -f registry:2.5.0
# - $(DOCKERRMIMAGE) -f nginx:1.11.5
cleandockercomposefile:
@echo "cleaning $(DOCKERCOMPOSEFILEPATH)/docker-compose.$(VERSIONTAG).yml"
@if [ -f $(DOCKERCOMPOSEFILEPATH)/docker-compose.$(VERSIONTAG).yml ] ; then rm $(DOCKERCOMPOSEFILEPATH)/docker-compose.$(VERSIONTAG).yml ; fi
@echo "cleaning $(DOCKERCOMPOSEFILEPATH)/docker-compose.yml"
@if [ -f $(DOCKERCOMPOSEFILEPATH)/docker-compose.yml ] ; then rm $(DOCKERCOMPOSEFILEPATH)/docker-compose.yml ; fi
cleanversiontag:
@echo "cleaning version TAG"

209
docs/compile_guide.md Normal file
View File

@ -0,0 +1,209 @@
## Introduction
This guide provides instructions for developers to build and run Harbor from source code.
## Step 1: Prepare for a build environment for Harbor
Harbor is deployed as several Docker containers and most of the code is written in Go language. The build host requires Python, Docker, Docker Compose and golang development environment. Please install the below prerequisites:
Software | Required Version
----------------------|--------------------------
docker | 1.10.0 +
docker-compose | 1.7.1 +
python | 2.7 +
git | 1.9.1 +
make | 3.81 +
golang* | 1.6.0 +
*optional
## Step 2: Getting the source code
```sh
$ git clone https://github.com/vmware/harbor
```
## Step 3: Resolving dependencies of Go language
You can compile the source code by using a Golang dev image. In this case, you can skip this step.
If you are building Harbor using your own Go compiling environment. You need to install LDAP packages manually.
For PhotonOS:
```sh
$ tdnf install -y sed apr-util-ldap
```
For Ubuntu:
```sh
$ apt-get update && apt-get install -y libldap2-dev
```
For other platforms, please consult the relevant documentation of installing LDAP package.
## Step 4: Building and installing Harbor
### Configuration
Edit the file **make/harbor.cfg** and make necessary configuration changes such as hostname, admin password and mail server. Refer to **[Installation and Configuration Guide](installation_guide.md#configuring-harbor)** for more info.
```sh
$ cd harbor
$ vi make/harbor.cfg
```
### Compiling and Running
You can compile the code by one of the three approaches:
#### I. Create a Golang dev image, then build Harbor
* Build Golang dev image:
```sh
$ make compile_buildgolangimage -e GOBUILDIMAGE=harborgo:1.6.2
```
* Build, install and bring up Harbor:
```sh
$ make install -e GOBUILDIMAGE=harborgo:1.6.2 COMPILETAG=compile_golangimage
```
#### II. Compile code with your own Golang environment, then build Harbor
* Move source code to $GOPATH
```sh
$ mkdir $GOPATH/src/github.com/vmware/
$ cd ..
$ mv harbor $GOPATH/src/github.com/vmware/.
```
* Build, install and run Harbor
```sh
$ cd $GOPATH/src/github.com/vmware/harbor
$ make install
```
#### III. Manual build process (compatible with previous versions)
```sh
$ cd make
$ ./prepare
Generated configuration file: ./config/ui/env
Generated configuration file: ./config/ui/app.conf
Generated configuration file: ./config/registry/config.yml
Generated configuration file: ./config/db/env
...
$ cd dev
$ docker-compose up -d
```
### Verify your installation
If everyting worked properly, you can get the below message:
```sh
...
----Harbor has been installed and started successfully.----
Now you should be able to visit the admin portal at http://$YOURIP.
For more details, please visit https://github.com/vmware/harbor .
```
Refer to [Installation and Configuration Guide](installation_guide.md#managing-harbors-lifecycle) for more information about managing your Harbor instance.
## Appendix
* Using the Makefile
The `Makefile` contains these configurable parameters:
Variable | Description
-------------------|-------------
BASEIMAGE | Container base image, default: photon
DEVFLAG | Build model flag, default: dev
COMPILETAG | Compile model flag, default: compile_normal (local golang build)
REGISTRYSERVER | Remote registry server IP address
REGISTRYUSER | Remote registry server user name
REGISTRYPASSWORD | Remote registry server user password
REGISTRYPROJECTNAME| Project name on remote registry server
* Predefined targets:
Target | Description
--------------------|-------------
all | prepare env, compile binaries, build images and install images
prepare | prepare env
compile | compile ui and jobservice code
compile_golangimage | compile local golang image
compile_ui | compile ui binary
compile_jobservice | compile jobservice binary
build | build Harbor docker images (default: using build_photon)
build_photon | build Harbor docker images from Photon OS base image
build_ubuntu | build Harbor docker images from Ubuntu base image
install | compile binaries, build images, prepare specific version of compose file and startup Harbor instance
start | startup Harbor instance
down | shutdown Harbor instance
package_online | prepare online install package
package_offline | prepare offline install package
pushimage | push Harbor images to specific registry server
clean all | remove binary, Harbor images, specific version docker-compose file, specific version tag and online/offline install package
cleanbinary | remove ui and jobservice binary
cleanimage | remove Harbor images
cleandockercomposefile | remove specific version docker-compose
cleanversiontag | remove specific version tag
cleanpackage | remove online/offline install package
#### EXAMPLE:
#### Build a golang dev image (for building Harbor):
```sh
$ make compile_golangimage -e GOBUILDIMAGE= [$YOURIMAGE]
```
#### Build Harbor images based on Ubuntu
```sh
$ make build -e BASEIMAGE=ubuntu
```
#### Push Harbor images to specific registry server
```sh
$ make pushimage -e DEVFLAG=false REGISTRYSERVER=[$SERVERADDRESS] REGISTRYUSER=[$USERNAME] REGISTRYPASSWORD=[$PASSWORD] REGISTRYPROJECTNAME=[$PROJECTNAME]
```
**Note**: need add "/" on end of REGISTRYSERVER. If REGISTRYSERVER is not set, images will be pushed directly to Docker Hub.
```sh
$ make pushimage -e DEVFLAG=false REGISTRYUSER=[$USERNAME] REGISTRYPASSWORD=[$PASSWORD] REGISTRYPROJECTNAME=[$PROJECTNAME]
```
#### Clean up binaries and images of a specific version
```sh
$ make clean -e VERSIONTAG=[TAG]
```
**Note**: If new code had been added to Github, the git commit TAG will change. Better use this command to clean up images and files of previous TAG.
#### By default, the make process create a development build. To create a release build of Harbor, set the below flag to false.
```sh
$ make XXXX -e DEVFLAG=false
```

View File

@ -27,26 +27,18 @@ Otherwise, if you use IP address to connect your registry host, CN can be anythi
```
3) Generate the certificate of your registry host:
On Ubuntu, the config file of openssl locates at **/etc/ssl/openssl.cnf**. Refer to openssl document for more information. The default CA directory of openssl is called demoCA. Let's create necessary directories and files:
```
mkdir demoCA
cd demoCA
touch index.txt
echo '01' > serial
cd ..
```
If you're using FQDN like **reg.yourdomain.com** to connect your registry host, then run this command to generate the certificate of your registry host:
```
openssl ca -in yourdomain.com.csr -out yourdomain.com.crt -cert ca.crt -keyfile ca.key -outdir .
openssl x509 -req -days 365 -in yourdomain.com.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out yourdomain.com.crt
```
If you're using **IP**, say **192.168.1.101** to connect your registry host, you may instead run the command below:
```
echo subjectAltName = IP:192.168.1.101 > extfile.cnf
openssl ca -in yourdomain.com.csr -out yourdomain.com.crt -cert ca.crt -keyfile ca.key -extfile extfile.cnf -outdir .
openssl x509 -req -days 365 -in yourdomain.com.csr -CA ca.crt -CAkey ca.key -CAcreateserial -extfile extfile.cnf -out yourdomain.com
.crt
```
##Configuration and Installation
After obtaining the **yourdomain.com.crt** and **yourdomain.com.key** files,
@ -124,3 +116,4 @@ If you've mapped nginx 443 port to another, you need to add the port to login, l
update-ca-trust
```

View File

@ -36,7 +36,7 @@ From time to time, you may need to mannually test Harbor REST API. You can deplo
```
* Change the directory to _make_
```sh
cd ../make
cd ../make/dev
```
* Edit the _docker-compose.yml_ file.
```sh

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 44 KiB

BIN
docs/img/ova/ova09.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -22,43 +22,55 @@ This guide walks you through the steps about installing and configuring Harbor o
![ova](img/ova/ova03.png)
5. Specify a name and a location for the virtual appliance.
5. Accept the end user license agreements and click "Next".
![ova](img/ova/ova04.png)
6. Select the datastore and virtual disk format, click "Next".
6. Specify a name and a location for the virtual appliance.
![ova](img/ova/ova05.png)
7. Configure the network(s) the virtual appliance should be connected to.
7. Select the datastore and virtual disk format, click "Next".
![ova](img/ova/ova06.png)
8. Customize the properties of Harbor. The properties are described below. Note that at the very least, you just need to set the **Root Password**, **Harbor Admin Password** and **Database Password** properties.
8. Configure the network(s) the virtual appliance should be connected to.
![ova](img/ova/ova07.png)
* Harbor
* **Root Password**: The password of the root user.
9. Customize the properties of Harbor. The properties are described below. Note that at the very least, you just need to set the **Root Password**, **Harbor Admin Password** and **Database Password** properties.
![ova](img/ova/ova08.png)
* System
* **Root Password**: The initial password of the root user. Subsequent changes of password should be performed in operating system. (8-128 characters)
* **Harbor Admin Password**: The initial password of Harbor admin. It only works for the first time when Harbor starts. It has no effect after the first launch of Harbor. Change the admin password from UI after launching Harbor.
* **Database Password**: The password of the root user of MySQL database.
* **Authentication Mode**: The default authentication mode is db_auth, i.e. the credentials are stored in a local database. Set it to ldap_auth if you want to verify the user's credential against an LDAP/AD server.
* **Database Password**: The initial password of the root user of MySQL database. Subsequent changes of password should be performed in operating system. (8-128 characters)
* **Permit Root Login**: Specifies whether root use can log in using SSH.
* **Self Registration**: Determine whether the self-registration is allowed or not. Set this to off to disable a user's self-registration in Harbor. This flag has no effect when users are stored in LDAP or AD.
* **Garbage Collection**: When setting this to true, Harbor performs garbage collection everytime it boots up. The first time setting this flag to true needs to power off the VM and power it on again.
* Authentication
* **Authentication Mode**: The default authentication mode is db_auth. Set it to ldap_auth when users' credentials are stored in an LDAP or AD server. Note: this option can only be set once.
* **LDAP URL**: The URL of an LDAP/AD server.
* **LDAP Search DN**: A user's DN who has the permission to search the LDAP/AD server. If your LDAP/AD server does not support anonymous search, you should configure this DN and LDAP Seach Password.
* **LDAP Search Password**: The password of the user for LDAP search.
* **LDAP Base DN**: The base DN from which to look up a user in LDAP/AD.
* **LDAP UID**: The attribute used in a search to match a user, it could be uid, cn, email, sAMAccountName or other attributes depending on your LDAP/AD server.
* Security
* **Protocol**: The protocol for accessing Harbor. Warning: setting it to http makes the communication insecure.
* **SSL Cert**: Paste in the content of a certificate file. Leave blank for a generated self-signed certificate.
* **SSL Cert Key**: Paste in the content of certificate key file. Leave blank for a generated key.
* **Verify Remote Cert**: Determine whether the image replication should verify the certificate when it connects to a remote registry via TLS. Set this flag to off when the remote registry uses a self-signed or untrusted certificate.
* Email Settings
* **Email Server**: The mail server to send out emails to reset password.
* **Email Server Port**: The port of mail server.
* **Email Username**: The user from whom the password reset email is sent.
* **Email Password**: The password of the user from whom the password reset email is sent.
* **Email From**: The name of the email sender.
* **Email SSL**: Whether to enabled secure mail transmission.
* **SSL Cert**: Paste in the content of a certificate file. If SSL Cert and SSL Cert Key are both set, HTTPS will be used.
* **SSL Cert Key**: Paste in the content of certificate key file. If SSL Cert and SSL Cert Key are both set, HTTPS will be used.
* **Self Registration**: Determine whether the self-registration is allowed or not. Set this to off to disable a user's self-registration in Harbor. This flag has no effect when users are stored in LDAP or AD.
* **Verify Remote Cert**: Determine whether the image replication should verify the certificate when it connects to a remote registry via TLS. Set this flag to off when the remote registry uses a self-signed or untrusted certificate.
* **Garbage Collection**: When setting this to true, Harbor performs garbage collection everytime it boots up. The first time setting this flag to true needs to power off the VM and power it on again.
* Networking properties
* **Default Gateway**: The default gateway address for this VM. Leave blank if DHCP is desired.
@ -68,19 +80,19 @@ This guide walks you through the steps about installing and configuring Harbor o
* **Network 1 IP Adress**: The IP address of this interface. Leave blank if DHCP is desired.
* **Network 1 Netmask**: The netmask or prefix for this interface. Leave blank if DHCP is desired.
**Notes:** If you want to enable HTTPS with a self-signed certificate, refer to the "Getting a certificate" part of this [guide](https://github.com/vmware/harbor/blob/master/docs/configure_https.md#getting-a-certificate) for generating a certificate.
**Notes:** If you want to enable HTTPS with a self-signed certificate created manually, refer to the "Getting a certificate" part of this [guide](https://github.com/vmware/harbor/blob/master/docs/configure_https.md#getting-a-certificate) for generating a certificate.
After you complete the properties, click "Next".
9. Review your settings and click "Finish" to complete the deployment.
10. Review your settings and click "Finish" to complete the deployment.
![ova](img/ova/ova08.png)
![ova](img/ova/ova09.png)
10. Power on the virtual appliance. It may take a few minutes for the first bootup. The virtual appliance needs to initialize itself for configuration like netowrk address and password.
11. Power on the virtual appliance. It may take a few minutes for the first bootup. The virtual appliance needs to initialize itself for configuration like netowrk address and password.
11. When the appliance is ready, check from vSphere Web Client for its IP address. Open a browser and type in the URL `http(s)://harbor_ip_address` or `http(s)://harbor_host_name`. Log in as the admin user and verify Harbor has been successfully installed.
12. When the appliance is ready, check from vSphere Web Client for its IP address. Open a browser and type in the URL `http(s)://harbor_ip_address` or `http(s)://harbor_host_name`. Log in as the admin user and verify Harbor has been successfully installed.
12. For information on how to use Harbor, please refer to [User Guide of Harbor](user_guide.md).
13. For information on how to use Harbor, please refer to [User Guide of Harbor](user_guide.md).
## Reconfiguration
If you want to change the properties of Harbor, follow the below steps:
@ -96,4 +108,10 @@ If you want to change the properties of Harbor, follow the below steps:
4. **Power on** the VM.
**Note:** The initial admin password, root password of the virtual appliance, MySql root password, and all networking properties can not be modified using this method after Harbor's first launch. The password of the admin user should be changed in the admin portal. The root password of virtual appliance, as well as the networking settings, can be changed by logging in the virtural appliance and doing it in the Linux operating system.
**Notes:**
1. The authentication mode can only be set once on firtst boot. So subsequent modification of this option will have no effect.
2. The initial admin password, root password of the virtual appliance, MySQL root password, and all networking properties can not be modified using this method after Harbor's first launch. Modify them by the following steps:
* Harbor Admin Password: Change it in Harbor admin portal.
* Root Password of Virtual Appliance: Change it by logging in the virtual appliance and doing it in the Linux operating system.
* MySQL Root Password: Change it by logging in the virtual appliance and doing it in the Linux operating system.
* Networking Properties: Visit `https://harbor_ip_address:5480`, login with root/password of your virtual appliance and modify networking properties. Reboot the system after you changing them.

View File

@ -27,35 +27,30 @@ When upgrading your existing Habor instance to a newer version, you may need to
git clone https://github.com/vmware/harbor
```
4. Before upgrading Harbor, perform database migration first.
The directory **tools/migration/** contains the tool for migration. The first step is to update values of `db_username`, `db_password`, `db_port`, `db_name` in **migration.cfg** so that they match your system's configuration.
5. The migration tool is delivered as a container, so you should build the image from its Dockerfile:
```
cd tools/migration/
docker build -t migrate-tool .
```
6. Back up database to a directory such as `/path/to/backup`. You need to create the directory if it does not exist.
4. Before upgrading Harbor, perform database migration first. The migration tool is delivered as a docker image, so you should pull the image from docker hub:
```
docker run -ti --rm -v /data/database:/var/lib/mysql -v /path/to/backup:/harbor-migration/backup migrate-tool backup
docker pull vmware/harbor-db-migrator
```
7. Upgrade database schema and migrate data:
5. Back up database to a directory such as `/path/to/backup`. You need to create the directory if it does not exist. Also, note that the username and password to access the db are provided via environment variable "DB_USR" and "DB_PWD"
```
docker run -ti --rm -v /data/database:/var/lib/mysql migrate-tool up head
docker run -ti --rm -e DB_USR=root -e DB_PWD=xxxx -v /data/database:/var/lib/mysql -v /path/to/backup:/harbor-migration/backup vmware/harbor-db-migrator backup
```
8. Change to `make/` directory, configure Harbor by modifying the file `harbor.cfg`, you may need to refer to the configuration files you've backed up during step 2. Refer to [Installation & Configuration Guide ](../docs/installation_guide.md) for more info.
6. Upgrade database schema and migrate data:
9. If HTTPS has been enabled for Harbor before, restore the `nginx.conf` and key/certificate files from the backup files in Step 2. Refer to [Configuring Harbor with HTTPS Access](../docs/configure_https.md) for more info.
```
docker run -ti --rm -e DB_USR=root -e DB_PWD=xxxx -v /data/database:/var/lib/mysql vmware/harbor-db-migrator up head
```
10. Under the directory `make/`, run the `./prepare` script to generate necessary config files.
7. Change to `make/` directory, configure Harbor by modifying the file `harbor.cfg`, you may need to refer to the configuration files you've backed up during step 2. Refer to [Installation & Configuration Guide ](../docs/installation_guide.md) for more info.
8. Under the directory `make/`, run the `./prepare` script to generate necessary config files.
11. Rebuild Harbor and restart the registry service
9. Rebuild Harbor and restart the registry service
```
docker-compose up --build -d
@ -73,7 +68,7 @@ For any reason, if you want to roll back to the previous version of Harbor, foll
2. Restore database 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
docker run -ti --rm -e DB_USR=root -e DB_PWD=xxxx -v /data/database:/var/lib/mysql -v /path/to/backup:/harbor-migration/backup vmware/harbor-db-migrator restore
```
3. Remove current source code of Harbor.
@ -95,9 +90,9 @@ For any reason, if you want to roll back to the previous version of Harbor, foll
### Migration tool reference
- Use `help` command to show instructions of the migration tool:
```docker run --rm migrate-tool help```
```docker run --rm -e DB_USR=root -e DB_PWD=xxxx vmware/harbor-db-migrator help```
- Use `test` command to test mysql connection:
```docker run --rm -v /data/database:/var/lib/mysql migrate-tool test```
```docker run --rm -e DB_USR=root -e DB_PWD=xxxx -v /data/database:/var/lib/mysql vmware/harbor-db-migrator test```

View File

@ -507,6 +507,21 @@ paths:
description: User registration can only be used by admin role user when self-registration is off.
500:
description: Unexpected internal errors.
/users/current:
get:
summary: Get current user info.
description: |
This endpoint is to get the current user infomation.
tags:
- Products
responses:
200:
description: Get current user information successfully.
in: body
schema:
$ref: '#/definitions/User'
401:
description: User need to log in first.
/users/{user_id}:
put:
summary: Update a registered user to change his profile.

View File

@ -1,75 +0,0 @@
worker_processes auto;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
tcp_nodelay on;
# this is necessary for us to be able to disable request buffering in all cases
proxy_http_version 1.1;
upstream registry {
server registry:5000;
}
upstream ui {
server ui:80;
}
server {
listen 80;
# disable any limits to avoid HTTP 413 for large image uploads
client_max_body_size 0;
location / {
proxy_pass http://ui/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# When setting up Harbor behind other proxy, such as an Nginx instance, remove the below line if the proxy already has similar settings.
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_request_buffering off;
}
location /v1/ {
return 404;
}
location /v2/ {
proxy_pass http://registry/v2/;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# When setting up Harbor behind other proxy, such as an Nginx instance, remove the below line if the proxy already has similar settings.
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_request_buffering off;
}
location /service/ {
proxy_pass http://ui/service/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# When setting up Harbor behind other proxy, such as an Nginx instance, remove the below line if the proxy already has similar settings.
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_request_buffering off;
}
}
}

View File

@ -12,4 +12,4 @@ LOG_LEVEL=debug
LOG_DIR=/var/log/jobs
GODEBUG=netdns=cgo
EXT_ENDPOINT=$ui_url
TOKEN_URL=http://ui
TOKEN_ENDPOINT=http://ui

View File

@ -51,6 +51,9 @@ http {
# When setting up Harbor behind other proxy, such as an Nginx instance, remove the below line if the proxy already has similar settings.
proxy_set_header X-Forwarded-Proto $$scheme;
# Add Secure flag when serving HTTPS
proxy_cookie_path / "/; secure";
proxy_buffering off;
proxy_request_buffering off;
}

View File

@ -9,6 +9,7 @@ names = en-US|zh-CN
httpport = 80
[mail]
identity = $email_identity
host = $email_server
port = $email_server_port
username = $email_username

View File

@ -3,11 +3,11 @@ MYSQL_PORT=3306
MYSQL_USR=root
MYSQL_PWD=$db_password
REGISTRY_URL=http://registry:5000
JOB_SERVICE_URL=http://jobservice
UI_URL=http://ui
CONFIG_PATH=/etc/ui/app.conf
HARBOR_REG_URL=$hostname
EXT_REG_URL=$hostname
HARBOR_ADMIN_PASSWORD=$harbor_admin_password
HARBOR_URL=$ui_url
AUTH_MODE=$auth_mode
LDAP_URL=$ldap_url
LDAP_SEARCH_DN=$ldap_searchdn
@ -23,6 +23,7 @@ USE_COMPRESSED_JS=$use_compressed_js
LOG_LEVEL=debug
GODEBUG=netdns=cgo
EXT_ENDPOINT=$ui_url
TOKEN_URL=http://ui
TOKEN_ENDPOINT=http://ui
VERIFY_REMOTE_CERT=$verify_remote_cert
TOKEN_EXPIRATION=$token_expiration
PROJECT_CREATION_RESTRICTION=$project_creation_restriction

View File

@ -23,7 +23,9 @@ COPY make/jsminify.sh /tmp/jsminify.sh
RUN chmod u+x /go/bin/harbor_ui \
&& sed -i 's/TLS_CACERT/#TLS_CAERT/g' /etc/ldap/ldap.conf \
&& sed -i '$a\TLS_REQCERT allow' /etc/ldap/ldap.conf \
&& /tmp/jsminify.sh /go/bin/views/sections/script-include.htm /go/bin/static/resources/js/harbor.app.min.js /go/bin/
&& timestamp=`date '+%s'` \
&& /tmp/jsminify.sh /go/bin/views/sections/script-include.htm /go/bin/static/resources/js/harbor.app.min.$timestamp.js /go/bin/ \
&& sed -i "s/harbor\.app\.min\.js/harbor\.app\.min\.$timestamp\.js/g" /go/bin/views/sections/script-min-include.htm
WORKDIR /go/bin/
ENTRYPOINT ["/go/bin/harbor_ui"]

View File

@ -50,6 +50,7 @@ services:
volumes:
- ./common/config/ui/app.conf:/etc/ui/app.conf
- ./common/config/ui/private_key.pem:/etc/ui/private_key.pem
- /data:/harbor_storage
depends_on:
- log
logging:

View File

@ -9,6 +9,7 @@ hostname = reg.mydomain.com
ui_url_protocol = http
#Email account settings for sending out password resetting emails.
email_identity = Mail Config
email_server = smtp.mydomain.com
email_server_port = 25
email_username = sample_admin@mydomain.com
@ -82,8 +83,11 @@ crt_organizationalunit = organizational unit
crt_commonname = example.com
crt_email = example@example.com
#The flag to control what users have permission to create projects
#Be default everyone can create a project, set to "adminonly" such that only admin can create project.
project_creation_restriction = everyone
#The path of cert and key files for nginx, they are applied only the protocol is set to https
ssl_cert = /path/to/server.crt
ssl_cert_key = /path/to/server.key
ssl_cert = /data/cert/server.crt
ssl_cert_key = /data/cert/server.key
#############

View File

@ -1,4 +1,4 @@
FROM library/photon:latest
FROM library/photon:1.0
RUN mkdir /harbor/
COPY ./make/dev/jobservice/harbor_jobservice /harbor/

View File

@ -1,4 +1,4 @@
FROM library/photon:latest
FROM library/photon:1.0
# run logrotate hourly, disable imklog model, provides TCP/UDP syslog reception
RUN tdnf install -y cronie rsyslog shadow tar gzip \

View File

@ -1,4 +1,4 @@
FROM library/photon:latest
FROM library/photon:1.0
RUN mkdir /harbor/
RUN tdnf install -y sed apr-util-ldap
@ -11,7 +11,10 @@ COPY ./src/favicon.ico /harbor/favicon.ico
COPY ./make/jsminify.sh /tmp/jsminify.sh
RUN chmod u+x /harbor/harbor_ui \
&& tmp/jsminify.sh /harbor/views/sections/script-include.htm /harbor/static/resources/js/harbor.app.min.js /harbor/ \
&& timestamp=`date '+%s'` \
&& /tmp/jsminify.sh /harbor/views/sections/script-include.htm /harbor/static/resources/js/harbor.app.min.$timestamp.js /harbor/ \
&& sed -i "s/harbor\.app\.min\.js/harbor\.app\.min\.$timestamp\.js/g" /harbor/views/sections/script-min-include.htm \
&& echo "TLS_REQCERT allow" >> /etc/openldap/ldap.conf
WORKDIR /harbor/
ENTRYPOINT ["/harbor/harbor_ui"]

View File

@ -32,6 +32,10 @@ def validate(conf):
cert_key_path = rcp.get("configuration", "ssl_cert_key")
if not os.path.isfile(cert_key_path):
raise Exception("Error: The path for certificate key: %s is invalid" % cert_key_path)
project_creation = rcp.get("configuration", "project_creation_restriction")
if project_creation != "everyone" and project_creation != "adminonly":
raise Exception("Error invalid value for project_creation_restriction: %s" % project_creation)
def get_secret_key(path):
key_file = os.path.join(path, "secretkey")
@ -73,6 +77,7 @@ validate(rcp)
hostname = rcp.get("configuration", "hostname")
protocol = rcp.get("configuration", "ui_url_protocol")
ui_url = protocol + "://" + hostname
email_identity = rcp.get("configuration", "email_identity")
email_server = rcp.get("configuration", "email_server")
email_server_port = rcp.get("configuration", "email_server_port")
email_username = rcp.get("configuration", "email_username")
@ -114,6 +119,7 @@ crt_email = rcp.get("configuration", "crt_email")
max_job_workers = rcp.get("configuration", "max_job_workers")
token_expiration = rcp.get("configuration", "token_expiration")
verify_remote_cert = rcp.get("configuration", "verify_remote_cert")
proj_cre_restriction = rcp.get("configuration", "project_creation_restriction")
#secret_key = rcp.get("configuration", "secret_key")
secret_key = get_secret_key(args.data_volume)
########
@ -132,6 +138,14 @@ job_config_dir = os.path.join(config_dir, "jobservice")
if not os.path.exists(job_config_dir):
os.makedirs(job_config_dir)
registry_config_dir = os.path.join(config_dir, "registry")
if not os.path.exists(registry_config_dir):
os.makedirs(registry_config_dir)
nginx_config_dir = os.path.join(config_dir, "nginx")
if not os.path.exists(nginx_config_dir):
os.makedirs(nginx_config_dir)
def render(src, dest, **kw):
t = Template(open(src, 'r').read())
with open(dest, 'w') as f:
@ -140,23 +154,29 @@ def render(src, dest, **kw):
ui_conf_env = os.path.join(config_dir, "ui", "env")
ui_conf = os.path.join(config_dir, "ui", "app.conf")
jobservice_conf = os.path.join(config_dir, "jobservice", "app.conf")
registry_conf = os.path.join(config_dir, "registry", "config.yml")
db_conf_env = os.path.join(config_dir, "db", "env")
job_conf_env = os.path.join(config_dir, "jobservice", "env")
nginx_conf = os.path.join(config_dir, "nginx", "nginx.conf")
cert_dir = os.path.join(config_dir, "nginx", "cert")
conf_files = [ ui_conf, ui_conf_env, registry_conf, db_conf_env, job_conf_env, nginx_conf, cert_dir ]
def rmdir(cf):
for f in cf:
if os.path.isdir(f):
rmdir(map(lambda x: os.path.join(f,x), os.listdir(f)))
elif os.path.exists(f) and os.path.basename(f) != ".gitignore":
print("Clearing the configuration file: %s" % f)
os.remove(f)
rmdir(conf_files)
def delfile(src):
if os.path.isfile(src):
try:
os.remove(src)
print("Clearing the configuration file: %s" % src)
except:
pass
elif os.path.isdir(src):
for item in os.listdir(src):
itemsrc=os.path.join(src,item)
delfile(itemsrc)
delfile(config_dir)
if protocol == "https":
target_cert_path = os.path.join(cert_dir, os.path.basename(cert_path))
if not os.path.exists(cert_dir):
os.makedirs(cert_dir)
shutil.copy2(cert_path,target_cert_path)
target_cert_key_path = os.path.join(cert_dir, os.path.basename(cert_key_path))
shutil.copy2(cert_key_path,target_cert_key_path)
@ -187,10 +207,12 @@ render(os.path.join(templates_dir, "ui", "env"),
ui_secret=ui_secret,
secret_key=secret_key,
verify_remote_cert=verify_remote_cert,
project_creation_restriction=proj_cre_restriction,
token_expiration=token_expiration)
render(os.path.join(templates_dir, "ui", "app.conf"),
ui_conf,
email_identity=email_identity,
email_server=email_server,
email_server_port=email_server_port,
email_username=email_username,
@ -215,6 +237,9 @@ render(os.path.join(templates_dir, "jobservice", "env"),
secret_key=secret_key,
ui_url=ui_url,
verify_remote_cert=verify_remote_cert)
print("Generated configuration file: %s" % jobservice_conf)
shutil.copyfile(os.path.join(templates_dir, "jobservice", "app.conf"), jobservice_conf)
def validate_crt_subj(dirty_subj):
subj_list = [item for item in dirty_subj.strip().split("/") \
@ -262,11 +287,15 @@ if customize_crt == 'on':
if openssl_is_installed(shell_stat):
private_key_pem = os.path.join(config_dir, "ui", "private_key.pem")
root_crt = os.path.join(config_dir, "registry", "root.crt")
crt_conf_files = [ private_key_pem, root_crt ]
rmdir(crt_conf_files)
check_private_key_stat(path=private_key_pem)
check_certificate_stat(path=root_crt)
else:
print("Generated configuration file: %s" % ui_config_dir + "private_key.pem")
shutil.copyfile(os.path.join(templates_dir, "ui", "private_key.pem"), os.path.join(ui_config_dir, "private_key.pem"))
print("Generated configuration file: %s" % registry_config_dir + "root.crt")
shutil.copyfile(os.path.join(templates_dir, "registry", "root.crt"), os.path.join(registry_config_dir, "root.crt"))
FNULL.close()
print("The configuration files are ready, please use docker-compose to start the service.")

View File

@ -20,7 +20,9 @@ COPY ./make/jsminify.sh /tmp/jsminify.sh
RUN chmod u+x /harbor/harbor_ui \
&& sed -i 's/TLS_CACERT/#TLS_CAERT/g' /etc/ldap/ldap.conf \
&& sed -i '$a\TLS_REQCERT allow' /etc/ldap/ldap.conf \
&& /tmp/jsminify.sh /harbor/views/sections/script-include.htm /harbor/static/resources/js/harbor.app.min.js /harbor/
&& timestamp=`date '+%s'` \
&& /tmp/jsminify.sh /harbor/views/sections/script-include.htm /harbor/static/resources/js/harbor.app.min.$timestamp.js /harbor/ \
&& sed -i "s/harbor\.app\.min\.js/harbor\.app\.min\.$timestamp\.js/g" /harbor/views/sections/script-min-include.htm
WORKDIR /harbor/
ENTRYPOINT ["/harbor/harbor_ui"]

View File

@ -19,14 +19,14 @@ import (
"encoding/json"
"fmt"
"net/http"
"os"
"strconv"
"github.com/astaxie/beego/validation"
"github.com/vmware/harbor/src/ui/auth"
"github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/auth"
"github.com/astaxie/beego"
)
@ -213,12 +213,5 @@ func (b *BaseAPI) GetPaginationParams() (page, pageSize int64) {
// GetIsInsecure ...
func GetIsInsecure() bool {
insecure := false
verifyRemoteCert := os.Getenv("VERIFY_REMOTE_CERT")
if verifyRemoteCert == "off" {
insecure = true
}
return insecure
return !config.VerifyRemoteCert()
}

View File

@ -0,0 +1,33 @@
/*
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 api
import (
"github.com/vmware/harbor/src/common/config"
"os"
"testing"
)
func TestGetIsInsecure(t *testing.T) {
os.Setenv("VERIFY_REMOTE_CERT", "off")
err := config.Reload()
if err != nil {
t.Errorf("Failed to load config, error: %v", err)
}
if !GetIsInsecure() {
t.Errorf("GetIsInsecure() should be true when VERIFY_REMOTE_CERT is off, in fact: false")
}
os.Unsetenv("VERIFY_REMOTE_CERT")
}

178
src/common/config/config.go Normal file
View File

@ -0,0 +1,178 @@
/*
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 config provide methods to get the configurations reqruied by code in src/common
package config
import (
"fmt"
"os"
"strings"
)
// ConfLoader is the interface to load configurations
type ConfLoader interface {
// Load will load configuration from different source into a string map, the values in the map will be parsed in to configurations.
Load() (map[string]string, error)
}
// EnvConfigLoader loads the config from env vars.
type EnvConfigLoader struct {
Keys []string
}
// Load ...
func (ec *EnvConfigLoader) Load() (map[string]string, error) {
m := make(map[string]string)
for _, k := range ec.Keys {
m[k] = os.Getenv(k)
}
return m, nil
}
// ConfParser ...
type ConfParser interface {
//Parse parse the input raw map into a config map
Parse(raw map[string]string, config map[string]interface{}) error
}
// Config wraps a map for the processed configuration values,
// and loader parser to read configuration from external source and process the values.
type Config struct {
Config map[string]interface{}
Loader ConfLoader
Parser ConfParser
}
// Load reload the configurations
func (conf *Config) Load() error {
rawMap, err := conf.Loader.Load()
if err != nil {
return err
}
err = conf.Parser.Parse(rawMap, conf.Config)
return err
}
// MySQLSetting wraps the settings of a MySQL DB
type MySQLSetting struct {
Database string
User string
Password string
Host string
Port string
}
// SQLiteSetting wraps the settings of a SQLite DB
type SQLiteSetting struct {
FilePath string
}
type commonParser struct{}
// Parse parses the db settings, veryfy_remote_cert, ext_endpoint, token_endpoint
func (cp *commonParser) Parse(raw map[string]string, config map[string]interface{}) error {
db := strings.ToLower(raw["DATABASE"])
if db == "mysql" || db == "" {
db = "mysql"
mySQLDB := raw["MYSQL_DATABASE"]
if len(mySQLDB) == 0 {
mySQLDB = "registry"
}
setting := MySQLSetting{
mySQLDB,
raw["MYSQL_USR"],
raw["MYSQL_PWD"],
raw["MYSQL_HOST"],
raw["MYSQL_PORT"],
}
config["mysql"] = setting
} else if db == "sqlite" {
f := raw["SQLITE_FILE"]
if len(f) == 0 {
f = "registry.db"
}
setting := SQLiteSetting{
f,
}
config["sqlite"] = setting
} else {
return fmt.Errorf("Invalid DB: %s", db)
}
config["database"] = db
//By default it's true
config["verify_remote_cert"] = raw["VERIFY_REMOTE_CERT"] != "off"
config["ext_endpoint"] = raw["EXT_ENDPOINT"]
config["token_endpoint"] = raw["TOKEN_ENDPOINT"]
config["log_level"] = raw["LOG_LEVEL"]
return nil
}
var commonConfig *Config
func init() {
commonKeys := []string{"DATABASE", "MYSQL_DATABASE", "MYSQL_USR", "MYSQL_PWD", "MYSQL_HOST", "MYSQL_PORT", "SQLITE_FILE", "VERIFY_REMOTE_CERT", "EXT_ENDPOINT", "TOKEN_ENDPOINT", "LOG_LEVEL"}
commonConfig = &Config{
Config: make(map[string]interface{}),
Loader: &EnvConfigLoader{Keys: commonKeys},
Parser: &commonParser{},
}
if err := commonConfig.Load(); err != nil {
panic(err)
}
}
// Reload will reload the configuration.
func Reload() error {
return commonConfig.Load()
}
// Database returns the DB type in configuration.
func Database() string {
return commonConfig.Config["database"].(string)
}
// MySQL returns the mysql setting in configuration.
func MySQL() MySQLSetting {
return commonConfig.Config["mysql"].(MySQLSetting)
}
// SQLite returns the SQLite setting
func SQLite() SQLiteSetting {
return commonConfig.Config["sqlite"].(SQLiteSetting)
}
// VerifyRemoteCert returns bool value.
func VerifyRemoteCert() bool {
return commonConfig.Config["verify_remote_cert"].(bool)
}
// ExtEndpoint ...
func ExtEndpoint() string {
return commonConfig.Config["ext_endpoint"].(string)
}
// TokenEndpoint returns the endpoint string of token service, which can be accessed by internal service of Harbor.
func TokenEndpoint() string {
return commonConfig.Config["token_endpoint"].(string)
}
// LogLevel returns the log level in string format.
func LogLevel() string {
return commonConfig.Config["log_level"].(string)
}

View File

@ -0,0 +1,111 @@
/*
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 config
import (
"os"
"testing"
)
func TestEnvConfLoader(t *testing.T) {
os.Unsetenv("KEY2")
os.Setenv("KEY1", "V1")
os.Setenv("KEY3", "V3")
keys := []string{"KEY1", "KEY2"}
ecl := EnvConfigLoader{
keys,
}
m, err := ecl.Load()
if err != nil {
t.Errorf("Error loading the configuration via env: %v", err)
}
if m["KEY1"] != "V1" {
t.Errorf("The value for key KEY1 should be V1, but infact: %s", m["KEY1"])
}
if len(m["KEY2"]) > 0 {
t.Errorf("The value for key KEY2 should be emptye, but infact: %s", m["KEY2"])
}
if _, ok := m["KEY3"]; ok {
t.Errorf("The KEY3 should not be in result as it's not in the initial key list")
}
os.Unsetenv("KEY1")
os.Unsetenv("KEY3")
}
func TestCommonConfig(t *testing.T) {
mysql := MySQLSetting{"registry", "root", "password", "127.0.0.1", "3306"}
sqlite := SQLiteSetting{"file.db"}
verify := "off"
ext := "http://harbor"
token := "http://token"
loglevel := "info"
os.Setenv("DATABASE", "")
os.Setenv("MYSQL_DATABASE", mysql.Database)
os.Setenv("MYSQL_USR", mysql.User)
os.Setenv("MYSQL_PWD", mysql.Password)
os.Setenv("MYSQL_HOST", mysql.Host)
os.Setenv("MYSQL_PORT", mysql.Port)
os.Setenv("SQLITE_FILE", sqlite.FilePath)
os.Setenv("VERIFY_REMOTE_CERT", verify)
os.Setenv("EXT_ENDPOINT", ext)
os.Setenv("TOKEN_ENDPOINT", token)
os.Setenv("LOG_LEVEL", loglevel)
err := Reload()
if err != nil {
t.Errorf("Unexpected error when loading the configurations, error: %v", err)
}
if Database() != "mysql" {
t.Errorf("Expected Database value: mysql, fact: %s", mysql)
}
if MySQL() != mysql {
t.Errorf("Expected MySQL setting: %+v, fact: %+v", mysql, MySQL())
}
if VerifyRemoteCert() {
t.Errorf("Expected VerifyRemoteCert: false, env var: %s, fact: %v", verify, VerifyRemoteCert())
}
if ExtEndpoint() != ext {
t.Errorf("Expected ExtEndpoint: %s, fact: %s", ext, ExtEndpoint())
}
if TokenEndpoint() != token {
t.Errorf("Expected TokenEndpoint: %s, fact: %s", token, TokenEndpoint())
}
if LogLevel() != loglevel {
t.Errorf("Expected LogLevel: %s, fact: %s", loglevel, LogLevel())
}
os.Setenv("DATABASE", "sqlite")
err = Reload()
if err != nil {
t.Errorf("Unexpected error when loading the configurations, error: %v", err)
}
if SQLite() != sqlite {
t.Errorf("Expected SQLite setting: %+v, fact %+v", sqlite, SQLite())
}
os.Unsetenv("DATABASE")
os.Unsetenv("MYSQL_DATABASE")
os.Unsetenv("MYSQL_USR")
os.Unsetenv("MYSQL_PWD")
os.Unsetenv("MYSQL_HOST")
os.Unsetenv("MYSQL_PORT")
os.Unsetenv("SQLITE_FILE")
os.Unsetenv("VERIFY_REMOTE_CERT")
os.Unsetenv("EXT_ENDPOINT")
os.Unsetenv("TOKEN_ENDPOINT")
os.Unsetenv("LOG_LEVEL")
}

View File

@ -55,7 +55,7 @@ func GetTotalOfAccessLogs(query models.AccessLog) (int64, error) {
left join user u
on al.user_id = u.user_id
where al.project_id = ? and u.username like ? `
queryParam = append(queryParam, "%"+query.Username+"%")
queryParam = append(queryParam, "%"+escape(query.Username)+"%")
}
sql += genFilterClauses(query, &queryParam)
@ -82,7 +82,7 @@ func GetAccessLogs(query models.AccessLog, limit, offset int64) ([]models.Access
if query.Username != "" {
sql += ` and u.username like ? `
queryParam = append(queryParam, "%"+query.Username+"%")
queryParam = append(queryParam, "%"+escape(query.Username)+"%")
}
sql += genFilterClauses(query, &queryParam)

View File

@ -17,11 +17,11 @@ package dao
import (
"fmt"
"os"
"strings"
"sync"
"github.com/astaxie/beego/orm"
"github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/utils/log"
)
@ -52,42 +52,18 @@ func InitDatabase() {
}
func getDatabase() (db Database, err error) {
switch strings.ToLower(os.Getenv("DATABASE")) {
switch config.Database() {
case "", "mysql":
host, port, usr, pwd, database := getMySQLConnInfo()
db = NewMySQL(host, port, usr, pwd, database)
db = NewMySQL(config.MySQL().Host, config.MySQL().Port, config.MySQL().User,
config.MySQL().Password, config.MySQL().Database)
case "sqlite":
file := getSQLiteConnInfo()
db = NewSQLite(file)
db = NewSQLite(config.SQLite().FilePath)
default:
err = fmt.Errorf("invalid database: %s", os.Getenv("DATABASE"))
}
return
}
// TODO read from config
func getMySQLConnInfo() (host, port, username, password, database string) {
host = os.Getenv("MYSQL_HOST")
port = os.Getenv("MYSQL_PORT")
username = os.Getenv("MYSQL_USR")
password = os.Getenv("MYSQL_PWD")
database = os.Getenv("MYSQL_DATABASE")
if len(database) == 0 {
database = "registry"
err = fmt.Errorf("invalid database: %s", config.Database())
}
return
}
// TODO read from config
func getSQLiteConnInfo() string {
file := os.Getenv("SQLITE_FILE")
if len(file) == 0 {
file = "registry.db"
}
return file
}
var globalOrm orm.Ormer
var once sync.Once
@ -102,3 +78,9 @@ func GetOrmer() orm.Ormer {
func paginateForRawSQL(sql string, limit, offset int64) string {
return fmt.Sprintf("%s limit %d offset %d", sql, limit, offset)
}
func escape(str string) string {
str = strings.Replace(str, `%`, `\%`, -1)
str = strings.Replace(str, `_`, `\_`, -1)
return str
}

View File

@ -705,7 +705,7 @@ func TestGetProjectById(t *testing.T) {
func TestGetUserByProject(t *testing.T) {
pid := currentProject.ProjectID
u1 := models.User{
Username: "%%Tester%%",
Username: "Tester",
}
u2 := models.User{
Username: "nononono",

View File

@ -195,7 +195,7 @@ func GetTotalOfUserRelevantProjects(userID int, projectName string) (int64, erro
queryParam = append(queryParam, userID)
if projectName != "" {
sql += " and p.name like ? "
queryParam = append(queryParam, "%"+projectName+"%")
queryParam = append(queryParam, "%"+escape(projectName)+"%")
}
var total int64
@ -254,7 +254,7 @@ func getProjects(userID int, name string, args ...int64) ([]models.Project, erro
if name != "" {
sql += ` and p.name like ? `
queryParam = append(queryParam, "%"+name+"%")
queryParam = append(queryParam, "%"+escape(name)+"%")
}
switch len(args) {

View File

@ -71,7 +71,7 @@ func GetUserByProject(projectID int64, queryUser models.User) ([]models.User, er
if queryUser.Username != "" {
sql += " and u.username like ? "
queryParam = append(queryParam, queryUser.Username)
queryParam = append(queryParam, "%"+escape(queryUser.Username)+"%")
}
sql += ` order by u.user_id `
_, err := o.Raw(sql, queryParam).QueryRows(&u)

View File

@ -52,7 +52,7 @@ func Register(user models.User) (int64, error) {
func UserExists(user models.User, target string) (bool, error) {
if user.Username == "" && user.Email == "" {
return false, errors.New("User name and email are blank.")
return false, errors.New("user name and email are blank")
}
o := GetOrmer()

View File

@ -90,7 +90,7 @@ func FilterRepTargets(name string) ([]*models.RepTarget, error) {
sql := `select * from replication_target `
if len(name) != 0 {
sql += `where name like ? `
args = append(args, "%"+name+"%")
args = append(args, "%"+escape(name)+"%")
}
sql += `order by creation_time`
@ -166,11 +166,11 @@ func FilterRepPolicies(name string, projectID int64) ([]*models.RepPolicy, error
if len(name) != 0 && projectID != 0 {
sql += `and rp.name like ? and rp.project_id = ? `
args = append(args, "%"+name+"%")
args = append(args, "%"+escape(name)+"%")
args = append(args, projectID)
} else if len(name) != 0 {
sql += `and rp.name like ? `
args = append(args, "%"+name+"%")
args = append(args, "%"+escape(name)+"%")
} else if projectID != 0 {
sql += `and rp.project_id = ? `
args = append(args, projectID)

View File

@ -138,7 +138,7 @@ func GetTotalOfPublicRepositories(name string) (int64, error) {
on r.project_id = p.project_id and p.public = 1 `
if len(name) != 0 {
sql += ` where r.name like ?`
params = append(params, "%"+name+"%")
params = append(params, "%"+escape(name)+"%")
}
var total int64
@ -162,7 +162,7 @@ func GetTotalOfUserRelevantRepositories(userID int, name string) (int64, error)
params = append(params, userID)
if len(name) != 0 {
sql += ` where r.name like ?`
params = append(params, "%"+name+"%")
params = append(params, "%"+escape(name)+"%")
}
var total int64

View File

@ -101,7 +101,7 @@ func ListUsers(query models.User) ([]models.User, error) {
queryParam := make([]interface{}, 1)
if query.Username != "" {
sql += ` and username like ? `
queryParam = append(queryParam, query.Username)
queryParam = append(queryParam, "%"+escape(query.Username)+"%")
}
sql += ` order by user_id desc `
@ -131,7 +131,7 @@ func ToggleUserAdminRole(userID, hasAdmin int) error {
// ChangeUserPassword ...
func ChangeUserPassword(u models.User, oldPassword ...string) (err error) {
if len(oldPassword) > 1 {
return errors.New("Wrong numbers of params.")
return errors.New("wrong numbers of params")
}
o := GetOrmer()
@ -153,7 +153,7 @@ func ChangeUserPassword(u models.User, oldPassword ...string) (err error) {
return err
}
if c == 0 {
return errors.New("No record has been modified, change password failed.")
return errors.New("no record has been modified, change password failed")
}
return nil
@ -171,7 +171,7 @@ func ResetUserPassword(u models.User) error {
return err
}
if count == 0 {
return errors.New("No record be changed, reset password failed.")
return errors.New("no record be changed, reset password failed")
}
return nil
}

View File

@ -22,6 +22,8 @@ import (
"runtime"
"sync"
"time"
"github.com/vmware/harbor/src/common/config"
)
var logger = New(os.Stdout, NewTextFormatter(), WarningLevel)
@ -29,8 +31,7 @@ var logger = New(os.Stdout, NewTextFormatter(), WarningLevel)
func init() {
logger.callDepth = 4
// TODO add item in configuaration file
lvl := os.Getenv("LOG_LEVEL")
lvl := config.LogLevel()
if len(lvl) == 0 {
logger.SetLevel(InfoLevel)
return

View File

@ -134,7 +134,7 @@ func loadConfig() {
useTLS = true
}
mc = MailConfig{
Identity: "Mail Config",
Identity: config["identity"],
Host: config["host"],
Port: config["port"],
Username: config["username"],

View File

@ -21,15 +21,15 @@ import (
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
"sync"
"time"
token_util "github.com/vmware/harbor/src/ui/service/token"
"github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/common/utils/registry"
registry_error "github.com/vmware/harbor/src/common/utils/registry/error"
token_util "github.com/vmware/harbor/src/ui/service/token"
)
const (
@ -234,11 +234,11 @@ func (s *standardTokenAuthorizer) generateToken(realm, service string, scopes []
// 2. the realm field returned by registry is an IP which can not reachable
// inside Harbor
func tokenURL(realm string) string {
extEndpoint := os.Getenv("EXT_ENDPOINT")
tokenURL := os.Getenv("TOKEN_URL")
if len(extEndpoint) != 0 && len(tokenURL) != 0 &&
extEndpoint := config.ExtEndpoint()
tokenEndpoint := config.TokenEndpoint()
if len(extEndpoint) != 0 && len(tokenEndpoint) != 0 &&
strings.Contains(realm, extEndpoint) {
realm = strings.TrimRight(tokenURL, "/") + "/service/token"
realm = strings.TrimRight(tokenEndpoint, "/") + "/service/token"
}
return realm
}

View File

@ -64,7 +64,7 @@ func (sm *SM) EnterState(s string) (string, error) {
_, exist := targets[s]
_, isForced := sm.ForcedStates[s]
if !exist && !isForced {
return "", fmt.Errorf("Job id: %d, transition from %s to %s does not exist!", sm.JobID, sm.CurrentState, s)
return "", fmt.Errorf("job id: %d, transition from %s to %s does not exist", sm.JobID, sm.CurrentState, s)
}
exitHandler, ok := sm.Handlers[sm.CurrentState]
if ok {

View File

@ -1,9 +0,0 @@
package api
import (
"testing"
)
func TestMain(t *testing.T) {
}

View File

@ -16,18 +16,22 @@
package api
import (
"os"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"os"
)
const (
//Prepare Test info
TestUserName = "testUser0001"
TestUserPwd = "testUser0001"
TestUserEmail = "testUser0001@mydomain.com"
TestProName = "testProject0001"
TestTargetName = "testTarget0001"
TestUserName = "testUser0001"
TestUserPwd = "testUser0001"
TestUserEmail = "testUser0001@mydomain.com"
TestProName = "testProject0001"
TestTargetName = "testTarget0001"
TestRepoName = "testRepo0001"
AdminName = "admin"
DefaultProjectName = "library"
)
func CommonAddUser() {
@ -104,3 +108,20 @@ func CommonDelTarget() {
func CommonPolicyEabled(policyID int, enabled int) {
_ = dao.UpdateRepPolicyEnablement(int64(policyID), enabled)
}
func CommonAddRepository() {
commonRepository := &models.RepoRecord{
RepositoryID: "1",
Name: TestRepoName,
OwnerName: AdminName,
OwnerID: 1,
ProjectName: DefaultProjectName,
ProjectID: 1,
PullCount: 1,
}
_ = dao.AddRepository(*commonRepository)
}
func CommonDelRepository() {
_ = dao.DeleteRepository(TestRepoName)
}

View File

@ -174,19 +174,20 @@ func (a testapi) ProjectsPost(prjUsr usrInfo, project apilib.ProjectReq) (int, e
return httpStatusCode, err
}
func (a testapi) StatisticGet(user usrInfo) (apilib.StatisticMap, error) {
func (a testapi) StatisticGet(user usrInfo) (int, apilib.StatisticMap, error) {
_sling := sling.New().Get(a.basePath)
// create path and map variables
path := "/api/statistics/"
fmt.Printf("project statistic path: %s\n", path)
_sling = _sling.Path(path)
var successPayload = new(apilib.StatisticMap)
code, body, err := request(_sling, jsonAcceptHeader, user)
if 200 == code && nil == err {
var successPayload apilib.StatisticMap
httpStatusCode, body, err := request(_sling, jsonAcceptHeader, user)
if err == nil && httpStatusCode == 200 {
err = json.Unmarshal(body, &successPayload)
}
return *successPayload, err
return httpStatusCode, successPayload, err
}
func (a testapi) LogGet(user usrInfo, startTime, endTime, lines string) (int, []apilib.AccessLog, error) {
@ -857,7 +858,7 @@ func updateInitPassword(userID int, password string) error {
return fmt.Errorf("Failed to get user, userID: %d %v", userID, err)
}
if user == nil {
return fmt.Errorf("User id: %d does not exist.", userID)
return fmt.Errorf("user id: %d does not exist", userID)
}
if user.Salt == "" {
user.Salt = utils.GenerateRandomString()

View File

@ -85,7 +85,7 @@ func (pma *ProjectMemberAPI) Get() {
}
if pma.memberID == 0 { //member id not set return list of the members
username := pma.GetString("username")
queryUser := models.User{Username: "%" + username + "%"}
queryUser := models.User{Username: username}
userList, err := dao.GetUserByProject(pid, queryUser)
if err != nil {
log.Errorf("Failed to query database for member list, error: %v", err)

View File

@ -24,6 +24,7 @@ import (
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/config"
"strconv"
"time"
@ -72,11 +73,19 @@ func (p *ProjectAPI) Prepare() {
// Post ...
func (p *ProjectAPI) Post() {
p.userID = p.ValidateUser()
isSysAdmin, err := dao.IsAdminRole(p.userID)
if err != nil {
log.Errorf("Failed to check admin role: %v", err)
}
if !isSysAdmin && config.OnlyAdminCreateProject() {
log.Errorf("Only sys admin can create project")
p.RenderError(http.StatusForbidden, "Only system admin can create project")
return
}
var req projectReq
p.DecodeJSONReq(&req)
public := req.Public
err := validateProjectReq(req)
err = validateProjectReq(req)
if err != nil {
log.Errorf("Invalid project request, error: %v", err)
p.RenderError(http.StatusBadRequest, fmt.Sprintf("invalid request: %v", err))
@ -413,7 +422,7 @@ func validateProjectReq(req projectReq) error {
validProjectName := regexp.MustCompile(`^[a-z0-9](?:-*[a-z0-9])*(?:[._][a-z0-9](?:-*[a-z0-9])*)*$`)
legal := validProjectName.MatchString(pn)
if !legal {
return fmt.Errorf("Project name is not in lower case or contains illegal characters!")
return fmt.Errorf("project name is not in lower case or contains illegal characters")
}
return nil
}

View File

@ -19,23 +19,23 @@ import (
"fmt"
"io/ioutil"
"net/http"
"os"
"sort"
"github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/manifest/schema2"
"github.com/vmware/harbor/src/common/api"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/common/utils/registry"
"github.com/vmware/harbor/src/ui/service/cache"
svc_utils "github.com/vmware/harbor/src/ui/service/utils"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/common/api"
"github.com/vmware/harbor/src/common/utils/registry"
registry_error "github.com/vmware/harbor/src/common/utils/registry/error"
"github.com/vmware/harbor/src/common/utils"
"github.com/vmware/harbor/src/common/utils/registry/auth"
"github.com/vmware/harbor/src/ui/config"
)
// RepositoryAPI handles request to /api/repositories /api/repositories/tags /api/repositories/manifests, the parm has to be put
@ -361,7 +361,7 @@ func (ra *RepositoryAPI) GetManifests() {
}
func (ra *RepositoryAPI) initRepositoryClient(repoName string) (r *registry.Repository, err error) {
endpoint := os.Getenv("REGISTRY_URL")
endpoint := config.InternalRegistryURL()
username, password, ok := ra.Ctx.Request.BasicAuth()
if ok {

View File

@ -2,14 +2,14 @@ package api
import (
"fmt"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/harbor/tests/apitests/apilib"
//"github.com/vmware/harbor/tests/apitests/apilib"
)
func TestStatisticGet(t *testing.T) {
fmt.Println("Testing Statistic API")
assert := assert.New(t)
@ -17,69 +17,60 @@ func TestStatisticGet(t *testing.T) {
//prepare for test
var myProCount, pubProCount, totalProCount int32
result, err := apiTest.StatisticGet(*admin)
var priMyProjectCount, priMyRepoCount int32
var priPublicProjectCount, priPublicRepoCount int32
var priTotalProjectCount, priTotalRepoCount int32
//case 1: case 1: user not login, expect fail to get status info.
fmt.Println("case 1: user not login, expect fail to get status info.")
httpStatusCode, result, err := apiTest.StatisticGet(*unknownUsr)
if err != nil {
t.Error("Error get statistic info.", err.Error())
t.Log(err)
} else {
assert.Equal(httpStatusCode, int(401), "Case 1: Get status info without login. (401)")
}
//case 2: admin successful login, expect get status info successful.
fmt.Println("case 2: admin successful login, expect get status info successful.")
httpStatusCode, result, err = apiTest.StatisticGet(*admin)
if err != nil {
t.Error("Error get statistic info.", err.Error())
t.Log(err)
} else {
assert.Equal(httpStatusCode, int(200), "Case 2: Get status info with admin login. (200)")
//fmt.Println("pri status data %+v", result)
priMyProjectCount = result.MyProjectCount
priMyRepoCount = result.MyRepoCount
priPublicProjectCount = result.PublicProjectCount
priPublicRepoCount = result.PublicRepoCount
priTotalProjectCount = result.TotalProjectCount
priTotalRepoCount = result.TotalRepoCount
}
//case 3: status info increased after add more project and repo.
fmt.Println("case 3: status info increased after add more project and repo.")
CommonAddProject()
CommonAddRepository()
httpStatusCode, result, err = apiTest.StatisticGet(*admin)
//fmt.Println("new status data %+v", result)
if err != nil {
t.Error("Error while get statistic information", err.Error())
t.Log(err)
} else {
myProCount = result.MyProjectCount
pubProCount = result.PublicProjectCount
totalProCount = result.TotalProjectCount
}
//post project
var project apilib.ProjectReq
project.ProjectName = "statistic_project"
project.Public = 1
//case 2: admin successful login, expect project creation success.
fmt.Println("case 2: admin successful login, expect project creation success.")
reply, err := apiTest.ProjectsPost(*admin, project)
if err != nil {
t.Error("Error while creat project", err.Error())
t.Log(err)
} else {
assert.Equal(reply, int(201), "Case 2: Project creation status should be 201")
}
//get and compare
result, err = apiTest.StatisticGet(*admin)
if err != nil {
t.Error("Error while get statistic information", err.Error())
t.Log(err)
} else {
assert.Equal(myProCount+1, result.MyProjectCount, "MyProjectCount should be equal")
assert.Equal(int32(2), result.MyRepoCount, "MyRepoCount should be equal")
assert.Equal(pubProCount+1, result.PublicProjectCount, "PublicProjectCount should be equal")
assert.Equal(int32(2), result.PublicRepoCount, "PublicRepoCount should be equal")
assert.Equal(totalProCount+1, result.TotalProjectCount, "TotalProCount should be equal")
assert.Equal(int32(2), result.TotalRepoCount, "TotalRepoCount should be equal")
assert.Equal(priMyProjectCount+1, result.MyProjectCount, "MyProjectCount should be +1")
assert.Equal(priMyRepoCount+1, result.MyRepoCount, "MyRepoCount should be +1")
assert.Equal(priPublicProjectCount, result.PublicProjectCount, "PublicProjectCount should be equal")
assert.Equal(priPublicRepoCount+1, result.PublicRepoCount, "PublicRepoCount should be +1")
assert.Equal(priTotalProjectCount+1, result.TotalProjectCount, "TotalProCount should be +1")
assert.Equal(priTotalRepoCount+1, result.TotalRepoCount, "TotalRepoCount should be +1")
}
//get the project
var projects []apilib.Project
var addProjectID int32
httpStatusCode, projects, err := apiTest.ProjectsGet(project.ProjectName, 1)
if err != nil {
t.Error("Error while search project by proName and isPublic", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
addProjectID = projects[0].ProjectId
}
//delete the project
projectID := strconv.Itoa(int(addProjectID))
httpStatusCode, err = apiTest.ProjectsDelete(*admin, projectID)
if err != nil {
t.Error("Error while delete project", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "Case 1: Project creation status should be 200")
//t.Log(result)
}
fmt.Printf("\n")
//delete the project and repo
CommonDelProject()
CommonDelRepository()
}

84
src/ui/api/systeminfo.go Normal file
View File

@ -0,0 +1,84 @@
package api
import (
"net/http"
"os"
"path/filepath"
"syscall"
"github.com/vmware/harbor/src/common/api"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/utils/log"
)
//SystemInfoAPI handle requests for getting system info /api/systeminfo
type SystemInfoAPI struct {
api.BaseAPI
currentUserID int
isAdmin bool
}
const harborStoragePath = "/harbor_storage"
const defaultRootCert = "/harbor_storage/ca_download/ca.crt"
//SystemInfo models for system info.
type SystemInfo struct {
HarborStorage Storage `json:"storage"`
}
//Storage models for storage.
type Storage struct {
Total uint64 `json:"total"`
Free uint64 `json:"free"`
}
// Prepare for validating user if an admin.
func (sia *SystemInfoAPI) Prepare() {
sia.currentUserID = sia.ValidateUser()
var err error
sia.isAdmin, err = dao.IsAdminRole(sia.currentUserID)
if err != nil {
log.Errorf("Error occurred in IsAdminRole:%v", err)
sia.CustomAbort(http.StatusInternalServerError, "Internal error.")
}
}
// GetVolumeInfo gets specific volume storage info.
func (sia *SystemInfoAPI) GetVolumeInfo() {
if !sia.isAdmin {
sia.RenderError(http.StatusForbidden, "User does not have admin role.")
return
}
var stat syscall.Statfs_t
err := syscall.Statfs(filepath.Join("/", harborStoragePath), &stat)
if err != nil {
log.Errorf("Error occurred in syscall.Statfs: %v", err)
sia.CustomAbort(http.StatusInternalServerError, "Internal error.")
return
}
systemInfo := SystemInfo{
HarborStorage: Storage{
Total: stat.Blocks * uint64(stat.Bsize),
Free: stat.Bavail * uint64(stat.Bsize),
},
}
sia.Data["json"] = systemInfo
sia.ServeJSON()
}
//GetCert gets default self-signed certificate.
func (sia *SystemInfoAPI) GetCert() {
if sia.isAdmin {
if _, err := os.Stat(defaultRootCert); !os.IsNotExist(err) {
sia.Ctx.Output.Header("Content-Disposition", "attachment; filename=ca.crt")
http.ServeFile(sia.Ctx.ResponseWriter, sia.Ctx.Request, defaultRootCert)
} else {
log.Error("No certificate found.")
sia.CustomAbort(http.StatusNotFound, "No certificate found.")
}
}
sia.CustomAbort(http.StatusUnauthorized, "")
}

View File

@ -20,17 +20,17 @@ import (
"net"
"net/http"
"net/url"
"os"
"strconv"
"github.com/vmware/harbor/src/common/api"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/common/api"
"github.com/vmware/harbor/src/common/utils/registry"
"github.com/vmware/harbor/src/common/utils/registry/auth"
registry_error "github.com/vmware/harbor/src/common/utils/registry/error"
"github.com/vmware/harbor/src/ui/config"
)
// TargetAPI handles request to /api/targets/ping /api/targets/{}
@ -41,8 +41,7 @@ type TargetAPI struct {
// Prepare validates the user
func (t *TargetAPI) Prepare() {
//TODO:move to config
t.secretKey = os.Getenv("SECRET_KEY")
t.secretKey = config.SecretKey()
userID := t.ValidateUser()
isSysAdmin, err := dao.IsAdminRole(userID)

View File

@ -18,7 +18,6 @@ package api
import (
"fmt"
"net/http"
"os"
"regexp"
"strconv"
"strings"
@ -27,6 +26,7 @@ import (
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/config"
)
// UserAPI handles request to /api/users/{}
@ -47,16 +47,9 @@ type passwordReq struct {
// Prepare validates the URL and parms
func (ua *UserAPI) Prepare() {
authMode := strings.ToLower(os.Getenv("AUTH_MODE"))
if authMode == "" {
authMode = "db_auth"
}
ua.AuthMode = authMode
ua.AuthMode = config.AuthMode()
selfRegistration := strings.ToLower(os.Getenv("SELF_REGISTRATION"))
if selfRegistration == "on" {
ua.SelfRegistration = true
}
ua.SelfRegistration = config.SelfRegistration()
if ua.Ctx.Input.IsPost() {
sessionUserID := ua.GetSession("userId")
@ -109,7 +102,7 @@ func (ua *UserAPI) Get() {
username := ua.GetString("username")
userQuery := models.User{}
if len(username) > 0 {
userQuery.Username = "%" + username + "%"
userQuery.Username = username
}
userList, err := dao.ListUsers(userQuery)
if err != nil {
@ -241,9 +234,7 @@ func (ua *UserAPI) Delete() {
return
}
// TODO read from conifg
authMode := os.Getenv("AUTH_MODE")
if authMode == "ldap_auth" {
if config.AuthMode() == "ldap_auth" {
ua.CustomAbort(http.StatusForbidden, "user can not be deleted in LDAP authentication mode")
}
@ -323,13 +314,13 @@ func (ua *UserAPI) ToggleUserAdminRole() {
func validate(user models.User) error {
if isIllegalLength(user.Username, 1, 20) {
return fmt.Errorf("Username with illegal length.")
return fmt.Errorf("username with illegal length")
}
if isContainIllegalChar(user.Username, []string{",", "~", "#", "$", "%"}) {
return fmt.Errorf("Username contains illegal characters.")
return fmt.Errorf("username contains illegal characters")
}
if isIllegalLength(user.Password, 8, 20) {
return fmt.Errorf("Password with illegal length.")
return fmt.Errorf("password with illegal length")
}
if err := commonValidate(user); err != nil {
return err
@ -342,21 +333,21 @@ func commonValidate(user models.User) error {
if len(user.Email) > 0 {
if m, _ := regexp.MatchString(`^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, user.Email); !m {
return fmt.Errorf("Email with illegal format.")
return fmt.Errorf("email with illegal format")
}
} else {
return fmt.Errorf("Email can't be empty")
}
if isIllegalLength(user.Realname, 0, 20) {
return fmt.Errorf("Realname with illegal length.")
return fmt.Errorf("realname with illegal length")
}
if isContainIllegalChar(user.Realname, []string{",", "~", "#", "$", "%"}) {
return fmt.Errorf("Realname contains illegal characters.")
return fmt.Errorf("realname contains illegal characters")
}
if isIllegalLength(user.Comment, -1, 30) {
return fmt.Errorf("Comment with illegal length.")
return fmt.Errorf("comment with illegal length")
}
return nil

View File

@ -22,18 +22,18 @@ import (
"io/ioutil"
"net"
"net/http"
"os"
"sort"
"strings"
"time"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/ui/service/cache"
"github.com/vmware/harbor/src/common/utils"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/common/utils/registry"
registry_error "github.com/vmware/harbor/src/common/utils/registry/error"
"github.com/vmware/harbor/src/ui/config"
"github.com/vmware/harbor/src/ui/service/cache"
)
func checkProjectPermission(userID int, projectID int64) bool {
@ -233,9 +233,8 @@ func postReplicationAction(policyID int64, acton string) error {
func addAuthentication(req *http.Request) {
if req != nil {
req.AddCookie(&http.Cookie{
Name: models.UISecretCookie,
// TODO read secret from config
Value: os.Getenv("UI_SECRET"),
Name: models.UISecretCookie,
Value: config.UISecret(),
})
}
}
@ -351,8 +350,7 @@ func diffRepos(reposInRegistry []string, reposInDB []string) ([]string, []string
}
// TODO remove the workaround when the bug of registry is fixed
// TODO read it from config
endpoint := os.Getenv("REGISTRY_URL")
endpoint := config.InternalRegistryURL()
client, err := cache.NewRepositoryClient(endpoint, true,
"admin", repoInR, "repository", repoInR)
if err != nil {
@ -374,8 +372,7 @@ func diffRepos(reposInRegistry []string, reposInDB []string) ([]string, []string
j++
} else {
// TODO remove the workaround when the bug of registry is fixed
// TODO read it from config
endpoint := os.Getenv("REGISTRY_URL")
endpoint := config.InternalRegistryURL()
client, err := cache.NewRepositoryClient(endpoint, true,
"admin", repoInR, "repository", repoInR)
if err != nil {
@ -425,7 +422,7 @@ func projectExists(repository string) (bool, error) {
}
func initRegistryClient() (r *registry.Registry, err error) {
endpoint := os.Getenv("REGISTRY_URL")
endpoint := config.InternalRegistryURL()
addr := endpoint
if strings.Contains(endpoint, "/") {
@ -462,32 +459,20 @@ func initRegistryClient() (r *registry.Registry, err error) {
}
func buildReplicationURL() string {
url := getJobServiceURL()
url := config.InternalJobServiceURL()
return fmt.Sprintf("%s/api/jobs/replication", url)
}
func buildJobLogURL(jobID string) string {
url := getJobServiceURL()
url := config.InternalJobServiceURL()
return fmt.Sprintf("%s/api/jobs/replication/%s/log", url, jobID)
}
func buildReplicationActionURL() string {
url := getJobServiceURL()
url := config.InternalJobServiceURL()
return fmt.Sprintf("%s/api/jobs/replication/actions", url)
}
func getJobServiceURL() string {
url := os.Getenv("JOB_SERVICE_URL")
url = strings.TrimSpace(url)
url = strings.TrimRight(url, "/")
if len(url) == 0 {
url = "http://jobservice"
}
return url
}
func getReposByProject(name string, keyword ...string) ([]string, error) {
repositories := []string{}

View File

@ -17,11 +17,11 @@ package auth
import (
"fmt"
"github.com/vmware/harbor/src/common/utils/log"
"os"
"time"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/config"
)
// 1.5 seconds
@ -50,7 +50,7 @@ func Register(name string, authenticator Authenticator) {
// Login authenticates user credentials based on setting.
func Login(m models.AuthModel) (*models.User, error) {
var authMode = os.Getenv("AUTH_MODE")
var authMode = config.AuthMode()
if authMode == "" || m.Principal == "admin" {
authMode = "db_auth"
}

View File

@ -18,14 +18,14 @@ package ldap
import (
"errors"
"fmt"
"os"
"strings"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/auth"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/ui/auth"
"github.com/vmware/harbor/src/ui/config"
"github.com/mqu/openldap"
)
@ -46,9 +46,9 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
return nil, fmt.Errorf("the principal contains meta char: %q", c)
}
}
ldapURL := os.Getenv("LDAP_URL")
ldapURL := config.LDAP().URL
if ldapURL == "" {
return nil, errors.New("Can not get any available LDAP_URL.")
return nil, errors.New("can not get any available LDAP_URL")
}
log.Debug("ldapURL:", ldapURL)
ldap, err := openldap.Initialize(ldapURL)
@ -57,16 +57,16 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
}
ldap.SetOption(openldap.LDAP_OPT_PROTOCOL_VERSION, openldap.LDAP_VERSION3)
ldapBaseDn := os.Getenv("LDAP_BASE_DN")
ldapBaseDn := config.LDAP().BaseDn
if ldapBaseDn == "" {
return nil, errors.New("Can not get any available LDAP_BASE_DN.")
return nil, errors.New("can not get any available LDAP_BASE_DN")
}
log.Debug("baseDn:", ldapBaseDn)
ldapSearchDn := os.Getenv("LDAP_SEARCH_DN")
ldapSearchDn := config.LDAP().SearchDn
if ldapSearchDn != "" {
log.Debug("Search DN: ", ldapSearchDn)
ldapSearchPwd := os.Getenv("LDAP_SEARCH_PWD")
ldapSearchPwd := config.LDAP().SearchPwd
err = ldap.Bind(ldapSearchDn, ldapSearchPwd)
if err != nil {
log.Debug("Bind search dn error", err)
@ -74,8 +74,8 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
}
}
attrName := os.Getenv("LDAP_UID")
filter := os.Getenv("LDAP_FILTER")
attrName := config.LDAP().UID
filter := config.LDAP().Filter
if filter != "" {
filter = "(&" + filter + "(" + attrName + "=" + m.Principal + "))"
} else {
@ -83,7 +83,7 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
}
log.Debug("one or more filter", filter)
ldapScope := os.Getenv("LDAP_SCOPE")
ldapScope := config.LDAP().Scope
var scope int
if ldapScope == "1" {
scope = openldap.LDAP_SCOPE_BASE

155
src/ui/config/config.go Normal file
View File

@ -0,0 +1,155 @@
/*
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 config provides methods to get configurations required by code in src/ui
package config
import (
"strconv"
"strings"
commonConfig "github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/utils/log"
)
// LDAPSetting wraps the setting of an LDAP server
type LDAPSetting struct {
URL string
BaseDn string
SearchDn string
SearchPwd string
UID string
Filter string
Scope string
}
type uiParser struct{}
// Parse parses the auth settings url settings and other configuration consumed by code under src/ui
func (up *uiParser) Parse(raw map[string]string, config map[string]interface{}) error {
mode := raw["AUTH_MODE"]
if mode == "ldap_auth" {
setting := LDAPSetting{
URL: raw["LDAP_URL"],
BaseDn: raw["LDAP_BASE_DN"],
SearchDn: raw["LDAP_SEARCH_DN"],
SearchPwd: raw["LDAP_SEARCH_PWD"],
UID: raw["LDAP_UID"],
Filter: raw["LDAP_FILTER"],
Scope: raw["LDAP_SCOPE"],
}
config["ldap"] = setting
}
config["auth_mode"] = mode
var tokenExpiration = 30 //minutes
if len(raw["TOKEN_EXPIRATION"]) > 0 {
i, err := strconv.Atoi(raw["TOKEN_EXPIRATION"])
if err != nil {
log.Warningf("failed to parse token expiration: %v, using default value %d", err, tokenExpiration)
} else if i <= 0 {
log.Warningf("invalid token expiration, using default value: %d minutes", tokenExpiration)
} else {
tokenExpiration = i
}
}
config["token_exp"] = tokenExpiration
config["admin_password"] = raw["HARBOR_ADMIN_PASSWORD"]
config["ext_reg_url"] = raw["EXT_REG_URL"]
config["ui_secret"] = raw["UI_SECRET"]
config["secret_key"] = raw["SECRET_KEY"]
config["self_registration"] = raw["SELF_REGISTRATION"] != "off"
config["admin_create_project"] = strings.ToLower(raw["PROJECT_CREATION_RESTRICTION"]) == "adminonly"
registryURL := raw["REGISTRY_URL"]
registryURL = strings.TrimRight(registryURL, "/")
config["internal_registry_url"] = registryURL
jobserviceURL := raw["JOB_SERVICE_URL"]
jobserviceURL = strings.TrimRight(jobserviceURL, "/")
config["internal_jobservice_url"] = jobserviceURL
return nil
}
var uiConfig *commonConfig.Config
func init() {
uiKeys := []string{"AUTH_MODE", "LDAP_URL", "LDAP_BASE_DN", "LDAP_SEARCH_DN", "LDAP_SEARCH_PWD", "LDAP_UID", "LDAP_FILTER", "LDAP_SCOPE", "TOKEN_EXPIRATION", "HARBOR_ADMIN_PASSWORD", "EXT_REG_URL", "UI_SECRET", "SECRET_KEY", "SELF_REGISTRATION", "PROJECT_CREATION_RESTRICTION", "REGISTRY_URL", "JOB_SERVICE_URL"}
uiConfig = &commonConfig.Config{
Config: make(map[string]interface{}),
Loader: &commonConfig.EnvConfigLoader{Keys: uiKeys},
Parser: &uiParser{},
}
if err := uiConfig.Load(); err != nil {
panic(err)
}
}
// Reload ...
func Reload() error {
return uiConfig.Load()
}
// AuthMode ...
func AuthMode() string {
return uiConfig.Config["auth_mode"].(string)
}
// LDAP returns the setting of ldap server
func LDAP() LDAPSetting {
return uiConfig.Config["ldap"].(LDAPSetting)
}
// TokenExpiration returns the token expiration time (in minute)
func TokenExpiration() int {
return uiConfig.Config["token_exp"].(int)
}
// ExtRegistryURL returns the registry URL to exposed to external client
func ExtRegistryURL() string {
return uiConfig.Config["ext_reg_url"].(string)
}
// UISecret returns the value of UI secret cookie, used for communication between UI and JobService
func UISecret() string {
return uiConfig.Config["ui_secret"].(string)
}
// SecretKey returns the secret key to encrypt the password of target
func SecretKey() string {
return uiConfig.Config["secret_key"].(string)
}
// SelfRegistration returns the enablement of self registration
func SelfRegistration() bool {
return uiConfig.Config["self_registration"].(bool)
}
// InternalRegistryURL returns registry URL for internal communication between Harbor containers
func InternalRegistryURL() string {
return uiConfig.Config["internal_registry_url"].(string)
}
// InternalJobServiceURL returns jobservice URL for internal communication between Harbor containers
func InternalJobServiceURL() string {
return uiConfig.Config["internal_jobservice_url"].(string)
}
// InitialAdminPassword returns the initial password for administrator
func InitialAdminPassword() string {
return uiConfig.Config["admin_password"].(string)
}
// OnlyAdminCreateProject returns the flag to restrict that only sys admin can create project
func OnlyAdminCreateProject() bool {
return uiConfig.Config["admin_create_project"].(bool)
}

View File

@ -0,0 +1,144 @@
/*
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 config
import (
"os"
"testing"
)
var (
auth = "ldap_auth"
ldap = LDAPSetting{
"ldap://test.ldap.com",
"ou=people",
"dc=whatever,dc=org",
"1234567",
"cn",
"uid",
"2",
}
tokenExp = "3"
tokenExpRes = 3
adminPassword = "password"
externalRegURL = "127.0.0.1"
uiSecret = "ffadsdfsdf"
secretKey = "keykey"
selfRegistration = "off"
projectCreationRestriction = "adminonly"
internalRegistryURL = "http://registry:5000"
jobServiceURL = "http://jobservice"
)
func TestMain(m *testing.M) {
os.Setenv("AUTH_MODE", auth)
os.Setenv("LDAP_URL", ldap.URL)
os.Setenv("LDAP_BASE_DN", ldap.BaseDn)
os.Setenv("LDAP_SEARCH_DN", ldap.SearchDn)
os.Setenv("LDAP_SEARCH_PWD", ldap.SearchPwd)
os.Setenv("LDAP_UID", ldap.UID)
os.Setenv("LDAP_SCOPE", ldap.Scope)
os.Setenv("LDAP_FILTER", ldap.Filter)
os.Setenv("TOKEN_EXPIRATION", tokenExp)
os.Setenv("HARBOR_ADMIN_PASSWORD", adminPassword)
os.Setenv("EXT_REG_URL", externalRegURL)
os.Setenv("UI_SECRET", uiSecret)
os.Setenv("SECRET_KEY", secretKey)
os.Setenv("SELF_REGISTRATION", selfRegistration)
os.Setenv("PROJECT_CREATION_RESTRICTION", projectCreationRestriction)
os.Setenv("REGISTRY_URL", internalRegistryURL)
os.Setenv("JOB_SERVICE_URL", jobServiceURL)
err := Reload()
if err != nil {
panic(err)
}
rc := m.Run()
os.Unsetenv("AUTH_MODE")
os.Unsetenv("LDAP_URL")
os.Unsetenv("LDAP_BASE_DN")
os.Unsetenv("LDAP_SEARCH_DN")
os.Unsetenv("LDAP_SEARCH_PWD")
os.Unsetenv("LDAP_UID")
os.Unsetenv("LDAP_SCOPE")
os.Unsetenv("LDAP_FILTER")
os.Unsetenv("TOKEN_EXPIRATION")
os.Unsetenv("HARBOR_ADMIN_PASSWORD")
os.Unsetenv("EXT_REG_URL")
os.Unsetenv("UI_SECRET")
os.Unsetenv("SECRET_KEY")
os.Unsetenv("SELF_REGISTRATION")
os.Unsetenv("CREATE_PROJECT_RESTRICTION")
os.Unsetenv("REGISTRY_URL")
os.Unsetenv("JOB_SERVICE_URL")
os.Exit(rc)
}
func TestAuth(t *testing.T) {
if AuthMode() != auth {
t.Errorf("Expected auth mode:%s, in fact: %s", auth, AuthMode())
}
if LDAP() != ldap {
t.Errorf("Expected ldap setting: %+v, in fact: %+v", ldap, LDAP())
}
}
func TestTokenExpiration(t *testing.T) {
if TokenExpiration() != tokenExpRes {
t.Errorf("Expected token expiration: %d, in fact: %d", tokenExpRes, TokenExpiration())
}
}
func TestURLs(t *testing.T) {
if InternalRegistryURL() != internalRegistryURL {
t.Errorf("Expected internal Registry URL: %s, in fact: %s", internalRegistryURL, InternalRegistryURL())
}
if InternalJobServiceURL() != jobServiceURL {
t.Errorf("Expected internal jobservice URL: %s, in fact: %s", jobServiceURL, InternalJobServiceURL())
}
if ExtRegistryURL() != externalRegURL {
t.Errorf("Expected External Registry URL: %s, in fact: %s", externalRegURL, ExtRegistryURL())
}
}
func TestSelfRegistration(t *testing.T) {
if SelfRegistration() {
t.Errorf("Expected Self Registration to be false")
}
}
func TestSecrets(t *testing.T) {
if SecretKey() != secretKey {
t.Errorf("Expected Secrect Key :%s, in fact: %s", secretKey, SecretKey())
}
if UISecret() != uiSecret {
t.Errorf("Expected UI Secret: %s, in fact: %s", uiSecret, UISecret())
}
}
func TestProjectCreationRestrict(t *testing.T) {
if !OnlyAdminCreateProject() {
t.Errorf("Expected OnlyAdminCreateProject to be true")
}
}
func TestInitAdminPassword(t *testing.T) {
if InitialAdminPassword() != adminPassword {
t.Errorf("Expected adminPassword: %s, in fact: %s", adminPassword, InitialAdminPassword())
}
}

View File

@ -9,6 +9,9 @@ type AccountSettingController struct {
func (asc *AccountSettingController) Get() {
var isAdminForLdap bool
sessionUserID, ok := asc.GetSession("userId").(int)
if !ok {
asc.Redirect("/", 302)
}
if ok && sessionUserID == 1 {
isAdminForLdap = true
}

View File

@ -12,6 +12,7 @@ import (
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/auth"
"github.com/vmware/harbor/src/ui/config"
)
// BaseController wraps common methods such as i18n support, forward, which can be leveraged by other UI render controllers.
@ -30,9 +31,10 @@ type langType struct {
}
const (
viewPath = "sections"
prefixNg = ""
defaultLang = "en-US"
viewPath = "sections"
prefixNg = ""
defaultLang = "en-US"
defaultRootCert = "/harbor_storage/ca_download/ca.crt"
)
var supportLanguages map[string]langType
@ -42,19 +44,21 @@ var mappingLangNames map[string]string
func (b *BaseController) Prepare() {
var lang string
var langHasChanged bool
langCookie, err := b.Ctx.Request.Cookie("language")
if err != nil {
log.Errorf("Error occurred in Request.Cookie: %v", err)
}
if langCookie != nil {
lang = langCookie.Value
}
if len(lang) == 0 {
sessionLang := b.GetSession("lang")
if sessionLang != nil {
b.SetSession("Lang", lang)
lang = sessionLang.(string)
var showDownloadCert bool
langRequest := b.GetString("lang")
if langRequest != "" {
lang = langRequest
langHasChanged = true
} else {
langCookie, err := b.Ctx.Request.Cookie("language")
if err != nil {
log.Errorf("Error occurred in Request.Cookie: %v", err)
}
if langCookie != nil {
lang = langCookie.Value
} else {
al := b.Ctx.Request.Header.Get("Accept-Language")
if len(al) > 4 {
@ -63,16 +67,23 @@ func (b *BaseController) Prepare() {
lang = al
}
}
langHasChanged = true
}
}
if _, exist := supportLanguages[lang]; !exist { //Check if support the request language.
lang = defaultLang //Set default language if not supported.
if langHasChanged {
if _, exist := supportLanguages[lang]; !exist { //Check if support the request language.
lang = defaultLang //Set default language if not supported.
}
cookies := &http.Cookie{
Name: "language",
Value: lang,
HttpOnly: true,
Path: "/",
}
http.SetCookie(b.Ctx.ResponseWriter, cookies)
}
b.Ctx.SetCookie("language", lang, 0, "/")
b.SetSession("Lang", lang)
curLang := langType{
Lang: lang,
}
@ -92,7 +103,7 @@ func (b *BaseController) Prepare() {
b.Data["CurLang"] = curLang.Name
b.Data["RestLangs"] = restLangs
authMode := strings.ToLower(os.Getenv("AUTH_MODE"))
authMode := config.AuthMode()
if authMode == "" {
authMode = "db_auth"
}
@ -104,15 +115,28 @@ func (b *BaseController) Prepare() {
b.UseCompressedJS = true
}
if _, err := os.Stat(filepath.Join("static", "resources", "js", "harbor.app.min.js")); os.IsNotExist(err) {
m, err := filepath.Glob(filepath.Join("static", "resources", "js", "harbor.app.min.*.js"))
if err != nil || len(m) == 0 {
b.UseCompressedJS = false
}
selfRegistration := strings.ToLower(os.Getenv("SELF_REGISTRATION"))
if selfRegistration == "on" {
b.SelfRegistration = true
b.SelfRegistration = config.SelfRegistration()
b.Data["SelfRegistration"] = config.SelfRegistration()
sessionUserID := b.GetSession("userId")
if sessionUserID != nil {
isAdmin, err := dao.IsAdminRole(sessionUserID.(int))
if err != nil {
log.Errorf("Error occurred in IsAdminRole: %v", err)
}
if isAdmin {
if _, err := os.Stat(defaultRootCert); !os.IsNotExist(err) {
showDownloadCert = true
}
}
}
b.Data["SelfRegistration"] = b.SelfRegistration
b.Data["ShowDownloadCert"] = showDownloadCert
}
// Forward to setup layout and template for content for a page.

View File

@ -9,6 +9,9 @@ type ChangePasswordController struct {
func (cpc *ChangePasswordController) Get() {
var isAdminForLdap bool
sessionUserID, ok := cpc.GetSession("userId").(int)
if !ok {
cpc.Redirect("/", 302)
}
if ok && sessionUserID == 1 {
isAdminForLdap = true
}

View File

@ -112,12 +112,12 @@ func TestMain(t *testing.T) {
r, _ = http.NewRequest("GET", "/account_setting", nil)
w = httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
assert.Equal(int(200), w.Code, "'/account_setting' httpStatusCode should be 200")
assert.Equal(int(302), w.Code, "'/account_setting' httpStatusCode should be 302")
r, _ = http.NewRequest("GET", "/change_password", nil)
w = httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
assert.Equal(int(200), w.Code, "'/change_password' httpStatusCode should be 200")
assert.Equal(int(302), w.Code, "'/change_password' httpStatusCode should be 302")
r, _ = http.NewRequest("GET", "/admin_option", nil)
w = httptest.NewRecorder()

View File

@ -3,11 +3,11 @@ package controllers
import (
"bytes"
"net/http"
"os"
"regexp"
"text/template"
"github.com/astaxie/beego"
"github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils"
@ -49,7 +49,7 @@ func (cc *CommonController) SendEmail() {
message := new(bytes.Buffer)
harborURL := os.Getenv("HARBOR_URL")
harborURL := config.ExtEndpoint()
if harborURL == "" {
harborURL = "localhost"
}

View File

@ -1,5 +1,11 @@
package controllers
import (
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/config"
)
// ProjectController handles requests to /project
type ProjectController struct {
BaseController
@ -7,5 +13,16 @@ type ProjectController struct {
// Get renders project page
func (pc *ProjectController) Get() {
var err error
isSysAdmin := false
uid := pc.GetSession("userId")
if uid != nil {
isSysAdmin, err = dao.IsAdminRole(uid)
if err != nil {
log.Warningf("Error in checking Admin Role for user, id: %d, error: %v", uid, err)
isSysAdmin = false
}
}
pc.Data["CanCreate"] = !config.OnlyAdminCreateProject() || isSysAdmin
pc.Forward("page_title_project", "project.htm")
}

View File

@ -1,6 +1,8 @@
package controllers
import "os"
import (
"github.com/vmware/harbor/src/ui/config"
)
// RepositoryController handles request to /repository
type RepositoryController struct {
@ -9,6 +11,6 @@ type RepositoryController struct {
// Get renders repository page
func (rc *RepositoryController) Get() {
rc.Data["HarborRegUrl"] = os.Getenv("HARBOR_REG_URL")
rc.Data["HarborRegUrl"] = config.ExtRegistryURL()
rc.Forward("page_title_repository", "repository.htm")
}

View File

@ -25,11 +25,12 @@ import (
"github.com/astaxie/beego"
_ "github.com/astaxie/beego/session/redis"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/ui/api"
_ "github.com/vmware/harbor/src/ui/auth/db"
_ "github.com/vmware/harbor/src/ui/auth/ldap"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/ui/config"
)
const (
@ -43,7 +44,7 @@ func updateInitPassword(userID int, password string) error {
return fmt.Errorf("Failed to get user, userID: %d %v", userID, err)
}
if user == nil {
return fmt.Errorf("User id: %d does not exist.", userID)
return fmt.Errorf("user id: %d does not exist", userID)
}
if user.Salt == "" {
salt := utils.GenerateRandomString()
@ -76,7 +77,7 @@ func main() {
dao.InitDatabase()
if err := updateInitPassword(adminUserID, os.Getenv("HARBOR_ADMIN_PASSWORD")); err != nil {
if err := updateInitPassword(adminUserID, config.InitialAdminPassword()); err != nil {
log.Error(err)
}
initRouters()

View File

@ -84,6 +84,9 @@ func initRouters() {
beego.Router("/api/users/:id/sysadmin", &api.UserAPI{}, "put:ToggleUserAdminRole")
beego.Router("/api/repositories/top", &api.RepositoryAPI{}, "get:GetTopRepos")
beego.Router("/api/logs", &api.LogAPI{})
beego.Router("/api/systeminfo/volumes", &api.SystemInfoAPI{}, "get:GetVolumeInfo")
beego.Router("/api/systeminfo/getcert", &api.SystemInfoAPI{}, "get:GetCert")
//external service that hosted on harbor process:
beego.Router("/service/notifications", &service.NotificationHandler{})
beego.Router("/service/token", &token.Handler{})

View File

@ -21,13 +21,12 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/config"
"github.com/docker/distribution/registry/auth/token"
"github.com/docker/libtrust"
@ -38,27 +37,10 @@ const (
privateKey = "/etc/ui/private_key.pem"
)
var (
expiration = 30 //minutes
)
var expiration int //minutes
func init() {
// TODO read it from config
expi := os.Getenv("TOKEN_EXPIRATION")
if len(expi) != 0 {
i, err := strconv.Atoi(expi)
if err != nil {
log.Errorf("failed to parse token expiration: %v, using default value: %d minutes", err, expiration)
return
}
if i <= 0 {
log.Warningf("invalid token expiration, using default value: %d minutes", expiration)
return
}
expiration = i
}
expiration = config.TokenExpiration()
log.Infof("token expiration: %d minutes", expiration)
}
@ -105,8 +87,17 @@ func FilterAccess(username string, a *token.ResourceActions) {
//clear action list to assign to new acess element after perm check.
a.Actions = []string{}
if a.Type == "repository" {
if strings.Contains(a.Name, "/") { //Only check the permission when the requested image has a namespace, i.e. project
projectName := a.Name[0:strings.LastIndex(a.Name, "/")]
repoSplit := strings.Split(a.Name, "/")
repoLength := len(repoSplit)
if repoLength > 1 { //Only check the permission when the requested image has a namespace, i.e. project
var projectName string
registryURL := config.ExtRegistryURL()
if repoSplit[0] == registryURL {
projectName = repoSplit[1]
log.Infof("Detected Registry URL in Project Name. Assuming this is a notary request and setting Project Name as %s\n", projectName)
} else {
projectName = repoSplit[0]
}
var permission string
if len(username) > 0 {
isAdmin, err := dao.IsAdminRole(username)

View File

@ -18,14 +18,14 @@ package utils
import (
"net/http"
"os"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/config"
)
// VerifySecret verifies the UI_SECRET cookie in a http request.
func VerifySecret(r *http.Request) bool {
secret := os.Getenv("UI_SECRET")
secret := config.UISecret()
c, err := r.Cookie("uisecret")
if err != nil {
log.Warningf("Failed to get secret cookie, error: %v", err)

View File

@ -48,8 +48,6 @@
element.find('.section').css({'height': (h - scope.subsHeight - scope.subsSection) + 'px'});
element.find('.sub-pane').css({'height': (h - scope.subsHeight - scope.subsSubPane) + 'px'});
element.find('.tab-pane').css({'height': (h - scope.subsHeight - scope.subsSubPane - scope.subsSection -100) + 'px'});
// var subPaneHeight = element.find('.sub-pane').height();
// element.find('.table-body-container').css({'height': (subPaneHeight - scope.subsTblBody) + 'px'});
}
}, true);

View File

@ -20,9 +20,9 @@
.module('harbor.log')
.directive('advancedSearch', advancedSearch);
AdvancedSearchController.$inject = ['$scope', 'ListLogService'];
AdvancedSearchController.$inject = ['$scope', 'ListLogService', '$filter', 'trFilter'];
function AdvancedSearchController($scope, ListLogService) {
function AdvancedSearchController($scope, ListLogService, $filter, trFilter) {
var vm = this;
vm.checkOperation = checkOperation;
@ -121,8 +121,21 @@
if(vm.opOthers && $.trim(vm.others) !== '') {
e.op.push(vm.others);
}
if(vm.fromDate && vm.toDate && (getDateValue(vm.fromDate) > getDateValue(vm.toDate))) {
$scope.$emit('modalTitle', $filter('tr')('error'));
$scope.$emit('modalMessage', $filter('tr')('begin_date_is_later_than_end_date'));
$scope.$emit('raiseError', true);
return
}
vm.search(e);
}
function getDateValue(date) {
if(date) {
return new Date(date);
}
return 0;
}
}
function advancedSearch(I18nService) {

View File

@ -143,7 +143,9 @@
}
function listLog() {
listLog.$inject = ['$timeout'];
function listLog($timeout) {
var directive = {
'restrict': 'E',
'templateUrl': '/static/resources/js/components/log/list-log.directive.html',
@ -162,6 +164,12 @@
element.find('#txtSearchInput').on('keydown', function(e) {
if($(this).is(':focus') && e.keyCode === 13) {
ctrl.search({'op': ctrl.op, 'username': ctrl.username});
} else {
$timeout(function() {
if(ctrl.username.length === 0) {
ctrl.search({'op': ctrl.op, 'username': ctrl.username});
}
});
}
});
}

View File

@ -20,26 +20,24 @@
.module('harbor.optional.menu')
.directive('optionalMenu', optionalMenu);
OptionalMenuController.$inject = ['$scope', '$window', 'I18nService', 'LogOutService', 'currentUser', '$timeout', 'trFilter', '$filter'];
OptionalMenuController.$inject = ['$scope', '$window', 'I18nService', 'LogOutService', 'currentUser', '$timeout', 'trFilter', '$filter', 'GetVolumeInfoService'];
function OptionalMenuController($scope, $window, I18nService, LogOutService, currentUser, $timeoutm, trFilter, $filter) {
function OptionalMenuController($scope, $window, I18nService, LogOutService, currentUser, $timeoutm, trFilter, $filter, GetVolumeInfoService) {
var vm = this;
vm.currentLanguage = I18nService().getCurrentLanguage();
vm.languageName = I18nService().getLanguageName(vm.currentLanguage);
I18nService().setCurrentLanguage(vm.currentLanguage);
var i18n = I18nService();
i18n.setCurrentLanguage(vm.language);
vm.languageName = i18n.getLanguageName(vm.language);
console.log('current language:' + vm.languageName);
vm.supportLanguages = I18nService().getSupportLanguages();
vm.supportLanguages = i18n.getSupportLanguages();
vm.user = currentUser.get();
vm.setLanguage = setLanguage;
vm.logOut = logOut;
vm.about = about;
function setLanguage(language) {
I18nService().setCurrentLanguage(language);
vm.languageName = i18n.getLanguageName(vm.language);
var hash = $window.location.hash;
$window.location.href = '/language?lang=' + language + '&hash=' + encodeURIComponent(hash);
}
@ -55,16 +53,43 @@
function logOutFailed(data, status) {
console.log('Failed to log out:' + data);
}
var raiseInfo = {
'confirmOnly': true,
'contentType': 'text/html',
'action': function() {}
};
function about() {
$scope.$emit('modalTitle', $filter('tr')('about_harbor'));
$scope.$emit('modalMessage', $filter('tr')('current_version', [vm.version || 'Unknown']));
var raiseInfo = {
'confirmOnly': true,
'contentType': 'text/html',
'action': function() {}
};
vm.modalMessage = $filter('tr')('current_version', [vm.version || 'Unknown']);
if(vm.showDownloadCert === 'true') {
appendDownloadCertLink();
}
GetVolumeInfoService("data")
.then(getVolumeInfoSuccess, getVolumeInfoFailed);
}
function getVolumeInfoSuccess(response) {
var storage = response.data;
vm.modalMessage += '<br/>' + $filter('tr')('current_storage',
[toGigaBytes(storage['storage']['free']), toGigaBytes(storage['storage']['total'])]);
$scope.$emit('modalMessage', vm.modalMessage);
$scope.$emit('raiseInfo', raiseInfo);
}
function getVolumeInfoFailed(response) {
$scope.$emit('modalMessage', vm.modalMessage);
$scope.$emit('raiseInfo', raiseInfo);
}
function toGigaBytes(val) {
return Math.round(val / (1024 * 1024 * 1024));
}
function appendDownloadCertLink() {
vm.modalMessage += '<br/>' + $filter('tr')('default_root_cert', ['/api/systeminfo/getcert', $filter('tr')('download')]);
}
}
function optionalMenu() {
@ -72,7 +97,9 @@
'restrict': 'E',
'templateUrl': '/optional_menu?timestamp=' + new Date().getTime(),
'scope': {
'version': '@'
'version': '@',
'language': '@',
'showDownloadCert': '@'
},
'controller': OptionalMenuController,
'controllerAs': 'vm',

View File

@ -1,14 +1,25 @@
<nav aria-label="Page navigation" class="pull-left">
<ul class="pagination" style="margin: 0;">
<li>
<a href="javascript:void(0);" ng-click="vm.previous()" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
<ul class="pagination" style="margin: 0 0 0 10px;">
<li ng-class="vm.disabledFirst" ng-show="vm.visible">
<a href="javascript:void(0);" ng-click="vm.gotoFirst()" aria-label="Previous">
<span aria-hidden="true">&lt;&lt;</span>
</a>
</li>
<li>
<li ng-class="vm.disabledPrevious" ng-show="vm.visible">
<a href="javascript:void(0);" ng-click="vm.previous()" aria-label="Previous">
<span aria-hidden="true">&lt;</span>
</a>
</li>
<li ng-class="vm.disabledNext" ng-show="vm.visible">
<a href="javascript:void(0);" ng-click="vm.next()" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
<span aria-hidden="true">&gt;</span>
</a>
</li>
<li ng-class="vm.disabledLast" ng-show="vm.visible">
<a href="javascript:void(0);" ng-click="vm.gotoLast()" aria-label="Next">
<span aria-hidden="true">&gt;&gt;</span>
</a>
</li>
</ul>
</nav>
<p class="pull-right" style="margin-right: 15%; margin-top: 5px;">// 'total' | tr // // vm.totalCount // // 'items' | tr //</p>

View File

@ -32,7 +32,6 @@
return directive;
function link(scope, element, attrs, ctrl) {
scope.$watch('vm.page', function(current) {
if(current) {
ctrl.page = current;
@ -45,34 +44,43 @@
scope.$watch('vm.totalCount', function(current) {
if(current) {
var totalCount = current;
element.find('ul li:first a').off('click');
element.find('ul li:last a').off('click');
tc = new TimeCounter();
console.log('Total Count:' + totalCount + ', Page Size:' + ctrl.pageSize + ', Display Count:' + ctrl.displayCount + ', Page:' + ctrl.page);
ctrl.buttonCount = Math.ceil(totalCount / ctrl.pageSize);
if(ctrl.buttonCount <= ctrl.displayCount) {
tc.setMaximum(1);
ctrl.visible = false;
}else{
tc.setMaximum(Math.ceil(ctrl.buttonCount / ctrl.displayCount));
ctrl.visible = true;
}
element.find('ul li:first a').on('click', previous);
element.find('ul li:last a').on('click', next);
ctrl.gotoFirst = gotoFirst;
ctrl.gotoLast = gotoLast;
if(ctrl.buttonCount < ctrl.page) {
ctrl.page = ctrl.buttonCount;
}
ctrl.previous = previous;
ctrl.next = next;
drawButtons(tc.getTime());
togglePrevious(tc.canDecrement());
toggleNext(tc.canIncrement());
toggleFirst();
toggleLast();
togglePageButton();
}
});
});
var TimeCounter = function() {
this.time = 0;
@ -84,6 +92,10 @@
this.maximum = maximum;
};
TimeCounter.prototype.getMaximum = function() {
return this.maximum;
};
TimeCounter.prototype.increment = function() {
if(this.time < this.maximum) {
++this.time;
@ -92,7 +104,6 @@
}
++ctrl.page;
}
scope.$apply();
};
TimeCounter.prototype.canIncrement = function() {
@ -106,13 +117,11 @@
if(this.time > this.minimum) {
if(this.time === 0) {
ctrl.page = ctrl.displayCount;
}else if((ctrl.page % ctrl.displayCount) != 0) {
}else{
ctrl.page = this.time * ctrl.displayCount;
}
--this.time;
--ctrl.page;
}
scope.$apply();
};
TimeCounter.prototype.canDecrement = function() {
@ -125,6 +134,10 @@
TimeCounter.prototype.getTime = function() {
return this.time;
};
TimeCounter.prototype.setTime = function(time) {
this.time = time;
};
function drawButtons(time) {
element.find('li[tag="pagination-button"]').remove();
@ -135,32 +148,38 @@
buttons.push('<li tag="pagination-button"><a href="javascript:void(0)" page="' + displayNumber + '">' + displayNumber + '<span class="sr-only"></span></a></li>');
}
}
$(buttons.join(''))
.insertAfter(element.find('ul li:eq(0)')).end()
.insertAfter(element.find('ul li:eq(' + (ctrl.visible ? 1 : 0) + ')')).end()
.on('click', buttonClickHandler);
}
function togglePrevious(status) {
if(status){
element.find('ul li:first').removeClass('disabled');
}else{
element.find('ul li:first').addClass('disabled');
}
}
ctrl.disabledPrevious = status ? '' : 'disabled';
toggleFirst();
toggleLast();
}
function toggleNext(status) {
if(status) {
element.find('ul li:last').removeClass('disabled');
}else{
element.find('ul li:last').addClass('disabled');
}
ctrl.disabledNext = status ? '' : 'disabled';
toggleFirst();
toggleLast();
}
function toggleFirst() {
ctrl.disabledFirst = (ctrl.page > 1) ? '' : 'disabled';
}
function toggleLast() {
ctrl.disabledLast = (ctrl.page < ctrl.buttonCount) ? '' : 'disabled';
}
function buttonClickHandler(e) {
ctrl.page = $(e.target).attr('page');
ctrl.page = $(e.target).attr('page');
togglePageButton();
togglePrevious(tc.canDecrement());
toggleNext(tc.canIncrement());
scope.$apply();
}
@ -177,8 +196,232 @@
togglePrevious(tc.canDecrement());
toggleNext(tc.canIncrement());
}
scope.$apply();
}
function gotoFirst() {
ctrl.page = 1;
tc.setTime(0);
drawButtons(0);
toggleFirst();
toggleLast();
togglePageButton();
togglePrevious(tc.canDecrement());
toggleNext(tc.canIncrement());
}
(function() {
'use strict';
angular
.module('harbor.paginator')
.directive('paginator', paginator);
PaginatorController.$inject = [];
function PaginatorController() {
var vm = this;
}
paginator.$inject = [];
function paginator() {
var directive = {
'restrict': 'E',
'templateUrl': '/static/resources/js/components/paginator/paginator.directive.html',
'scope': {
'totalCount': '@',
'pageSize': '@',
'page': '=',
'displayCount': '@'
},
'link': link,
'controller': PaginatorController,
'controllerAs': 'vm',
'bindToController': true
};
return directive;
function link(scope, element, attrs, ctrl) {
scope.$watch('vm.page', function(current) {
if(current) {
ctrl.page = current;
togglePageButton();
}
});
var tc;
scope.$watch('vm.totalCount', function(current) {
if(current) {
var totalCount = current;
tc = new TimeCounter();
console.log('Total Count:' + totalCount + ', Page Size:' + ctrl.pageSize + ', Display Count:' + ctrl.displayCount + ', Page:' + ctrl.page);
ctrl.buttonCount = Math.ceil(totalCount / ctrl.pageSize);
if(ctrl.buttonCount <= ctrl.displayCount) {
tc.setMaximum(1);
ctrl.visible = false;
}else{
tc.setMaximum(Math.ceil(ctrl.buttonCount / ctrl.displayCount));
ctrl.visible = true;
}
ctrl.gotoFirst = gotoFirst;
ctrl.gotoLast = gotoLast;
if(ctrl.buttonCount < ctrl.page) {
ctrl.page = ctrl.buttonCount;
}
ctrl.previous = previous;
ctrl.next = next;
drawButtons(tc.getTime());
togglePrevious(tc.canDecrement());
toggleNext(tc.canIncrement());
toggleFirst();
toggleLast();
togglePageButton();
}
});
var TimeCounter = function() {
this.time = 0;
this.minimum = 0;
this.maximum = 0;
};
TimeCounter.prototype.setMaximum = function(maximum) {
this.maximum = maximum;
};
TimeCounter.prototype.getMaximum = function() {
return this.maximum;
};
TimeCounter.prototype.increment = function() {
if(this.time < this.maximum) {
++this.time;
if((ctrl.page % ctrl.displayCount) != 0) {
ctrl.page = this.time * ctrl.displayCount;
}
++ctrl.page;
}
};
TimeCounter.prototype.canIncrement = function() {
if(this.time + 1 < this.maximum) {
return true;
}
return false;
};
TimeCounter.prototype.decrement = function() {
if(this.time > this.minimum) {
if(this.time === 0) {
ctrl.page = ctrl.displayCount;
}else{
ctrl.page = this.time * ctrl.displayCount;
}
--this.time;
}
};
TimeCounter.prototype.canDecrement = function() {
if(this.time > this.minimum) {
return true;
}
return false;
};
TimeCounter.prototype.getTime = function() {
return this.time;
};
TimeCounter.prototype.setTime = function(time) {
this.time = time;
};
function drawButtons(time) {
element.find('li[tag="pagination-button"]').remove();
var buttons = [];
for(var i = 1; i <= ctrl.displayCount; i++) {
var displayNumber = ctrl.displayCount * time + i;
if(displayNumber <= ctrl.buttonCount) {
buttons.push('<li tag="pagination-button"><a href="javascript:void(0)" page="' + displayNumber + '">' + displayNumber + '<span class="sr-only"></span></a></li>');
}
}
$(buttons.join(''))
.insertAfter(element.find('ul li:eq(' + (ctrl.visible ? 1 : 0) + ')')).end()
.on('click', buttonClickHandler);
}
function togglePrevious(status) {
ctrl.disabledPrevious = status ? '' : 'disabled';
toggleFirst();
toggleLast();
}
function toggleNext(status) {
ctrl.disabledNext = status ? '' : 'disabled';
toggleFirst();
toggleLast();
}
function toggleFirst() {
ctrl.disabledFirst = (ctrl.page > 1) ? '' : 'disabled';
}
function toggleLast() {
ctrl.disabledLast = (ctrl.page < ctrl.buttonCount) ? '' : 'disabled';
}
function buttonClickHandler(e) {
ctrl.page = $(e.target).attr('page');
togglePageButton();
togglePrevious(tc.canDecrement());
toggleNext(tc.canIncrement());
scope.$apply();
}
function togglePageButton() {
element.find('li[tag="pagination-button"]').removeClass('active');
element.find('li[tag="pagination-button"] a[page="' + ctrl.page + '"]').parent().addClass('active');
}
function previous() {
if(tc.canDecrement()) {
tc.decrement();
drawButtons(tc.getTime());
togglePageButton();
togglePrevious(tc.canDecrement());
toggleNext(tc.canIncrement());
}
}
function gotoFirst() {
ctrl.page = 1;
tc.setTime(0);
drawButtons(0);
toggleFirst();
toggleLast();
togglePageButton();
togglePrevious(tc.canDecrement());
toggleNext(tc.canIncrement());
}
function next() {
if(tc.canIncrement()) {
@ -188,7 +431,45 @@
togglePrevious(tc.canDecrement());
toggleNext(tc.canIncrement());
}
scope.$apply();
}
function gotoLast() {
ctrl.page = ctrl.buttonCount;
tc.setTime(Math.ceil(ctrl.buttonCount / ctrl.displayCount) - 1);
drawButtons(tc.getTime());
toggleFirst();
toggleLast();
togglePageButton();
togglePrevious(tc.canDecrement());
toggleNext(tc.canIncrement());
}
}
}
})();
function next() {
if(tc.canIncrement()) {
tc.increment();
drawButtons(tc.getTime());
togglePageButton();
togglePrevious(tc.canDecrement());
toggleNext(tc.canIncrement());
}
}
function gotoLast() {
ctrl.page = ctrl.buttonCount;
tc.setTime(Math.ceil(ctrl.buttonCount / ctrl.displayCount) - 1);
drawButtons(tc.getTime());
toggleFirst();
toggleLast();
togglePageButton();
togglePrevious(tc.canDecrement());
toggleNext(tc.canIncrement());
}
}
}

View File

@ -14,7 +14,7 @@
-->
<div class="well panel-group well-custom" style="margin-top: 10px; position: absolute; width: 98%;">
<div class="row">
<form name="form" class="css-form form-custom" novalidate>
<form name="form" class="css-form form-custom" novalidate autocomplete="off">
<div class="col-xs-10 col-md-10">
<div class="form-group col-md-6">
<input type="text" class="form-control" id="addUsername" placeholder="// 'username' | tr //" ng-model="pm.username" name="uUsername" ng-model-options="{ debounce: 250 }" ng-change="vm.reset()" required>

View File

@ -88,8 +88,10 @@
}
}
addProjectMember.$inject = ['$timeout'];
function addProjectMember() {
function addProjectMember($timeout) {
var directive = {
'restrict': 'E',
'templateUrl': '/static/resources/js/components/project-member/add-project-member.directive.html',
@ -109,6 +111,13 @@
function link(scope, element, attrs, ctrl) {
scope.form.$setPristine();
scope.form.$setUntouched();
scope.$watch('vm.isOpen', function(current) {
if(current) {
$timeout(function() {
element.find('[name=uUsername]:input').focus();
});
}
});
}
}

View File

@ -96,7 +96,9 @@
}
function listProjectMember() {
listProjectMember.$inject = ['$timeout'];
function listProjectMember($timeout) {
var directive = {
'restrict': 'E',
'templateUrl': '/static/resources/js/components/project-member/list-project-member.directive.html',
@ -116,6 +118,12 @@
element.find('#txtSearchInput').on('keydown', function(e) {
if($(this).is(':focus') && e.keyCode === 13) {
ctrl.retrieve();
} else {
$timeout(function() {
if(ctrl.username.length === 0) {
ctrl.retrieve();
}
});
}
});
}

View File

@ -12,8 +12,8 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="well well-custom" style="position: absolute; margin-left: 10px; margin-top: 5px; width: 96%;">
<form name="form" class="css-form form-custom" novalidate>
<div class="well well-custom" style="position: absolute; margin-left: 10px; margin-top: 5px; width: 94%;">
<form name="form" class="css-form form-custom" novalidate autocomplete="off">
<div class="row">
<div class="col-xs-10 col-md-10">
<div class="form-group col-md-7">

View File

@ -97,18 +97,31 @@
}
}
function addProject() {
addProject.$inject = ['$timeout'];
function addProject($timeout) {
var directive = {
'restrict': 'E',
'templateUrl': '/static/resources/js/components/project/add-project.directive.html',
'controller': AddProjectController,
'link': link,
'scope' : {
'isOpen': '='
},
'controllerAs': 'vm',
'bindToController': true
};
return directive;
return directive;
function link(scope, element, attrs, ctrl) {
scope.$watch('vm.isOpen', function(current) {
if(current) {
$timeout(function() {
element.find(':input[name=uProjectName]').focus();
});
}
});
}
}
})();

View File

@ -14,7 +14,7 @@
-->
<div class="modal fade" data-backdrop="static" id="createPolicyModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<form name="form" class="form-horizontal css-form" novalidate>
<form name="form" class="form-horizontal css-form" novalidate autocomplete="off">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
@ -38,7 +38,7 @@
<label for="name" class="col-md-3 control-label">// 'name' | tr //:</label>
<div class="col-md-9">
<input type="text" class="form-control form-control-custom" id="name" ng-model="replication.policy.name" name="uName" required maxlength="20" ng-disabled="!vm.targetEditable">
<div ng-messages="form.$submitted && form.uName.$error">
<div class="error-message" ng-messages="form.$submitted && form.uName.$error">
<span ng-message="required">// 'name_is_required' | tr //</span>
<span ng-message="maxlength">// 'name_is_too_long' | tr //</span>
</div>
@ -48,7 +48,7 @@
<label for="description" class="col-md-3 control-label">// 'description' | tr //:</label>
<div class="col-md-9">
<textarea class="form-control form-control-custom" id="description" ng-model="replication.policy.description" name="uDescription" ng-disabled="!vm.targetEditable"></textarea>
<div ng-messages="form.$submitted && form.uDescription.$error">
<div class="error-message" ng-messages="form.$submitted && form.uDescription.$error">
<span ng-message="maxlength">// 'description_is_too_long' | tr //</span>
</div>
</div>
@ -79,7 +79,7 @@
<label for="endpoint" class="col-md-3 control-label">// 'endpoint' | tr //:</label>
<div class="col-md-9">
<input type="text" class="form-control form-control-custom" id="endpoint" ng-model="replication.destination.endpoint" name="uEndpoint" ng-value="vm.endpoint" placeholder="http://ip_address" required ng-disabled="!vm.targetEditable || !vm.checkedAddTarget">
<div ng-messages="form.$submitted && form.uEndpoint.$error">
<div class="error-message" ng-messages="form.$submitted && form.uEndpoint.$error">
<span ng-message="required">// 'endpoint_is_required' | tr //</span>
</div>
</div>
@ -105,7 +105,7 @@
<div class="form-group col-md-12 form-group-custom">
<div class="col-md-3"></div>
<div class="col-md-9">
<span>// vm.pingMessage //</span>
<span class="error-message" >// vm.pingMessage //</span>
</div>
</div>
</div>

View File

@ -105,6 +105,12 @@
}
function refreshReplicationJob() {
if(vm.fromDate && vm.toDate && (getDateValue(vm.fromDate) > getDateValue(vm.toDate))) {
$scope.$emit('modalTitle', $filter('tr')('error'));
$scope.$emit('modalMessage', $filter('tr')('begin_date_is_later_than_end_date'));
$scope.$emit('raiseError', true);
return;
}
if(vm.lastPolicyId !== -1) {
vm.refreshJobTIP = true;
vm.retrieveJob(vm.lastPolicyId, vm.page, vm.pageSize);
@ -288,8 +294,17 @@
return t.getTime() / 1000;
}
function getDateValue(date) {
if(date) {
return new Date(date);
}
return 0;
}
}
listReplication.inject = ['$timeout', 'I18nService'];
function listReplication($timeout, I18nService) {
var directive = {
'restrict': 'E',
@ -398,12 +413,24 @@
element.find('#txtSearchPolicyInput').on('keydown', function(e) {
if($(this).is(':focus') && e.keyCode === 13) {
ctrl.searchReplicationPolicy();
} else {
$timeout(function() {
if(ctrl.replicationPolicyName.length === 0) {
ctrl.searchReplicationPolicy();
}
});
}
});
element.find('#txtSearchJobInput').on('keydown', function(e) {
if($(this).is(':focus') && e.keyCode === 13) {
ctrl.searchReplicationJob();
} else {
$timeout(function() {
if(ctrl.replicationJobName.length === 0) {
ctrl.searchReplicationJob();
}
});
}
});

View File

@ -192,7 +192,9 @@
}
function listRepository() {
listRepository.$inject = ['$timeout'];
function listRepository($timeout) {
var directive = {
'restrict': 'E',
'templateUrl': '/static/resources/js/components/repository/list-repository.directive.html',
@ -212,6 +214,12 @@
element.find('#txtSearchInput').on('keydown', function(e) {
if($(this).is(':focus') && e.keyCode === 13) {
ctrl.retrieve();
} else {
$timeout(function() {
if(ctrl.filterInput.length === 0) {
ctrl.retrieve();
}
});
}
});
}

View File

@ -37,19 +37,26 @@
vm.signInTIP = false;
$scope.user = {};
function reset() {
vm.hasError = false;
vm.errorMessage = '';
}
function doSignIn(user) {
if(user && angular.isDefined(user.principal) && angular.isDefined(user.password)) {
}
function doSignIn(user) {
if(!$scope.user.principal || !$scope.user.password ||
$scope.user.principal.length === 0 || $scope.user.password.length === 0) {
vm.hasError = true;
vm.errorMessage = 'username_and_password_are_required';
}
if(user.principal && user.password) {
vm.lastUrl = getParameterByName('last_url', $location.absUrl());
vm.signInTIP = true;
SignInService(user.principal, user.password)
.success(signedInSuccess)
.error(signedInFailed);
}
}
}
function signedInSuccess(data, status) {

View File

@ -12,7 +12,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<form name="form" class="form-horizontal" ng-submit="form.$valid && vm.changeSettings(system)" >
<form name="form" class="form-horizontal" ng-submit="form.$valid && vm.changeSettings(system)" autocomplete="off">
<div class="col-md-12">
<h5>System Settings</h5>
<hr/>

View File

@ -14,7 +14,7 @@
-->
<div class="modal fade" data-backdrop="static" id="createDestinationModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<form name="form" class="form-horizontal css-form" novalidate>
<form name="form" class="form-horizontal css-form" novalidate autocomplete="off">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
@ -34,7 +34,7 @@
<label for="name" class="col-md-3 control-label">// 'name' | tr //:</label>
<div class="col-md-9">
<input type="text" class="form-control form-control-custom" id="name" ng-model="destination.name" name="uName" required ng-disabled="!vm.editable">
<div ng-messages="form.$submitted && form.uName.$error">
<div class="error-message" ng-messages="form.$submitted && form.uName.$error">
<span ng-message="required">// 'name_is_required' | tr //</span>
</div>
</div>
@ -43,7 +43,7 @@
<label for="description" class="col-md-3 control-label">// 'endpoint' | tr //:</label>
<div class="col-md-9">
<input type="text" class="form-control form-control-custom" id="endpoint" ng-model="destination.endpoint" name="uEndpoint" required ng-disabled="!vm.editable">
<div ng-messages="form.$submitted && form.uEndpoint.$error">
<div class="error-message" ng-messages="form.$submitted && form.uEndpoint.$error">
<span ng-message="required">// 'endpoint_is_required' | tr //</span>
</div>
</div>
@ -69,7 +69,7 @@
<div class="form-group col-md-12 form-group-custom">
<div class="col-md-3"></div>
<div class="col-md-9">
<span>// vm.pingMessage //</span>
<span class="error-message">// vm.pingMessage //</span>
</div>
</div>
</div>

View File

@ -103,7 +103,9 @@
}
}
function destination() {
destination.$inject = ['$timeout'];
function destination($timeout) {
var directive = {
'restrict': 'E',
'templateUrl': '/static/resources/js/components/system-management/destination.directive.html',
@ -119,6 +121,12 @@
element.find('#txtSearchInput').on('keydown', function(e) {
if($(this).is(':focus') && e.keyCode === 13) {
ctrl.retrieve();
} else {
$timeout(function() {
if(ctrl.destinationName.length === 0) {
ctrl.retrieve();
}
});
}
});
}

View File

@ -106,7 +106,9 @@
}
}
function replication() {
replication.$inject = ['$timeout'];
function replication($timeout) {
var directive = {
'restrict': 'E',
'templateUrl': '/static/resources/js/components/system-management/replication.directive.html',
@ -122,6 +124,12 @@
element.find('#txtSearchInput').on('keydown', function(e) {
if($(this).is(':focus') && e.keyCode === 13) {
ctrl.retrieve();
} else {
$timeout(function() {
if(ctrl.replicationName.length === 0) {
ctrl.retrieve();
}
});
}
});
}

View File

@ -55,7 +55,7 @@
</div>
</div>
<div class="col-xs-4 col-md-12 well well-sm well-custom">
<div class="col-md-offset-10">//vm.users ? vm.users.length : 0// items</div>
<div class="col-md-offset-10">//vm.users ? vm.users.length : 0// // 'items' | tr //</div>
</div>
</div>
</div>

View File

@ -32,9 +32,7 @@
vm.searchUser = searchUser;
vm.deleteUser = deleteUser;
vm.confirmToDelete = confirmToDelete;
vm.retrieve = retrieve;
vm.currentUser = currentUser.get();
vm.retrieve = retrieve;
vm.retrieve();
@ -82,6 +80,7 @@
}
function listUserSuccess(data, status) {
vm.currentUser = currentUser.get();
vm.users = data;
}
@ -93,7 +92,9 @@
}
}
function listUser() {
listUser.$inject = ['$timeout'];
function listUser($timeout) {
var directive = {
'restrict': 'E',
'templateUrl': '/static/resources/js/components/user/list-user.directive.html',
@ -111,6 +112,12 @@
element.find('#txtSearchInput').on('keydown', function(e) {
if($(this).is(':focus') && e.keyCode === 13) {
ctrl.retrieve();
} else {
$timeout(function() {
if(ctrl.username.length === 0) {
ctrl.retrieve();
}
});
}
});
}

Some files were not shown because too many files have changed in this diff Show More