Merge pull request #1449 from ywk253100/170224_merge_config

Merge configuration to dev
This commit is contained in:
Daniel Jiang 2017-02-24 15:44:36 +08:00 committed by GitHub
commit 01e105090e
106 changed files with 4575 additions and 1177 deletions

3
.gitignore vendored
View File

@ -1,8 +1,11 @@
harbor
make/common/config/*
make/dev/adminserver/harbor_adminserver
make/dev/ui/harbor_ui
make/dev/jobservice/harbor_jobservice
src/adminserver/adminserver
src/ui/ui
src/jobservice/jobservice
src/common/dao/dao.test
*.pyc
jobservice/test

View File

@ -13,22 +13,21 @@ services:
dist: trusty
env:
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_USR: root
DB_PWD: root123
MYSQL_HOST: localhost
MYSQL_PORT: 3306
MYSQL_USR: root
MYSQL_PWD: root123
MYSQL_DATABASE: registry
SQLITE_FILE: /tmp/registry.db
ADMIN_SERVER_URL: http://127.0.0.1:8888
DOCKER_COMPOSE_VERSION: 1.7.1
HARBOR_ADMIN: admin
HARBOR_ADMIN_PASSWD: Harbor12345
UI_SECRET: tempString
MAX_JOB_WORKERS: 3
SECRET_KEY: 1234567890123456
AUTH_MODE: db_auth
SELF_REGISTRATION: "on"
SELF_REGISTRATION: on
KEY_PATH: /data/secretkey
before_install:
- sudo ./tests/hostcfg.sh
@ -70,7 +69,8 @@ install:
before_script:
# create tables and load data
# - mysql < ./make/db/registry.sql -uroot --verbose
- sudo sqlite3 /registry.db < make/common/db/registry_sqlite.sql
- sudo sqlite3 /tmp/registry.db < make/common/db/registry_sqlite.sql
- sudo chmod 777 /tmp/registry.db
script:
- sudo mkdir -p /harbor_storage/ca_download
@ -88,7 +88,8 @@ script:
- goveralls -coverprofile=profile.cov -service=travis-ci
- docker-compose -f make/docker-compose.test.yml down
- sudo make/prepare
- sudo rm -rf /data/config/*
- docker-compose -f make/dev/docker-compose.yml up -d
- docker ps

View File

@ -4,18 +4,17 @@
#
# all: prepare env, compile binarys, build images and install images
# prepare: prepare env
# compile: compile ui and jobservice code
# compile: compile adminserver, ui and jobservice code
#
# compile_golangimage:
# compile from golang image
# for example: make compile_golangimage -e GOBUILDIMAGE= \
# golang:1.7.3
# compile_ui, compile_jobservice: compile specific binary
# compile_adminserver, compile_ui, compile_jobservice: compile specific binary
#
# build: build Harbor docker images (defuault: build_photon)
# for example: make build -e BASEIMAGE=photon
# build_photon: build Harbor docker images from photon bsaeimage
# build_ubuntu: build Harbor docker images from ubuntu baseimage
# build_photon: build Harbor docker images from photon baseimage
#
# install: include compile binarys, build images, prepare specific \
# version composefile and startup Harbor instance
@ -46,7 +45,7 @@
#
# clean: remove binary, Harbor images, specific version docker-compose \
# file, specific version tag and online/offline install package
# cleanbinary: remove ui and jobservice binary
# cleanbinary: remove adminserver, ui and jobservice binary
# cleanimage: remove Harbor images
# cleandockercomposefile:
# remove specific version docker-compose
@ -100,14 +99,19 @@ GOBUILDIMAGE=reg.mydomain.com/library/harborgo[:tag]
GOBUILDPATH=$(GOBASEPATH)/harbor
GOIMAGEBUILDCMD=/usr/local/go/bin/go
GOIMAGEBUILD=$(GOIMAGEBUILDCMD) build
GOBUILDPATH_ADMINSERVER=$(GOBUILDPATH)/src/adminserver
GOBUILDPATH_UI=$(GOBUILDPATH)/src/ui
GOBUILDPATH_JOBSERVICE=$(GOBUILDPATH)/src/jobservice
GOBUILDMAKEPATH=$(GOBUILDPATH)/make
GOBUILDMAKEPATH_ADMINSERVER=$(GOBUILDMAKEPATH)/dev/adminserver
GOBUILDMAKEPATH_UI=$(GOBUILDMAKEPATH)/dev/ui
GOBUILDMAKEPATH_JOBSERVICE=$(GOBUILDMAKEPATH)/dev/jobservice
GOLANGDOCKERFILENAME=Dockerfile.golang
# binary
ADMINSERVERSOURCECODE=$(SRCPATH)/adminserver
ADMINSERVERBINARYPATH=$(MAKEDEVPATH)/adminserver
ADMINSERVERBINARYNAME=harbor_adminserver
UISOURCECODE=$(SRCPATH)/ui
UIBINARYPATH=$(MAKEDEVPATH)/ui
UIBINARYNAME=harbor_ui
@ -125,7 +129,6 @@ CONFIGFILE=harbor.cfg
# makefile
MAKEFILEPATH_PHOTON=$(MAKEPATH)/photon
MAKEFILEPATH_UBUNTU=$(MAKEPATH)/ubuntu
# common dockerfile
DOCKERFILEPATH_COMMON=$(MAKEPATH)/common
@ -133,6 +136,7 @@ DOCKERFILEPATH_DB=$(DOCKERFILEPATH_COMMON)/db
DOCKERFILENAME_DB=Dockerfile
# docker image name
DOCKERIMAGENAME_ADMINSERVER=vmware/harbor-adminserver
DOCKERIMAGENAME_UI=vmware/harbor-ui
DOCKERIMAGENAME_JOBSERVICE=vmware/harbor-jobservice
DOCKERIMAGENAME_LOG=vmware/harbor-log
@ -177,6 +181,11 @@ version:
check_environment:
@$(MAKEPATH)/$(CHECKENVCMD)
compile_adminserver:
@echo "compiling binary for adminserver..."
@$(GOBUILD) -o $(ADMINSERVERBINARYPATH)/$(ADMINSERVERBINARYNAME) $(ADMINSERVERSOURCECODE)
@echo "Done."
compile_ui:
@echo "compiling binary for ui..."
@$(GOBUILD) -o $(UIBINARYPATH)/$(UIBINARYNAME) $(UISOURCECODE)
@ -187,9 +196,15 @@ compile_jobservice:
@$(GOBUILD) -o $(JOBSERVICEBINARYPATH)/$(JOBSERVICEBINARYNAME) $(JOBSERVICESOURCECODE)
@echo "Done."
compile_normal: compile_ui compile_jobservice
compile_normal: compile_adminserver compile_ui compile_jobservice
compile_golangimage:
@echo "compiling binary for adminserver (golang image)..."
@echo $(GOBASEPATH)
@echo $(GOBUILDPATH)
@$(DOCKERCMD) run --rm -v $(BUILDPATH):$(GOBUILDPATH) -w $(GOBUILDPATH_ADMINSERVER) $(GOBUILDIMAGE) $(GOIMAGEBUILD) -v -o $(GOBUILDMAKEPATH_ADMINSERVER)/$(ADMINSERVERBINARYNAME)
@echo "Done."
@echo "compiling binary for ui (golang image)..."
@echo $(GOBASEPATH)
@echo $(GOBUILDPATH)
@ -214,9 +229,6 @@ build_common: version
build_photon: build_common
make -f $(MAKEFILEPATH_PHOTON)/Makefile build -e DEVFLAG=$(DEVFLAG)
build_ubuntu: build_common
make -f $(MAKEFILEPATH_UBUNTU)/Makefile build -e DEVFLAG=$(DEVFLAG)
build: build_$(BASEIMAGE)
modify_composefile:
@ -240,7 +252,6 @@ package_online: modify_composefile
@cp NOTICE $(HARBORPKG)/NOTICE
@$(TARCMD) -zcvf harbor-online-installer-$(VERSIONTAG).tgz \
--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 \
@ -264,6 +275,7 @@ package_offline: compile build modify_composefile
@echo "saving harbor docker image"
@$(DOCKERSAVE) -o $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tgz \
$(DOCKERIMAGENAME_ADMINSERVER):$(VERSIONTAG) \
$(DOCKERIMAGENAME_UI):$(VERSIONTAG) \
$(DOCKERIMAGENAME_LOG):$(VERSIONTAG) \
$(DOCKERIMAGENAME_DB):$(VERSIONTAG) \
@ -272,7 +284,6 @@ package_offline: compile build modify_composefile
@$(TARCMD) -zcvf harbor-offline-installer-$(VERSIONTAG).tgz \
--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 \
@ -285,6 +296,11 @@ package_offline: compile build modify_composefile
pushimage:
@echo "pushing harbor images ..."
@$(DOCKERTAG) $(DOCKERIMAGENAME_ADMINSERVER):$(VERSIONTAG) $(REGISTRYSERVER)$(DOCKERIMAGENAME_ADMINSERVER):$(VERSIONTAG)
@$(PUSHSCRIPTPATH)/$(PUSHSCRIPTNAME) $(REGISTRYSERVER)$(DOCKERIMAGENAME_ADMINSERVER):$(VERSIONTAG) \
$(REGISTRYUSER) $(REGISTRYPASSWORD) $(REGISTRYSERVER)
@$(DOCKERRMIMAGE) $(REGISTRYSERVER)$(DOCKERIMAGENAME_ADMINSERVER):$(VERSIONTAG)
@$(DOCKERTAG) $(DOCKERIMAGENAME_UI):$(VERSIONTAG) $(REGISTRYSERVER)$(DOCKERIMAGENAME_UI):$(VERSIONTAG)
@$(PUSHSCRIPTPATH)/$(PUSHSCRIPTNAME) $(REGISTRYSERVER)$(DOCKERIMAGENAME_UI):$(VERSIONTAG) \
$(REGISTRYUSER) $(REGISTRYPASSWORD) $(REGISTRYSERVER)
@ -317,11 +333,13 @@ down:
cleanbinary:
@echo "cleaning binary..."
@if [ -f $(ADMINSERVERBINARYPATH)/$(ADMINSERVERBINARYNAME) ] ; then rm $(ADMINSERVERBINARYPATH)/$(ADMINSERVERBINARYNAME) ; fi
@if [ -f $(UIBINARYPATH)/$(UIBINARYNAME) ] ; then rm $(UIBINARYPATH)/$(UIBINARYNAME) ; fi
@if [ -f $(JOBSERVICEBINARYPATH)/$(JOBSERVICEBINARYNAME) ] ; then rm $(JOBSERVICEBINARYPATH)/$(JOBSERVICEBINARYNAME) ; fi
cleanimage:
@echo "cleaning image for photon..."
- $(DOCKERRMIMAGE) -f $(DOCKERIMAGENAME_ADMINSERVER):$(VERSIONTAG)
- $(DOCKERRMIMAGE) -f $(DOCKERIMAGENAME_UI):$(VERSIONTAG)
- $(DOCKERRMIMAGE) -f $(DOCKERIMAGENAME_DB):$(VERSIONTAG)
- $(DOCKERRMIMAGE) -f $(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG)

View File

@ -127,7 +127,6 @@ 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
@ -143,13 +142,6 @@ cleanpackage | remove online/offline install package
#### EXAMPLE:
#### Build Harbor images based on Ubuntu
```sh
$ make build -e BASEIMAGE=ubuntu
```
#### Push Harbor images to specific registry server
```sh

View File

@ -1347,7 +1347,70 @@ paths:
404:
description: Not found the default root certificate.
500:
description: Unexpected internal errors.
description: Unexpected internal errors.
/ldap/ping:
post:
summary: Ping available ldap service.
description: |
This endpoint ping the available ldap service for test related configuration parameters.
parameters:
- name: ldapconf
in: body
description: ldap configuration.
required: true
schema:
$ref: '#/definitions/LdapConf'
tags:
- Products
responses:
200:
description: Ping ldap service successfully.
401:
description: Only admin has this authority.
403:
description: Inviald ldap configuration parameters.
500:
description: Unexpected internal errors.
/configurations:
get:
summary: Get system configurations.
description: |
This endpoint is for retrieving system configurations that only provides for admin user.
tags:
- Products
responses:
200:
description: Get system configurations successfully. The response body is a map.
schema:
type: object
401:
description: User need to log in first.
403:
description: User does not have permission of admin role.
500:
description: Unexpected internal errors.
put:
summary: Modify system configurations.
description: |
This endpoint is for modifying system configurations that only provides for admin user.
tags:
- Products
parameters:
- name: configurations
in: body
required: true
schema:
type: object
description: The configurations map need to be modified, the following are keys "auth_mode", "email_from", "email_host", "email_identity", "email_password", "email_port", "email_ssl", "email_username", "ldap_base_dn", "ldap_filter", "ldap_scope", "ldap_search_dn", "ldap_search_password", "ldap_timeout", "ldap_uid", "ldap_url", "project_creation_restriction", "self_registration", "verify_remote_cert".
responses:
200:
description: Modify system configurations successfully.
401:
description: User need to log in first.
403:
description: User does not have permission of admin role.
500:
description: Unexpected internal errors.
definitions:
Search:
type: object
@ -1798,3 +1861,32 @@ definitions:
description: The storage of system.
items:
$ref: '#/definitions/Storage'
LdapConf:
type: object
properties:
ldap_url:
type: string
description: The url of ldap service.
ldap_search_dn:
type: string
description: The search dn of ldap service.
ldap_search_password:
type: string
description: The search password of ldap service.
ldap_base_dn:
type: string
description: The base dn of ldap service.
ldap_filter:
type: string
description: The serach filter of ldap service.
ldap_uid:
type: string
description: The serach uid of ldap service.
ldap_scope:
type: integer
format: int64
description: The serach scope of ldap service.
ldap_connection_timeout:
type: integer
format: int64
description: The connect timeout of ldap service(second).

View File

@ -0,0 +1,38 @@
LOG_LEVEL=debug
EXT_ENDPOINT=$ui_url
AUTH_MODE=$auth_mode
SELF_REGISTRATION=$self_registration
LDAP_URL=$ldap_url
LDAP_SEARCH_DN=$ldap_searchdn
LDAP_SEARCH_PWD=$ldap_search_pwd
LDAP_BASE_DN=$ldap_basedn
LDAP_FILTER=$ldap_filter
LDAP_UID=$ldap_uid
LDAP_SCOPE=$ldap_scope
LDAP_TIMEOUT=$ldap_timeout
DATABASE_TYPE=mysql
MYSQL_HOST=mysql
MYSQL_PORT=3306
MYSQL_USR=root
MYSQL_PWD=$db_password
MYSQL_DATABASE=registry
REGISTRY_URL=http://registry:5000
TOKEN_SERVICE_URL=http://ui/service/token
EMAIL_HOST=$email_host
EMAIL_PORT=$email_port
EMAIL_USR=$email_usr
EMAIL_PWD=$email_pwd
EMAIL_SSL=$email_ssl
EMAIL_FROM=$email_from
EMAIL_IDENTITY=$email_identity
HARBOR_ADMIN_PASSWORD=$harbor_admin_password
PROJECT_CREATION_RESTRICTION=$project_creation_restriction
VERIFY_REMOTE_CERT=$verify_remote_cert
MAX_JOB_WORKERS=$max_job_workers
LOG_DIR=/var/log/jobs
UI_SECRET=$ui_secret
JOBSERVICE_SECRET=$jobservice_secret
TOKEN_EXPIRATION=$token_expiration
CFG_EXPIRATION=5
USE_COMPRESSED_JS=$use_compressed_js
GODEBUG=netdns=cgo

View File

@ -1,15 +1,5 @@
MYSQL_HOST=mysql
MYSQL_PORT=3306
MYSQL_USR=root
MYSQL_PWD=$db_password
UI_SECRET=$ui_secret
SECRET_KEY=$secret_key
CONFIG_PATH=/etc/jobservice/app.conf
REGISTRY_URL=http://registry:5000
VERIFY_REMOTE_CERT=$verify_remote_cert
MAX_JOB_WORKERS=$max_job_workers
LOG_LEVEL=debug
LOG_DIR=/var/log/jobs
CONFIG_PATH=/etc/jobservice/app.conf
UI_SECRET=$ui_secret
JOBSERVICE_SECRET=$jobservice_secret
GODEBUG=netdns=cgo
EXT_ENDPOINT=$ui_url
TOKEN_ENDPOINT=http://ui

View File

@ -6,13 +6,4 @@ types = en-US|zh-CN
names = en-US|zh-CN
[dev]
httpport = 80
[mail]
identity = $email_identity
host = $email_server
port = $email_server_port
username = $email_username
password = $email_password
from = $email_from
ssl = $email_ssl
httpport = 80

View File

@ -1,30 +1,5 @@
MYSQL_HOST=mysql
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
EXT_REG_URL=$hostname
HARBOR_ADMIN_PASSWORD=$harbor_admin_password
AUTH_MODE=$auth_mode
LDAP_URL=$ldap_url
LDAP_SEARCH_DN=$ldap_searchdn
LDAP_SEARCH_PWD=$ldap_search_pwd
LDAP_BASE_DN=$ldap_basedn
LDAP_FILTER=$ldap_filter
LDAP_UID=$ldap_uid
LDAP_SCOPE=$ldap_scope
LDAP_CONNECT_TIMEOUT=$ldap_connect_timeout
UI_SECRET=$ui_secret
SECRET_KEY=$secret_key
SELF_REGISTRATION=$self_registration
USE_COMPRESSED_JS=$use_compressed_js
LOG_LEVEL=debug
CONFIG_PATH=/etc/ui/app.conf
UI_SECRET=$ui_secret
JOBSERVICE_SECRET=$jobservice_secret
GODEBUG=netdns=cgo
EXT_ENDPOINT=$ui_url
TOKEN_ENDPOINT=http://ui
VERIFY_REMOTE_CERT=$verify_remote_cert
TOKEN_EXPIRATION=$token_expiration
PROJECT_CREATION_RESTRICTION=$project_creation_restriction

View File

@ -0,0 +1,12 @@
FROM golang:1.7.3
MAINTAINER yinw@vmware.com
COPY . /go/src/github.com/vmware/harbor
WORKDIR /go/src/github.com/vmware/harbor/src/adminserver
RUN go build -v -a -o /go/bin/harbor_adminserver \
&& chmod u+x /go/bin/harbor_adminserver
WORKDIR /go/bin/
ENTRYPOINT ["/go/bin/harbor_adminserver"]

View File

@ -3,7 +3,7 @@ services:
log:
build:
context: ../../
dockerfile: make/ubuntu/log/Dockerfile
dockerfile: make/photon/log/Dockerfile
restart: always
volumes:
- /var/log/harbor/:/var/log/docker/
@ -40,6 +40,23 @@ services:
options:
syslog-address: "tcp://127.0.0.1:1514"
tag: "mysql"
adminserver:
build:
context: ../../
dockerfile: make/dev/adminserver/Dockerfile
env_file:
- ../common/config/adminserver/env
restart: always
volumes:
- /data/config/:/etc/adminserver/
- /data/secretkey:/etc/adminserver/key
depends_on:
- log
logging:
driver: "syslog"
options:
syslog-address: "tcp://127.0.0.1:1514"
tag: "adminserver"
ui:
build:
context: ../../
@ -50,8 +67,11 @@ services:
volumes:
- ../common/config/ui/app.conf:/etc/ui/app.conf
- ../common/config/ui/private_key.pem:/etc/ui/private_key.pem
- /data/secretkey:/etc/ui/key
depends_on:
- log
- adminserver
- registry
logging:
driver: "syslog"
options:
@ -67,8 +87,10 @@ services:
volumes:
- /data/job_logs:/var/log/jobs
- ../common/config/jobservice/app.conf:/etc/jobservice/app.conf
- /data/secretkey:/etc/jobservice/key
depends_on:
- ui
- adminserver
logging:
driver: "syslog"
options:

View File

@ -47,6 +47,24 @@ services:
options:
syslog-address: "tcp://127.0.0.1:1514"
tag: "mysql"
adminserver:
image: vmware/harbor-adminserver
container_name: harbor-adminserver
env_file:
- ./common/config/adminserver/env
restart: always
volumes:
- /data/config/:/etc/adminserver/
- /data/secretkey:/etc/adminserver/key
networks:
- harbor
depends_on:
- log
logging:
driver: "syslog"
options:
syslog-address: "tcp://127.0.0.1:1514"
tag: "adminserver"
ui:
image: vmware/harbor-ui
container_name: harbor-ui
@ -57,10 +75,13 @@ services:
- ./common/config/ui/app.conf:/etc/ui/app.conf
- ./common/config/ui/private_key.pem:/etc/ui/private_key.pem
- /data:/harbor_storage
- /data/secretkey:/etc/ui/key
networks:
- harbor
depends_on:
- log
- adminserver
- registry
logging:
driver: "syslog"
options:
@ -75,10 +96,12 @@ services:
volumes:
- /data/job_logs:/var/log/jobs
- ./common/config/jobservice/app.conf:/etc/jobservice/app.conf
- /data/secretkey:/etc/jobservice/key
networks:
- harbor
depends_on:
- ui
- adminserver
logging:
driver: "syslog"
options:

View File

@ -53,7 +53,7 @@ ldap_uid = uid
ldap_scope = 3
#Timeout (in seconds) when connecting to an LDAP Server. The default value (and most reasonable) is 5 seconds.
ldap_connect_timeout = 5
ldap_timeout = 5
#The password for the root user of mysql db, change this before any production use.
db_password = root123

View File

@ -3,7 +3,7 @@
# Targets:
#
# build: build harbor photon images
# clean: clean ui and jobservice harbor images
# clean: clean adminserver, ui and jobservice harbor images
# common
SHELL := /bin/bash
@ -22,6 +22,9 @@ DOCKERRMIMAGE=$(DOCKERCMD) rmi
DOCKERIMASES=$(DOCKERCMD) images
# binary
ADMINSERVERSOURCECODE=$(SRCPATH)/adminserver
ADMINSERVERBINARYPATH=$(MAKEDEVPATH)/adminserver
ADMINSERVERBINARYNAME=harbor_adminserver
UISOURCECODE=$(SRCPATH)/ui
UIBINARYPATH=$(MAKEDEVPATH)/ui
UIBINARYNAME=harbor_ui
@ -31,6 +34,9 @@ JOBSERVICEBINARYNAME=harbor_jobservice
# photon dockerfile
DOCKERFILEPATH=$(MAKEPATH)/photon
DOCKERFILEPATH_ADMINSERVER=$(DOCKERFILEPATH)/adminserver
DOCKERFILENAME_ADMINSERVER=Dockerfile
DOCKERIMAGENAME_ADMINSERVER=vmware/harbor-adminserver
DOCKERFILEPATH_UI=$(DOCKERFILEPATH)/ui
DOCKERFILENAME_UI=Dockerfile
DOCKERIMAGENAME_UI=vmware/harbor-ui
@ -56,6 +62,10 @@ check_environment:
@$(MAKEPATH)/$(CHECKENVCMD)
build:
@echo "building adminserver container for photon..."
$(DOCKERBUILD) -f $(DOCKERFILEPATH_ADMINSERVER)/$(DOCKERFILENAME_ADMINSERVER) -t $(DOCKERIMAGENAME_ADMINSERVER):$(VERSIONTAG) .
@echo "Done."
@echo "building ui container for photon..."
$(DOCKERBUILD) -f $(DOCKERFILEPATH_UI)/$(DOCKERFILENAME_UI) -t $(DOCKERIMAGENAME_UI):$(VERSIONTAG) .
@echo "Done."
@ -70,6 +80,7 @@ build:
cleanimage:
@echo "cleaning image for photon..."
- $(DOCKERRMIMAGE) -f $(DOCKERIMAGENAME_ADMINSERVER):$(VERSIONTAG)
- $(DOCKERRMIMAGE) -f $(DOCKERIMAGENAME_UI):$(VERSIONTAG)
- $(DOCKERRMIMAGE) -f $(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG)
- $(DOCKERRMIMAGE) -f $(DOCKERIMAGENAME_LOG):$(VERSIONTAG)

View File

@ -0,0 +1,8 @@
FROM library/photon:1.0
RUN mkdir /harbor/
COPY ./make/dev/adminserver/harbor_adminserver /harbor/
RUN chmod u+x /harbor/harbor_adminserver
WORKDIR /harbor/
ENTRYPOINT ["/harbor/harbor_adminserver"]

View File

@ -103,10 +103,10 @@ 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")
email_password = rcp.get("configuration", "email_password")
email_host = rcp.get("configuration", "email_server")
email_port = rcp.get("configuration", "email_server_port")
email_usr = rcp.get("configuration", "email_username")
email_pwd = rcp.get("configuration", "email_password")
email_from = rcp.get("configuration", "email_from")
email_ssl = rcp.get("configuration", "email_ssl")
harbor_admin_password = rcp.get("configuration", "harbor_admin_password")
@ -127,7 +127,7 @@ else:
ldap_filter = ""
ldap_uid = rcp.get("configuration", "ldap_uid")
ldap_scope = rcp.get("configuration", "ldap_scope")
ldap_connect_timeout = rcp.get("configuration", "ldap_connect_timeout")
ldap_timeout = rcp.get("configuration", "ldap_timeout")
db_password = rcp.get("configuration", "db_password")
self_registration = rcp.get("configuration", "self_registration")
use_compressed_js = rcp.get("configuration", "use_compressed_js")
@ -151,6 +151,11 @@ secret_key = get_secret_key(secretkey_path)
########
ui_secret = ''.join(random.choice(string.ascii_letters+string.digits) for i in range(16))
jobservice_secret = ''.join(random.choice(string.ascii_letters+string.digits) for i in range(16))
adminserver_config_dir = os.path.join(config_dir,"adminserver")
if not os.path.exists(adminserver_config_dir):
os.makedirs(os.path.join(config_dir, "adminserver"))
ui_config_dir = prep_conf_dir(config_dir,"ui")
db_config_dir = prep_conf_dir(config_dir, "db")
@ -159,6 +164,7 @@ registry_config_dir = prep_conf_dir(config_dir, "registry")
nginx_config_dir = prep_conf_dir (config_dir, "nginx")
nginx_conf_d = prep_conf_dir(nginx_config_dir, "conf.d")
adminserver_conf_env = os.path.join(config_dir, "adminserver", "env")
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")
@ -182,14 +188,12 @@ if protocol == "https":
else:
render(os.path.join(templates_dir, "nginx", "nginx.http.conf"),
nginx_conf)
render(os.path.join(templates_dir, "ui", "env"),
ui_conf_env,
hostname=hostname,
db_password=db_password,
ui_url=ui_url,
auth_mode=auth_mode,
harbor_admin_password=harbor_admin_password,
render(os.path.join(templates_dir, "adminserver", "env"),
adminserver_conf_env,
ui_url=ui_url,
auth_mode=auth_mode,
self_registration=self_registration,
ldap_url=ldap_url,
ldap_searchdn =ldap_searchdn,
ldap_search_pwd =ldap_search_pwd,
@ -197,27 +201,32 @@ render(os.path.join(templates_dir, "ui", "env"),
ldap_filter=ldap_filter,
ldap_uid=ldap_uid,
ldap_scope=ldap_scope,
ldap_connect_timeout=ldap_connect_timeout,
self_registration=self_registration,
use_compressed_js=use_compressed_js,
ui_secret=ui_secret,
secret_key=secret_key,
verify_remote_cert=verify_remote_cert,
project_creation_restriction=proj_cre_restriction,
token_expiration=token_expiration)
ldap_timeout=ldap_timeout,
db_password=db_password,
email_host=email_host,
email_port=email_port,
email_usr=email_usr,
email_pwd=email_pwd,
email_ssl=email_ssl,
email_from=email_from,
email_identity=email_identity,
harbor_admin_password=harbor_admin_password,
project_creation_restriction=proj_cre_restriction,
verify_remote_cert=verify_remote_cert,
max_job_workers=max_job_workers,
ui_secret=ui_secret,
jobservice_secret=jobservice_secret,
token_expiration=token_expiration,
use_compressed_js=use_compressed_js
)
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,
email_password=email_password,
email_from=email_from,
email_ssl=email_ssl,
ui_url=ui_url)
render(os.path.join(templates_dir, "ui", "env"),
ui_conf_env,
ui_secret=ui_secret,
jobservice_secret=jobservice_secret,)
render(os.path.join(templates_dir, "registry", "config.yml"),
render(os.path.join(templates_dir, "registry",
"config.yml"),
registry_conf,
ui_url=ui_url)
@ -227,16 +236,16 @@ render(os.path.join(templates_dir, "db", "env"),
render(os.path.join(templates_dir, "jobservice", "env"),
job_conf_env,
db_password=db_password,
ui_secret=ui_secret,
max_job_workers=max_job_workers,
secret_key=secret_key,
ui_url=ui_url,
verify_remote_cert=verify_remote_cert)
jobservice_secret=jobservice_secret)
print("Generated configuration file: %s" % jobservice_conf)
shutil.copyfile(os.path.join(templates_dir, "jobservice", "app.conf"), jobservice_conf)
print("Generated configuration file: %s" % ui_conf)
shutil.copyfile(os.path.join(templates_dir, "ui", "app.conf"), ui_conf)
def validate_crt_subj(dirty_subj):
subj_list = [item for item in dirty_subj.strip().split("/") \
if len(item.split("=")) == 2 and len(item.split("=")[1]) > 0]

View File

@ -1,79 +0,0 @@
# Makefile for a harbor project
#
# Targets:
#
# build: build harbor ubuntu images
# clean: clean ui and jobservice harbor images
# common
SHELL := /bin/bash
BUILDPATH=$(CURDIR)
MAKEPATH=$(BUILDPATH)/make
MAKEDEVPATH=$(MAKEPATH)/dev
SRCPATH=./src
TOOLSPATH=$(BUILDPATH)/tools
CHECKENVCMD=checkenv.sh
DEVFLAG=true
# docker parameters
DOCKERCMD=$(shell which docker)
DOCKERBUILD=$(DOCKERCMD) build
DOCKERRMIMAGE=$(DOCKERCMD) rmi
DOCKERIMASES=$(DOCKERCMD) images
# binary
UISOURCECODE=$(SRCPATH)/ui
UIBINARYPATH=$(MAKEDEVPATH)/ui
UIBINARYNAME=harbor_ui
JOBSERVICESOURCECODE=$(SRCPATH)/jobservice
JOBSERVICEBINARYPATH=$(MAKEDEVPATH)/jobservice
JOBSERVICEBINARYNAME=harbor_jobservice
# ubuntu dockerfile
DOCKERFILEPATH=$(MAKEPATH)/ubuntu
DOCKERFILEPATH_UI=$(DOCKERFILEPATH)/ui
DOCKERFILENAME_UI=Dockerfile
DOCKERIMAGENAME_UI=vmware/harbor-ui
DOCKERFILEPATH_JOBSERVICE=$(DOCKERFILEPATH)/jobservice
DOCKERFILENAME_JOBSERVICE=Dockerfile
DOCKERIMAGENAME_JOBSERVICE=vmware/harbor-jobservice
DOCKERFILEPATH_LOG=$(DOCKERFILEPATH)/log
DOCKERFILENAME_LOG=Dockerfile
DOCKERIMAGENAME_LOG=vmware/harbor-log
# version prepare
VERSIONFILEPATH=$(SRCPATH)/views/sections
VERSIONFILENAME=header-content.htm
GITCMD=$(shell which git)
GITTAG=$(GITCMD) describe --tags
ifeq ($(DEVFLAG), true)
VERSIONTAG=dev
else
VERSIONTAG=$(shell $(GITTAG))
endif
check_environment:
@$(MAKEPATH)/$(CHECKENVCMD)
build:
@echo "building ui container for ubuntu..."
$(DOCKERBUILD) -f $(DOCKERFILEPATH_UI)/$(DOCKERFILENAME_UI) -t $(DOCKERIMAGENAME_UI):$(VERSIONTAG) .
@echo "Done."
@echo "building jobservice container for ubuntu..."
$(DOCKERBUILD) -f $(DOCKERFILEPATH_JOBSERVICE)/$(DOCKERFILENAME_JOBSERVICE) -t $(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG) .
@echo "Done."
@echo "building log container for ubuntu..."
$(DOCKERBUILD) -f $(DOCKERFILEPATH_LOG)/$(DOCKERFILENAME_LOG) -t $(DOCKERIMAGENAME_LOG):$(VERSIONTAG) .
@echo "Done."
cleanimage:
@echo "cleaning image for ubuntu..."
- $(DOCKERRMIMAGE) -f $(DOCKERIMAGENAME_UI):$(VERSIONTAG)
- $(DOCKERRMIMAGE) -f $(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG)
- $(DOCKERRMIMAGE) -f $(DOCKERIMAGENAME_LOG):$(VERSIONTAG)
.PHONY: clean
clean: cleanimage

View File

@ -1,11 +0,0 @@
FROM library/ubuntu:14.04
MAINTAINER jiangd@vmware.com
RUN mkdir /harbor/
COPY ./make/dev/jobservice/harbor_jobservice /harbor/
RUN chmod u+x /harbor/harbor_jobservice
WORKDIR /harbor/
ENTRYPOINT ["/harbor/harbor_jobservice"]

View File

@ -1,18 +0,0 @@
FROM library/ubuntu:14.04
RUN rm /etc/rsyslog.d/* && rm /etc/rsyslog.conf
ADD make/common/log/rsyslog.conf /etc/rsyslog.conf
# rotate logs weekly
# notes: file name cannot contain dot, or the script will not run
ADD make/common/log/rotate.sh /etc/cron.weekly/rotate
# rsyslog configuration file for docker
ADD make/common/log/rsyslog_docker.conf /etc/rsyslog.d/
VOLUME /var/log/docker/
EXPOSE 514
CMD cron && rsyslogd -n

View File

@ -1,26 +0,0 @@
FROM library/ubuntu:14.04
MAINTAINER jiangd@vmware.com
ENV MYSQL_USR root \
MYSQL_PWD root \
REGISTRY_URL localhost:5000
RUN mkdir /harbor/
COPY ./make/dev/ui/harbor_ui /harbor/
COPY ./src/ui/views /harbor/views
COPY ./src/ui/static /harbor/static
COPY ./src/favicon.ico /harbor/favicon.ico
COPY ./make/jsminify.sh /tmp/jsminify.sh
RUN chmod u+x /harbor/harbor_ui \
&& 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"]
EXPOSE 80

View File

@ -0,0 +1,34 @@
/*
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 (
"net/http"
)
func handleInternalServerError(w http.ResponseWriter) {
http.Error(w, http.StatusText(http.StatusInternalServerError),
http.StatusInternalServerError)
}
func handleBadRequestError(w http.ResponseWriter, error string) {
http.Error(w, error, http.StatusBadRequest)
}
func handleUnauthorized(w http.ResponseWriter) {
http.Error(w, http.StatusText(http.StatusUnauthorized),
http.StatusUnauthorized)
}

View File

@ -0,0 +1,32 @@
/*
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 (
"net/http"
"net/http/httptest"
"testing"
)
func TestHandleInternalServerError(t *testing.T) {
w := httptest.NewRecorder()
handleInternalServerError(w)
if w.Code != http.StatusInternalServerError {
t.Errorf("unexpected status code: %d != %d", w.Code, http.StatusInternalServerError)
}
}

106
src/adminserver/api/cfg.go Normal file
View File

@ -0,0 +1,106 @@
/*
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 (
"encoding/json"
"io/ioutil"
"net/http"
"os"
cfg "github.com/vmware/harbor/src/adminserver/systemcfg"
"github.com/vmware/harbor/src/common/utils/log"
)
func isAuthenticated(r *http.Request) (bool, error) {
uiSecret := os.Getenv("UI_SECRET")
jobserviceSecret := os.Getenv("JOBSERVICE_SECRET")
c, err := r.Cookie("secret")
if err != nil {
if err == http.ErrNoCookie {
return false, nil
}
return false, err
}
return c != nil && (c.Value == uiSecret ||
c.Value == jobserviceSecret), nil
}
// ListCfgs lists configurations
func ListCfgs(w http.ResponseWriter, r *http.Request) {
authenticated, err := isAuthenticated(r)
if err != nil {
log.Errorf("failed to check whether the request is authenticated or not: %v", err)
handleInternalServerError(w)
return
}
if !authenticated {
handleUnauthorized(w)
return
}
cfg, err := cfg.GetSystemCfg()
if err != nil {
log.Errorf("failed to get system configurations: %v", err)
handleInternalServerError(w)
return
}
b, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
log.Errorf("failed to marshal configurations: %v", err)
handleInternalServerError(w)
return
}
if _, err = w.Write(b); err != nil {
log.Errorf("failed to write response: %v", err)
}
}
// UpdateCfgs updates configurations
func UpdateCfgs(w http.ResponseWriter, r *http.Request) {
authenticated, err := isAuthenticated(r)
if err != nil {
log.Errorf("failed to check whether the request is authenticated or not: %v", err)
handleInternalServerError(w)
return
}
if !authenticated {
handleUnauthorized(w)
return
}
b, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Errorf("failed to read request body: %v", err)
handleInternalServerError(w)
return
}
m := map[string]interface{}{}
if err = json.Unmarshal(b, &m); err != nil {
handleBadRequestError(w, err.Error())
return
}
if err = cfg.UpdateSystemCfg(m); err != nil {
log.Errorf("failed to update system configurations: %v", err)
handleInternalServerError(w)
return
}
}

View File

@ -0,0 +1,183 @@
/*
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 (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/vmware/harbor/src/adminserver/systemcfg"
comcfg "github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/utils/test"
)
func TestConfigAPI(t *testing.T) {
configPath := "/tmp/config.json"
secretKeyPath := "/tmp/secretkey"
_, err := test.GenerateKey(secretKeyPath)
if err != nil {
t.Errorf("failed to generate secret key: %v", err)
return
}
defer os.Remove(secretKeyPath)
secret := "secret"
envs := map[string]string{
"JSON_CFG_STORE_PATH": configPath,
"KEY_PATH": secretKeyPath,
"UI_SECRET": secret,
"MYSQL_PORT": "3306",
"TOKEN_EXPIRATION": "30",
"CFG_EXPIRATION": "5",
"MAX_JOB_WORKERS": "3",
"LDAP_SCOPE": "3",
"LDAP_TIMEOUT": "30",
"EMAIL_PORT": "25",
"MYSQL_PWD": "",
"LDAP_SEARCH_PWD": "",
"EMAIL_PWD": "",
"HARBOR_ADMIN_PASSWORD": "",
}
for k, v := range envs {
if err := os.Setenv(k, v); err != nil {
t.Errorf("failed to set env %s: %v", k, err)
return
}
}
defer os.Remove(configPath)
if err := systemcfg.Init(); err != nil {
t.Errorf("failed to initialize system configurations: %v", err)
return
}
r, err := http.NewRequest("GET", "", nil)
if err != nil {
t.Errorf("failed to create request: %v", err)
return
}
w := httptest.NewRecorder()
ListCfgs(w, r)
if w.Code != http.StatusUnauthorized {
t.Errorf("unexpected status code: %d != %d", w.Code, http.StatusUnauthorized)
return
}
r.AddCookie(&http.Cookie{
Name: "secret",
Value: secret,
})
w = httptest.NewRecorder()
ListCfgs(w, r)
if w.Code != http.StatusOK {
t.Errorf("unexpected status code: %d != %d", w.Code, http.StatusOK)
return
}
m, err := parse(w.Body)
if err != nil {
t.Errorf("failed to parse response body: %v", err)
return
}
scope := int(m[comcfg.LDAPScope].(float64))
if scope != 3 {
t.Errorf("unexpected ldap scope: %d != %d", scope, 3)
return
}
// modify configurations
c := map[string]interface{}{
comcfg.AUTHMode: comcfg.LDAPAuth,
}
b, err := json.Marshal(c)
if err != nil {
t.Errorf("failed to marshal configuartions: %v", err)
return
}
w = httptest.NewRecorder()
r, err = http.NewRequest("GET", "", bytes.NewReader(b))
if err != nil {
t.Errorf("failed to create request: %v", err)
return
}
r.AddCookie(&http.Cookie{
Name: "secret",
Value: secret,
})
UpdateCfgs(w, r)
if w.Code != http.StatusOK {
t.Errorf("unexpected status code: %d != %d", w.Code, http.StatusOK)
return
}
// confirm the modification is done
r, err = http.NewRequest("GET", "", nil)
if err != nil {
t.Errorf("failed to create request: %v", err)
return
}
r.AddCookie(&http.Cookie{
Name: "secret",
Value: secret,
})
w = httptest.NewRecorder()
ListCfgs(w, r)
if w.Code != http.StatusOK {
t.Errorf("unexpected status code: %d != %d", w.Code, http.StatusOK)
return
}
m, err = parse(w.Body)
if err != nil {
t.Errorf("failed to parse response body: %v", err)
return
}
mode := m[comcfg.AUTHMode].(string)
if mode != comcfg.LDAPAuth {
t.Errorf("unexpected ldap scope: %s != %s", mode, comcfg.LDAPAuth)
return
}
}
func parse(reader io.Reader) (map[string]interface{}, error) {
b, err := ioutil.ReadAll(reader)
if err != nil {
return nil, err
}
m := map[string]interface{}{}
if err := json.Unmarshal(b, &m); err != nil {
return nil, err
}
return m, nil
}

60
src/adminserver/main.go Normal file
View File

@ -0,0 +1,60 @@
/*
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 main
import (
"net/http"
"os"
syscfg "github.com/vmware/harbor/src/adminserver/systemcfg"
"github.com/vmware/harbor/src/common/utils/log"
)
// Server for admin component
type Server struct {
Port string
Handler http.Handler
}
// Serve the API
func (s *Server) Serve() error {
server := &http.Server{
Addr: ":" + s.Port,
Handler: s.Handler,
}
return server.ListenAndServe()
}
func main() {
log.Info("initializing system configurations...")
if err := syscfg.Init(); err != nil {
log.Fatalf("failed to initialize the system: %v", err)
}
log.Info("system initialization completed")
port := os.Getenv("PORT")
if len(port) == 0 {
port = "80"
}
server := &Server{
Port: port,
Handler: newHandler(),
}
if err := server.Serve(); err != nil {
log.Fatal(err)
}
}

30
src/adminserver/router.go Normal file
View File

@ -0,0 +1,30 @@
/*
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 main
import (
"net/http"
"github.com/gorilla/mux"
"github.com/vmware/harbor/src/adminserver/api"
)
func newHandler() http.Handler {
r := mux.NewRouter()
r.HandleFunc("/api/configurations", api.ListCfgs).Methods("GET")
r.HandleFunc("/api/configurations", api.UpdateCfgs).Methods("PUT")
return r
}

View File

@ -0,0 +1,27 @@
/*
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 store
// Driver defines methods that a configuration store driver must implement
type Driver interface {
// Name returns a human-readable name of the driver
Name() string
// Read reads all the configurations from store
Read() (map[string]interface{}, error)
// Write writes the configurations to store, the configurations can be
// part of all
Write(map[string]interface{}) error
}

View File

@ -0,0 +1,124 @@
/*
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 json
import (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"sync"
"github.com/vmware/harbor/src/adminserver/systemcfg/store"
"github.com/vmware/harbor/src/common/utils/log"
)
const (
// the default path of configuration file
defaultPath = "/etc/harbor/config.json"
)
type cfgStore struct {
path string // the path of cfg file
sync.RWMutex
}
// NewCfgStore returns an instance of cfgStore that stores the configurations
// in a json file. The file will be created if it does not exist.
func NewCfgStore(path ...string) (store.Driver, error) {
p := defaultPath
if len(path) > 0 && len(path[0]) > 0 {
p = path[0]
}
log.Debugf("path of configuration file: %s", p)
if _, err := os.Stat(p); os.IsNotExist(err) {
log.Infof("the configuration file %s does not exist, creating it...", p)
if err = os.MkdirAll(filepath.Dir(p), 0600); err != nil {
return nil, err
}
if err = ioutil.WriteFile(p, []byte{}, 0600); err != nil {
return nil, err
}
}
return &cfgStore{
path: p,
}, nil
}
// Name ...
func (c *cfgStore) Name() string {
return "JSON"
}
// Read ...
func (c *cfgStore) Read() (map[string]interface{}, error) {
c.RLock()
defer c.RUnlock()
return read(c.path)
}
func read(path string) (map[string]interface{}, error) {
b, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
// empty file
if len(b) == 0 {
return nil, nil
}
config := map[string]interface{}{}
if err = json.Unmarshal(b, &config); err != nil {
return nil, err
}
return config, nil
}
// Write ...
func (c *cfgStore) Write(config map[string]interface{}) error {
c.Lock()
defer c.Unlock()
cfg, err := read(c.path)
if err != nil {
return err
}
if cfg == nil {
cfg = config
} else {
for k, v := range config {
cfg[k] = v
}
}
b, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}
if err = ioutil.WriteFile(c.path, b, 0600); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,52 @@
/*
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 json
import (
"os"
"testing"
)
func TestReadWrite(t *testing.T) {
path := "/tmp/config.json"
store, err := NewCfgStore(path)
if err != nil {
t.Fatalf("failed to create json cfg store: %v", err)
}
defer func() {
if err := os.Remove(path); err != nil {
t.Fatalf("failed to remove the json file %s: %v", path, err)
}
}()
if store.Name() != "JSON" {
t.Errorf("unexpected name: %s != %s", store.Name(), "JSON")
return
}
config := map[string]interface{}{
"key": "value",
}
if err := store.Write(config); err != nil {
t.Errorf("failed to write configurations to json file: %v", err)
return
}
if _, err = store.Read(); err != nil {
t.Errorf("failed to read configurations from json file: %v", err)
return
}
}

View File

@ -0,0 +1,298 @@
/*
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 systemcfg
import (
"fmt"
"os"
"strconv"
"strings"
"github.com/vmware/harbor/src/adminserver/systemcfg/store"
"github.com/vmware/harbor/src/adminserver/systemcfg/store/json"
comcfg "github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/utils"
"github.com/vmware/harbor/src/common/utils/log"
)
const (
defaultCfgStoreDriver string = "json"
defaultJSONCfgStorePath string = "/etc/adminserver/config.json"
defaultKeyPath string = "/etc/adminserver/key"
)
var (
cfgStore store.Driver
keyProvider comcfg.KeyProvider
// attrs need to be encrypted or decrypted
attrs = []string{
comcfg.EmailPassword,
comcfg.LDAPSearchPwd,
comcfg.MySQLPassword,
comcfg.AdminInitialPassword,
}
// envs are configurations need read from environment variables
envs = map[string]interface{}{
comcfg.ExtEndpoint: "EXT_ENDPOINT",
comcfg.AUTHMode: "AUTH_MODE",
comcfg.SelfRegistration: &parser{
env: "SELF_REGISTRATION",
parse: parseStringToBool,
},
comcfg.DatabaseType: "DATABASE_TYPE",
comcfg.MySQLHost: "MYSQL_HOST",
comcfg.MySQLPort: &parser{
env: "MYSQL_PORT",
parse: parseStringToInt,
},
comcfg.MySQLUsername: "MYSQL_USR",
comcfg.MySQLPassword: "MYSQL_PWD",
comcfg.MySQLDatabase: "MYSQL_DATABASE",
comcfg.SQLiteFile: "SQLITE_FILE",
comcfg.LDAPURL: "LDAP_URL",
comcfg.LDAPSearchDN: "LDAP_SEARCH_DN",
comcfg.LDAPSearchPwd: "LDAP_SEARCH_PWD",
comcfg.LDAPBaseDN: "LDAP_BASE_DN",
comcfg.LDAPFilter: "LDAP_FILTER",
comcfg.LDAPUID: "LDAP_UID",
comcfg.LDAPScope: &parser{
env: "LDAP_SCOPE",
parse: parseStringToInt,
},
comcfg.LDAPTimeout: &parser{
env: "LDAP_TIMEOUT",
parse: parseStringToInt,
},
comcfg.EmailHost: "EMAIL_HOST",
comcfg.EmailPort: &parser{
env: "EMAIL_PORT",
parse: parseStringToInt,
},
comcfg.EmailUsername: "EMAIL_USR",
comcfg.EmailPassword: "EMAIL_PWD",
comcfg.EmailSSL: &parser{
env: "EMAIL_SSL",
parse: parseStringToBool,
},
comcfg.EmailFrom: "EMAIL_FROM",
comcfg.EmailIdentity: "EMAIL_IDENTITY",
comcfg.RegistryURL: "REGISTRY_URL",
comcfg.TokenExpiration: &parser{
env: "TOKEN_EXPIRATION",
parse: parseStringToInt,
},
comcfg.JobLogDir: "LOG_DIR",
comcfg.UseCompressedJS: &parser{
env: "USE_COMPRESSED_JS",
parse: parseStringToBool,
},
comcfg.CfgExpiration: &parser{
env: "CFG_EXPIRATION",
parse: parseStringToInt,
},
comcfg.MaxJobWorkers: &parser{
env: "MAX_JOB_WORKERS",
parse: parseStringToInt,
},
comcfg.VerifyRemoteCert: &parser{
env: "VERIFY_REMOTE_CERT",
parse: parseStringToBool,
},
comcfg.ProjectCreationRestriction: "PROJECT_CREATION_RESTRICTION",
comcfg.AdminInitialPassword: "HARBOR_ADMIN_PASSWORD",
}
)
type parser struct {
// the name of env
env string
// parse the value of env, e.g. parse string to int or
// parse string to bool
parse func(string) (interface{}, error)
}
func parseStringToInt(str string) (interface{}, error) {
return strconv.Atoi(str)
}
func parseStringToBool(str string) (interface{}, error) {
return strings.ToLower(str) == "true" ||
strings.ToLower(str) == "on", nil
}
// Init system configurations. Read from config store first,
// if null read from env
func Init() (err error) {
//init configuation store
if err = initCfgStore(); err != nil {
return err
}
//init key provider
initKeyProvider()
cfg, err := GetSystemCfg()
if err != nil {
return err
}
if cfg != nil {
return nil
}
log.Info("configurations read from store driver are null, initializing system from environment variables...")
cfg, err = loadFromEnv()
if err != nil {
return err
}
//sync configurations into cfg store
return UpdateSystemCfg(cfg)
}
func initCfgStore() (err error) {
t := os.Getenv("CFG_STORE_DRIVER")
if len(t) == 0 {
t = defaultCfgStoreDriver
}
log.Infof("configuration store driver: %s", t)
switch t {
case "json":
path := os.Getenv("JSON_CFG_STORE_PATH")
if len(path) == 0 {
path = defaultJSONCfgStorePath
}
log.Infof("json configuration store path: %s", path)
cfgStore, err = json.NewCfgStore(path)
default:
err = fmt.Errorf("unsupported configuration store driver %s", t)
}
return err
}
func initKeyProvider() {
path := os.Getenv("KEY_PATH")
if len(path) == 0 {
path = defaultKeyPath
}
log.Infof("key path: %s", path)
keyProvider = comcfg.NewFileKeyProvider(path)
}
//load the configurations from env
func loadFromEnv() (map[string]interface{}, error) {
cfg := map[string]interface{}{}
for k, v := range envs {
if str, ok := v.(string); ok {
cfg[k] = os.Getenv(str)
continue
}
if parser, ok := v.(*parser); ok {
i, err := parser.parse(os.Getenv(parser.env))
if err != nil {
return nil, err
}
cfg[k] = i
continue
}
return nil, fmt.Errorf("%v is not string or parse type", v)
}
return cfg, nil
}
// GetSystemCfg returns the system configurations
func GetSystemCfg() (map[string]interface{}, error) {
m, err := cfgStore.Read()
if err != nil {
return nil, err
}
key, err := keyProvider.Get(nil)
if err != nil {
return nil, fmt.Errorf("failed to get key: %v", err)
}
if err = decrypt(m, attrs, key); err != nil {
return nil, err
}
return m, nil
}
// UpdateSystemCfg updates the system configurations
func UpdateSystemCfg(cfg map[string]interface{}) error {
key, err := keyProvider.Get(nil)
if err != nil {
return fmt.Errorf("failed to get key: %v", err)
}
if err := encrypt(cfg, attrs, key); err != nil {
return err
}
return cfgStore.Write(cfg)
}
func encrypt(m map[string]interface{}, keys []string, secretKey string) error {
for _, key := range keys {
v, ok := m[key]
if !ok {
continue
}
if len(v.(string)) == 0 {
continue
}
cipherText, err := utils.ReversibleEncrypt(v.(string), secretKey)
if err != nil {
return err
}
m[key] = cipherText
}
return nil
}
func decrypt(m map[string]interface{}, keys []string, secretKey string) error {
for _, key := range keys {
v, ok := m[key]
if !ok {
continue
}
if len(v.(string)) == 0 {
continue
}
text, err := utils.ReversibleDecrypt(v.(string), secretKey)
if err != nil {
return err
}
m[key] = text
}
return nil
}

View File

@ -0,0 +1,120 @@
/*
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 systemcfg
import (
"os"
"testing"
comcfg "github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/utils/test"
)
// test functions in adminserver/systemcfg/systemcfg.go
func TestSystemcfg(t *testing.T) {
configPath := "/tmp/config.json"
if _, err := os.Stat(configPath); err == nil {
if err := os.Remove(configPath); err != nil {
t.Errorf("failed to remove %s: %v", configPath, err)
return
}
} else if !os.IsNotExist(err) {
t.Errorf("failed to check the existence of %s: %v", configPath, err)
return
}
if err := os.Setenv("JSON_CFG_STORE_PATH", configPath); err != nil {
t.Errorf("failed to set env: %v", err)
return
}
keyPath := "/tmp/secretkey"
if _, err := test.GenerateKey(keyPath); err != nil {
t.Errorf("failed to generate key: %v", err)
return
}
defer os.Remove(keyPath)
if err := os.Setenv("KEY_PATH", keyPath); err != nil {
t.Errorf("failed to set env: %v", err)
return
}
m := map[string]string{
"AUTH_MODE": comcfg.DBAuth,
"LDAP_SCOPE": "1",
"LDAP_TIMEOUT": "30",
"MYSQL_PORT": "3306",
"MAX_JOB_WORKERS": "3",
"TOKEN_EXPIRATION": "30",
"CFG_EXPIRATION": "5",
"EMAIL_PORT": "25",
"MYSQL_PWD": "",
"LDAP_SEARCH_PWD": "",
"EMAIL_PWD": "",
"HARBOR_ADMIN_PASSWORD": "",
}
for k, v := range m {
if err := os.Setenv(k, v); err != nil {
t.Fatalf("failed to set env %s: %v", k, err)
}
}
if err := Init(); err != nil {
t.Errorf("failed to initialize system configurations: %v", err)
return
}
defer os.Remove(configPath)
// run Init again to make sure it works well when the configuration file
// already exists
if err := Init(); err != nil {
t.Errorf("failed to initialize system configurations: %v", err)
return
}
cfg, err := GetSystemCfg()
if err != nil {
t.Errorf("failed to get system configurations: %v", err)
return
}
if cfg[comcfg.AUTHMode] != comcfg.DBAuth {
t.Errorf("unexpected auth mode: %s != %s",
cfg[comcfg.AUTHMode], comcfg.DBAuth)
return
}
cfg[comcfg.AUTHMode] = comcfg.LDAPAuth
if err = UpdateSystemCfg(cfg); err != nil {
t.Errorf("failed to update system configurations: %v", err)
return
}
cfg, err = GetSystemCfg()
if err != nil {
t.Errorf("failed to get system configurations: %v", err)
return
}
if cfg[comcfg.AUTHMode] != comcfg.LDAPAuth {
t.Errorf("unexpected auth mode: %s != %s",
cfg[comcfg.AUTHMode], comcfg.DBAuth)
return
}
}

View File

@ -22,7 +22,6 @@ import (
"strconv"
"github.com/astaxie/beego/validation"
"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"
@ -210,8 +209,3 @@ func (b *BaseAPI) GetPaginationParams() (page, pageSize int64) {
return page, pageSize
}
// GetIsInsecure ...
func GetIsInsecure() bool {
return !config.VerifyRemoteCert()
}

View File

@ -13,21 +13,3 @@
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")
}

View File

@ -17,162 +17,249 @@
package config
import (
"bytes"
"encoding/json"
"fmt"
"os"
"io/ioutil"
"net/http"
"strings"
"time"
"github.com/astaxie/beego/cache"
"github.com/vmware/harbor/src/common/utils"
"github.com/vmware/harbor/src/common/utils/log"
)
// 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)
// const variables
const (
DBAuth = "db_auth"
LDAPAuth = "ldap_auth"
ProCrtRestrEveryone = "everyone"
ProCrtRestrAdmOnly = "adminonly"
LDAPScopeBase = "1"
LDAPScopeOnelevel = "2"
LDAPScopeSubtree = "3"
ExtEndpoint = "ext_endpoint"
AUTHMode = "auth_mode"
DatabaseType = "database_type"
MySQLHost = "mysql_host"
MySQLPort = "mysql_port"
MySQLUsername = "mysql_username"
MySQLPassword = "mysql_password"
MySQLDatabase = "mysql_database"
SQLiteFile = "sqlite_file"
SelfRegistration = "self_registration"
LDAPURL = "ldap_url"
LDAPSearchDN = "ldap_search_dn"
LDAPSearchPwd = "ldap_search_password"
LDAPBaseDN = "ldap_base_dn"
LDAPUID = "ldap_uid"
LDAPFilter = "ldap_filter"
LDAPScope = "ldap_scope"
LDAPTimeout = "ldap_timeout"
TokenServiceURL = "token_service_url"
RegistryURL = "registry_url"
EmailHost = "email_host"
EmailPort = "email_port"
EmailUsername = "email_username"
EmailPassword = "email_password"
EmailFrom = "email_from"
EmailSSL = "email_ssl"
EmailIdentity = "email_identity"
ProjectCreationRestriction = "project_creation_restriction"
VerifyRemoteCert = "verify_remote_cert"
MaxJobWorkers = "max_job_workers"
TokenExpiration = "token_expiration"
CfgExpiration = "cfg_expiration"
JobLogDir = "job_log_dir"
UseCompressedJS = "use_compressed_js"
AdminInitialPassword = "admin_initial_password"
)
// Manager manages configurations
type Manager struct {
Loader *Loader
Parser *Parser
Cache bool
cache cache.Cache
key string
}
// 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)
// NewManager returns an instance of Manager
// url: the url from which loader loads configurations
func NewManager(url, secret string, enableCache bool) *Manager {
m := &Manager{
Loader: NewLoader(url, secret),
Parser: &Parser{},
}
return m, nil
if enableCache {
m.Cache = true
m.cache = cache.NewMemoryCache()
m.key = "cfg"
}
return m
}
// ConfParser ...
type ConfParser interface {
//Parse parse the input raw map into a config map
Parse(raw map[string]string, config map[string]interface{}) error
// Init loader
func (m *Manager) Init() error {
return m.Loader.Init()
}
// 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 configurations, if cache is enabled, cache the configurations
func (m *Manager) Load() (map[string]interface{}, error) {
b, err := m.Loader.Load()
if err != nil {
return nil, err
}
c, err := m.Parser.Parse(b)
if err != nil {
return nil, err
}
if m.Cache {
expi, err := getCfgExpiration(c)
if err != nil {
return nil, err
}
if err = m.cache.Put(m.key, c,
time.Duration(expi)*time.Second); err != nil {
return nil, err
}
}
return c, nil
}
// Load reload the configurations
func (conf *Config) Load() error {
rawMap, err := conf.Loader.Load()
func getCfgExpiration(m map[string]interface{}) (int, error) {
if m == nil {
return 0, fmt.Errorf("can not get cfg expiration as configurations are null")
}
expi, ok := m[CfgExpiration]
if !ok {
return 0, fmt.Errorf("cfg expiration is not set")
}
return int(expi.(float64)), nil
}
// Get : if cache is enabled, read configurations from cache,
// if cache is null or cache is disabled it loads configurations directly
func (m *Manager) Get() (map[string]interface{}, error) {
if m.Cache {
c := m.cache.Get(m.key)
if c != nil {
return c.(map[string]interface{}), nil
}
}
return m.Load()
}
// Upload configurations
func (m *Manager) Upload(b []byte) error {
return m.Loader.Upload(b)
}
// Loader loads and uploads configurations
type Loader struct {
url string
secret string
client *http.Client
}
// NewLoader ...
func NewLoader(url, secret string) *Loader {
return &Loader{
url: url,
secret: secret,
client: &http.Client{},
}
}
// Init waits remote server to be ready by testing connections with it
func (l *Loader) Init() error {
addr := l.url
if strings.Contains(addr, "://") {
addr = strings.Split(addr, "://")[1]
}
if !strings.Contains(addr, ":") {
addr = addr + ":80"
}
return utils.TestTCPConn(addr, 60, 2)
}
// Load configurations from remote server
func (l *Loader) Load() ([]byte, error) {
log.Debug("loading configurations...")
req, err := http.NewRequest("GET", l.url+"/api/configurations", nil)
if err != nil {
return nil, err
}
req.AddCookie(&http.Cookie{
Name: "secret",
Value: l.secret,
})
resp, err := l.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%d", resp.StatusCode)
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
log.Debug("configurations load completed")
return b, nil
}
// Upload configuratons to remote server
func (l *Loader) Upload(b []byte) error {
req, err := http.NewRequest("PUT", l.url+"/api/configurations", bytes.NewReader(b))
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
}
req.AddCookie(&http.Cookie{
Name: "secret",
Value: l.secret,
})
// 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)
resp, err := l.client.Do(req)
if err != nil {
return err
}
config["database"] = db
//By default it's true
config["verify_remote_cert"] = raw["VERIFY_REMOTE_CERT"] != "off"
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected http status code: %d", resp.StatusCode)
}
log.Debug("configurations uploaded")
config["ext_endpoint"] = raw["EXT_ENDPOINT"]
config["token_endpoint"] = raw["TOKEN_ENDPOINT"]
config["log_level"] = raw["LOG_LEVEL"]
return nil
}
var commonConfig *Config
// Parser parses configurations
type Parser struct {
}
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)
// Parse parses []byte to a map configuration
func (p *Parser) Parse(b []byte) (map[string]interface{}, error) {
c := map[string]interface{}{}
if err := json.Unmarshal(b, &c); err != nil {
return nil, 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)
return c, nil
}

View File

@ -14,98 +14,5 @@
*/
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")
}
// the functions in common/config/config.go have been tested
// by cases in UI and Jobservice

View File

@ -0,0 +1,47 @@
/*
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 (
"io/ioutil"
)
// KeyProvider provides the key used to encrypt and decrypt attrs
type KeyProvider interface {
// Get returns the key
// params can be used to pass parameters in different implements
Get(params map[string]interface{}) (string, error)
}
// FileKeyProvider reads key from file
type FileKeyProvider struct {
path string
}
// NewFileKeyProvider returns an instance of FileKeyProvider
// path: where the key should be read from
func NewFileKeyProvider(path string) KeyProvider {
return &FileKeyProvider{
path: path,
}
}
// Get returns the key read from file
func (f *FileKeyProvider) Get(params map[string]interface{}) (string, error) {
b, err := ioutil.ReadFile(f.path)
if err != nil {
return "", err
}
return string(b), nil
}

View File

@ -0,0 +1,43 @@
/*
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 (
"io/ioutil"
"os"
"testing"
)
func TestGetOfFileKeyProvider(t *testing.T) {
path := "/tmp/key"
key := "key_content"
if err := ioutil.WriteFile(path, []byte(key), 0777); err != nil {
t.Errorf("failed to write to file %s: %v", path, err)
return
}
defer os.Remove(path)
provider := NewFileKeyProvider(path)
k, err := provider.Get(nil)
if err != nil {
t.Errorf("failed to get key from the file provider: %v", err)
return
}
if k != key {
t.Errorf("unexpected key: %s != %s", k, key)
return
}
}

View File

@ -17,11 +17,12 @@ package dao
import (
"fmt"
"strconv"
"strings"
"sync"
"github.com/astaxie/beego/orm"
"github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
)
@ -39,27 +40,32 @@ type Database interface {
}
// InitDatabase initializes the database
func InitDatabase() {
database, err := getDatabase()
func InitDatabase(database *models.Database) error {
db, err := getDatabase(database)
if err != nil {
panic(err)
return err
}
log.Infof("initializing database: %s", database.String())
if err := database.Register(); err != nil {
panic(err)
log.Infof("initializing database: %s", db.String())
if err := db.Register(); err != nil {
return err
}
log.Info("initialize database completed")
return nil
}
func getDatabase() (db Database, err error) {
switch config.Database() {
func getDatabase(database *models.Database) (db Database, err error) {
switch database.Type {
case "", "mysql":
db = NewMySQL(config.MySQL().Host, config.MySQL().Port, config.MySQL().User,
config.MySQL().Password, config.MySQL().Database)
db = NewMySQL(database.MySQL.Host,
strconv.Itoa(database.MySQL.Port),
database.MySQL.Username,
database.MySQL.Password,
database.MySQL.Database)
case "sqlite":
db = NewSQLite(config.SQLite().FilePath)
db = NewSQLite(database.SQLite.File)
default:
err = fmt.Errorf("invalid database: %s", config.Database())
err = fmt.Errorf("invalid database: %s", database.Type)
}
return
}

32
src/common/dao/config.go Normal file
View File

@ -0,0 +1,32 @@
/*
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 dao
import (
"github.com/vmware/harbor/src/common/models"
)
// AuthModeCanBeModified determines whether auth mode can be
// modified or not. Auth mode can modified when there is only admin
// user in database.
func AuthModeCanBeModified() (bool, error) {
c, err := GetOrmer().QueryTable(&models.User{}).Count()
if err != nil {
return false, err
}
// admin and anonymous
return c == 2, nil
}

View File

@ -0,0 +1,72 @@
/*
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 dao
import (
"testing"
"github.com/vmware/harbor/src/common/models"
)
func TestAuthModeCanBeModified(t *testing.T) {
c, err := GetOrmer().QueryTable(&models.User{}).Count()
if err != nil {
t.Fatalf("failed to count users: %v", err)
}
if c == 2 {
flag, err := AuthModeCanBeModified()
if err != nil {
t.Fatalf("failed to determine whether auth mode can be modified: %v", err)
}
if !flag {
t.Errorf("unexpected result: %t != %t", flag, true)
}
user := models.User{
Username: "user_for_config_test",
Email: "user_for_config_test@vmware.com",
Password: "P@ssword",
Realname: "user_for_config_test",
}
id, err := Register(user)
if err != nil {
t.Fatalf("failed to register user: %v", err)
}
defer func(id int64) {
if err := deleteUser(id); err != nil {
t.Fatalf("failed to delete user %d: %v", id, err)
}
}(id)
flag, err = AuthModeCanBeModified()
if err != nil {
t.Fatalf("failed to determine whether auth mode can be modified: %v", err)
}
if flag {
t.Errorf("unexpected result: %t != %t", flag, false)
}
} else {
flag, err := AuthModeCanBeModified()
if err != nil {
t.Fatalf("failed to determine whether auth mode can be modified: %v", err)
}
if flag {
t.Errorf("unexpected result: %t != %t", flag, false)
}
}
}

View File

@ -17,10 +17,12 @@ package dao
import (
"os"
"strconv"
"testing"
"time"
"github.com/astaxie/beego/orm"
//"github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils"
"github.com/vmware/harbor/src/common/utils/log"
@ -42,7 +44,7 @@ func execUpdate(o orm.Ormer, sql string, params ...interface{}) error {
func clearUp(username string) {
var err error
o := orm.NewOrm()
o := GetOrmer()
o.Begin()
err = execUpdate(o, `delete
@ -156,53 +158,63 @@ func TestMain(m *testing.M) {
}
func testForMySQL(m *testing.M) int {
db := os.Getenv("DATABASE")
defer os.Setenv("DATABASE", db)
os.Setenv("DATABASE", "mysql")
dbHost := os.Getenv("DB_HOST")
dbHost := os.Getenv("MYSQL_HOST")
if len(dbHost) == 0 {
log.Fatalf("environment variable DB_HOST is not set")
log.Fatalf("environment variable MYSQL_HOST is not set")
}
dbUser := os.Getenv("DB_USR")
dbUser := os.Getenv("MYSQL_USR")
if len(dbUser) == 0 {
log.Fatalf("environment variable DB_USR is not set")
log.Fatalf("environment variable MYSQL_USR is not set")
}
dbPort := os.Getenv("DB_PORT")
if len(dbPort) == 0 {
log.Fatalf("environment variable DB_PORT is not set")
dbPortStr := os.Getenv("MYSQL_PORT")
if len(dbPortStr) == 0 {
log.Fatalf("environment variable MYSQL_PORT is not set")
}
dbPort, err := strconv.Atoi(dbPortStr)
if err != nil {
log.Fatalf("invalid MYSQL_PORT: %v", err)
}
dbPassword := os.Getenv("DB_PWD")
log.Infof("DB_HOST: %s, DB_USR: %s, DB_PORT: %s, DB_PWD: %s\n", dbHost, dbUser, dbPort, dbPassword)
dbPassword := os.Getenv("MYSQL_PWD")
dbDatabase := os.Getenv("MYSQL_DATABASE")
if len(dbDatabase) == 0 {
log.Fatalf("environment variable MYSQL_DATABASE is not set")
}
os.Setenv("MYSQL_HOST", dbHost)
os.Setenv("MYSQL_PORT", dbPort)
os.Setenv("MYSQL_USR", dbUser)
os.Setenv("MYSQL_PWD", dbPassword)
database := &models.Database{
Type: "mysql",
MySQL: &models.MySQL{
Host: dbHost,
Port: dbPort,
Username: dbUser,
Password: dbPassword,
Database: dbDatabase,
},
}
return testForAll(m)
log.Infof("MYSQL_HOST: %s, MYSQL_USR: %s, MYSQL_PORT: %s, MYSQL_PWD: %s\n", dbHost, dbUser, dbPort, dbPassword)
return testForAll(m, database)
}
func testForSQLite(m *testing.M) int {
db := os.Getenv("DATABASE")
defer os.Setenv("DATABASE", db)
os.Setenv("DATABASE", "sqlite")
file := os.Getenv("SQLITE_FILE")
if len(file) == 0 {
os.Setenv("SQLITE_FILE", "/registry.db")
defer os.Setenv("SQLITE_FILE", "")
log.Fatalf("environment variable SQLITE_FILE is not set")
}
return testForAll(m)
database := &models.Database{
Type: "sqlite",
SQLite: &models.SQLite{
File: file,
},
}
return testForAll(m, database)
}
func testForAll(m *testing.M) int {
os.Setenv("AUTH_MODE", "db_auth")
initDatabaseForTest()
func testForAll(m *testing.M, database *models.Database) int {
initDatabaseForTest(database)
clearUp(username)
return m.Run()
@ -210,8 +222,8 @@ func testForAll(m *testing.M) int {
var defaultRegistered = false
func initDatabaseForTest() {
database, err := getDatabase()
func initDatabaseForTest(db *models.Database) {
database, err := getDatabase(db)
if err != nil {
panic(err)
}
@ -226,6 +238,12 @@ func initDatabaseForTest() {
if err := database.Register(alias); err != nil {
panic(err)
}
if alias != "default" {
if err = globalOrm.Using(alias); err != nil {
log.Fatalf("failed to create new orm: %v", err)
}
}
}
func TestRegister(t *testing.T) {

View File

@ -16,15 +16,11 @@
package dao
import (
"errors"
"fmt"
"net"
"time"
"github.com/astaxie/beego/orm"
_ "github.com/go-sql-driver/mysql" //register mysql driver
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/common/utils"
)
type mysql struct {
@ -48,7 +44,8 @@ func NewMySQL(host, port, usr, pwd, database string) Database {
// Register registers MySQL as the underlying database used
func (m *mysql) Register(alias ...string) error {
if err := m.testConn(m.host, m.port); err != nil {
if err := utils.TestTCPConn(m.host+":"+m.port, 60, 2); err != nil {
return err
}
@ -65,30 +62,6 @@ func (m *mysql) Register(alias ...string) error {
return orm.RegisterDataBase(an, "mysql", conn)
}
func (m *mysql) testConn(host, port string) error {
ch := make(chan int, 1)
go func() {
var err error
var c net.Conn
for {
c, err = net.DialTimeout("tcp", host+":"+port, 20*time.Second)
if err == nil {
c.Close()
ch <- 1
} else {
log.Errorf("failed to connect to db, retry after 2 seconds :%v", err)
time.Sleep(2 * time.Second)
}
}
}()
select {
case <-ch:
return nil
case <-time.After(60 * time.Second):
return errors.New("failed to connect to database after 60 seconds")
}
}
// Name returns the name of MySQL
func (m *mysql) Name() string {
return "MySQL"

View File

@ -38,6 +38,11 @@ func TestDeleteUser(t *testing.T) {
if err != nil {
t.Fatalf("failed to register user: %v", err)
}
defer func(id int64) {
if err := deleteUser(id); err != nil {
t.Fatalf("failed to delete user %d: %v", id, err)
}
}(id)
err = DeleteUser(int(id))
if err != nil {
@ -67,3 +72,11 @@ func TestDeleteUser(t *testing.T) {
expected)
}
}
func deleteUser(id int64) error {
if _, err := GetOrmer().QueryTable(&models.User{}).
Filter("UserID", id).Delete(); err != nil {
return err
}
return nil
}

100
src/common/models/config.go Normal file
View File

@ -0,0 +1,100 @@
/*
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 models
/*
// Authentication ...
type Authentication struct {
Mode string `json:"mode"`
SelfRegistration bool `json:"self_registration"`
LDAP *LDAP `json:"ldap,omitempty"`
}
*/
// LDAP ...
type LDAP struct {
URL string `json:"url"`
SearchDN string `json:"search_dn"`
SearchPassword string `json:"search_password"`
BaseDN string `json:"base_dn"`
Filter string `json:"filter"`
UID string `json:"uid"`
Scope int `json:"scope"`
Timeout int `json:"timeout"` // in second
}
// Database ...
type Database struct {
Type string `json:"type"`
MySQL *MySQL `json:"mysql,omitempty"`
SQLite *SQLite `json:"sqlite,omitempty"`
}
// MySQL ...
type MySQL struct {
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password,omitempty"`
Database string `json:"database"`
}
// SQLite ...
type SQLite struct {
File string `json:"file"`
}
// Email ...
type Email struct {
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
SSL bool `json:"ssl"`
Identity string `json:"identity"`
From string `json:"from"`
}
/*
// Registry ...
type Registry struct {
URL string `json:"url"`
}
// TokenService ...
type TokenService struct {
URL string `json:"url"`
}
// SystemCfg holds all configurations of system
type SystemCfg struct {
DomainName string `json:"domain_name"` // Harbor external URL: protocal://host:port
Authentication *Authentication `json:"authentication"`
Database *Database `json:"database"`
TokenService *TokenService `json:"token_service"`
Registry *Registry `json:"registry"`
Email *Email `json:"email"`
VerifyRemoteCert bool `json:"verify_remote_cert"`
ProjectCreationRestriction string `json:"project_creation_restriction"`
MaxJobWorkers int `json:"max_job_workers"`
JobLogDir string `json:"job_log_dir"`
InitialAdminPwd string `json:"initial_admin_pwd,omitempty"`
CompressJS bool `json:"compress_js"` //TODO remove
TokenExpiration int `json:"token_expiration"` // in minute
SecretKey string `json:"secret_key,omitempty"`
CfgExpiration int `json:"cfg_expiration"`
}
*/

28
src/common/models/ldap.go Normal file
View File

@ -0,0 +1,28 @@
/*
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 models
// LdapConf holds information about repository that accessed most
type LdapConf struct {
LdapURL string `json:"ldap_url"`
LdapSearchDn string `json:"ldap_search_dn"`
LdapSearchPassword string `json:"ldap_search_password"`
LdapBaseDn string `json:"ldap_base_dn"`
LdapFilter string `json:"ldap_filter"`
LdapUID string `json:"ldap_uid"`
LdapScope int `json:"ldap_scope"`
LdapConnectionTimeout int `json:"ldap_connection_timeout"`
}

View File

@ -44,7 +44,7 @@ const (
//RepOpDelete represents the operation of a job to remove repository from a remote registry/harbor instance.
RepOpDelete string = "delete"
//UISecretCookie is the cookie name to contain the UI secret
UISecretCookie string = "uisecret"
UISecretCookie string = "secret"
)
// RepPolicy is the model for a replication policy, which associate to a project and a target (destination)

View File

@ -13,17 +13,20 @@
limitations under the License.
*/
package utils
package email
import (
"bytes"
"crypto/tls"
"strings"
"strconv"
//"strings"
"net/smtp"
"text/template"
"github.com/astaxie/beego"
//"github.com/astaxie/beego"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/ui/config"
)
// Mail holds information about content of Email
@ -34,24 +37,15 @@ type Mail struct {
Message string
}
// MailConfig holds information about Email configurations
type MailConfig struct {
Identity string
Host string
Port string
Username string
Password string
TLS bool
}
var mc MailConfig
var mc models.Email
// SendMail sends Email according to the configurations
func (m Mail) SendMail() error {
if mc.Host == "" {
loadConfig()
mc, err := config.Email()
if err != nil {
return err
}
mailTemplate, err := template.ParseFiles("views/mail.tpl")
if err != nil {
return err
@ -64,7 +58,7 @@ func (m Mail) SendMail() error {
content := mailContent.Bytes()
auth := smtp.PlainAuth(mc.Identity, mc.Username, mc.Password, mc.Host)
if mc.TLS {
if mc.SSL {
err = sendMailWithTLS(m, auth, content)
} else {
err = sendMail(m, auth, content)
@ -74,11 +68,11 @@ func (m Mail) SendMail() error {
}
func sendMail(m Mail, auth smtp.Auth, content []byte) error {
return smtp.SendMail(mc.Host+":"+mc.Port, auth, m.From, m.To, content)
return smtp.SendMail(mc.Host+":"+strconv.Itoa(mc.Port), auth, m.From, m.To, content)
}
func sendMailWithTLS(m Mail, auth smtp.Auth, content []byte) error {
conn, err := tls.Dial("tcp", mc.Host+":"+mc.Port, nil)
conn, err := tls.Dial("tcp", mc.Host+":"+strconv.Itoa(mc.Port), nil)
if err != nil {
return err
}
@ -123,6 +117,7 @@ func sendMailWithTLS(m Mail, auth smtp.Auth, content []byte) error {
return client.Quit()
}
/*
func loadConfig() {
config, err := beego.AppConfig.GetSection("mail")
if err != nil {
@ -142,3 +137,4 @@ func loadConfig() {
TLS: useTLS,
}
}
*/

View File

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

View File

@ -25,7 +25,7 @@ import (
"sync"
"time"
"github.com/vmware/harbor/src/common/config"
//"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"
@ -133,19 +133,24 @@ func (t *tokenAuthorizer) updateCachedToken(token string, expiresIn int) {
// Implements interface Authorizer
type standardTokenAuthorizer struct {
tokenAuthorizer
client *http.Client
credential Credential
client *http.Client
credential Credential
tokenServiceEndpoint string
}
// NewStandardTokenAuthorizer returns a standard token authorizer. The authorizer will request a token
// from token server and add it to the origin request
func NewStandardTokenAuthorizer(credential Credential, insecure bool, scopeType, scopeName string, scopeActions ...string) Authorizer {
// If tokenServiceEndpoint is set, the token request will be sent to it instead of the server get from authorizer
// The usage please refer to the function tokenURL
func NewStandardTokenAuthorizer(credential Credential, insecure bool,
tokenServiceEndpoint string, scopeType, scopeName string, scopeActions ...string) Authorizer {
authorizer := &standardTokenAuthorizer{
client: &http.Client{
Transport: registry.GetHTTPTransport(insecure),
Timeout: 30 * time.Second,
},
credential: credential,
credential: credential,
tokenServiceEndpoint: tokenServiceEndpoint,
}
if len(scopeType) != 0 || len(scopeName) != 0 {
@ -162,7 +167,7 @@ func NewStandardTokenAuthorizer(credential Credential, insecure bool, scopeType,
}
func (s *standardTokenAuthorizer) generateToken(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error) {
realm = tokenURL(realm)
realm = s.tokenURL(realm)
u, err := url.Parse(realm)
if err != nil {
@ -229,16 +234,14 @@ func (s *standardTokenAuthorizer) generateToken(realm, service string, scopes []
// when the registry client is used inside Harbor, the token request
// can be posted to token service directly rather than going through nginx.
// this solution can resolve two problems:
// If realm is set as the internal url of token service, this can resolve
// two problems:
// 1. performance issue
// 2. the realm field returned by registry is an IP which can not reachable
// inside Harbor
func tokenURL(realm string) string {
extEndpoint := config.ExtEndpoint()
tokenEndpoint := config.TokenEndpoint()
if len(extEndpoint) != 0 && len(tokenEndpoint) != 0 &&
strings.Contains(realm, extEndpoint) {
realm = strings.TrimRight(tokenEndpoint, "/") + "/service/token"
func (s *standardTokenAuthorizer) tokenURL(realm string) string {
if len(s.tokenServiceEndpoint) != 0 {
return s.tokenServiceEndpoint
}
return realm
}

View File

@ -40,7 +40,7 @@ func TestAuthorizeOfStandardTokenAuthorizer(t *testing.T) {
})
defer server.Close()
authorizer := NewStandardTokenAuthorizer(nil, false, "repository", "library/ubuntu", "pull")
authorizer := NewStandardTokenAuthorizer(nil, false, "", "repository", "library/ubuntu", "pull")
req, err := http.NewRequest("GET", "http://registry", nil)
if err != nil {
t.Fatalf("failed to create request: %v", err)

View File

@ -0,0 +1,96 @@
/*
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 test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"github.com/vmware/harbor/src/common/config"
)
var adminServerDefaultConfig = map[string]interface{}{
config.ExtEndpoint: "host01.com",
config.AUTHMode: config.DBAuth,
config.DatabaseType: "mysql",
config.MySQLHost: "127.0.0.1",
config.MySQLPort: 3306,
config.MySQLUsername: "user01",
config.MySQLPassword: "password",
config.MySQLDatabase: "registry",
config.SQLiteFile: "/tmp/registry.db",
config.SelfRegistration: true,
config.LDAPURL: "ldap://127.0.0.1",
config.LDAPSearchDN: "uid=searchuser,ou=people,dc=mydomain,dc=com",
config.LDAPSearchPwd: "password",
config.LDAPBaseDN: "ou=people,dc=mydomain,dc=com",
config.LDAPUID: "uid",
config.LDAPFilter: "",
config.LDAPScope: 3,
config.LDAPTimeout: 30,
config.TokenServiceURL: "http://token_service",
config.RegistryURL: "http://registry",
config.EmailHost: "127.0.0.1",
config.EmailPort: 25,
config.EmailUsername: "user01",
config.EmailPassword: "password",
config.EmailFrom: "from",
config.EmailSSL: true,
config.EmailIdentity: "",
config.ProjectCreationRestriction: config.ProCrtRestrAdmOnly,
config.VerifyRemoteCert: false,
config.MaxJobWorkers: 3,
config.TokenExpiration: 30,
config.CfgExpiration: 5,
config.JobLogDir: "/var/log/jobs",
config.UseCompressedJS: true,
config.AdminInitialPassword: "password",
}
// NewAdminserver returns a mock admin server
func NewAdminserver(config map[string]interface{}) (*httptest.Server, error) {
m := []*RequestHandlerMapping{}
if config == nil {
config = adminServerDefaultConfig
}
b, err := json.Marshal(config)
if err != nil {
return nil, err
}
resp := &Response{
StatusCode: http.StatusOK,
Body: b,
}
m = append(m, &RequestHandlerMapping{
Method: "GET",
Pattern: "/api/configurations",
Handler: Handler(resp),
})
m = append(m, &RequestHandlerMapping{
Method: "PUT",
Pattern: "/api/configurations",
Handler: Handler(&Response{
StatusCode: http.StatusOK,
}),
})
return NewServer(m...), nil
}

View File

@ -0,0 +1,41 @@
/*
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 test
import (
"crypto/aes"
"crypto/rand"
"fmt"
"io/ioutil"
)
// GenerateKey generates aes key
func GenerateKey(path string) (string, error) {
data := make([]byte, aes.BlockSize)
n, err := rand.Read(data)
if err != nil {
return "", fmt.Errorf("failed to generate random bytes: %v", err)
}
if n != aes.BlockSize {
return "", fmt.Errorf("the length of random bytes %d != %d", n, aes.BlockSize)
}
if err = ioutil.WriteFile(path, data, 0777); err != nil {
return "", fmt.Errorf("failed write secret key to file %s: %v", path, err)
}
return string(data), nil
}

View File

@ -21,6 +21,8 @@ import (
"net/http"
"net/http/httptest"
"strings"
"github.com/gorilla/mux"
)
// RequestHandlerMapping is a mapping between request and its handler
@ -78,11 +80,11 @@ func Handler(resp *Response) func(http.ResponseWriter, *http.Request) {
// NewServer creates a HTTP server for unit test
func NewServer(mappings ...*RequestHandlerMapping) *httptest.Server {
mux := http.NewServeMux()
r := mux.NewRouter()
for _, mapping := range mappings {
mux.Handle(mapping.Pattern, mapping)
r.PathPrefix(mapping.Pattern).Handler(mapping).Methods(mapping.Method)
}
return httptest.NewServer(mux)
return httptest.NewServer(r)
}

View File

@ -16,10 +16,14 @@
package utils
import (
"fmt"
"math/rand"
"net"
"net/url"
"strings"
"time"
"github.com/vmware/harbor/src/common/utils/log"
)
// FormatEndpoint formats endpoint
@ -70,3 +74,40 @@ func GenerateRandomString() string {
}
return string(result)
}
// TestTCPConn tests TCP connection
// timeout: the total time before returning if something is wrong
// with the connection, in second
// interval: the interval time for retring after failure, in second
func TestTCPConn(addr string, timeout, interval int) error {
success := make(chan int)
cancel := make(chan int)
go func() {
for {
select {
case <-cancel:
break
default:
conn, err := net.DialTimeout("tcp", addr, time.Duration(timeout)*time.Second)
if err != nil {
log.Errorf("failed to connect to tcp://%s, retry after %d seconds :%v",
addr, interval, err)
time.Sleep(time.Duration(interval) * time.Second)
continue
}
conn.Close()
success <- 1
break
}
}
}()
select {
case <-success:
return nil
case <-time.After(time.Duration(timeout) * time.Second):
cancel <- 1
return fmt.Errorf("failed to connect to tcp:%s after %d seconds", addr, timeout)
}
}

View File

@ -17,6 +17,7 @@ package utils
import (
"encoding/base64"
"net/http/httptest"
"strings"
"testing"
)
@ -178,3 +179,12 @@ func TestParseLink(t *testing.T) {
t.Errorf("unexpected prev: %s != %s", links.Next(), next)
}
}
func TestTestTCPConn(t *testing.T) {
server := httptest.NewServer(nil)
defer server.Close()
addr := strings.TrimLeft(server.URL, "http://")
if err := TestTCPConn(addr, 60, 2); err != nil {
t.Fatalf("failed to test tcp connection of %s: %v", addr, err)
}
}

View File

@ -25,12 +25,12 @@ import (
"github.com/vmware/harbor/src/common/api"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/jobservice/job"
"github.com/vmware/harbor/src/jobservice/config"
"github.com/vmware/harbor/src/jobservice/utils"
"github.com/vmware/harbor/src/common/models"
u "github.com/vmware/harbor/src/common/utils"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/jobservice/config"
"github.com/vmware/harbor/src/jobservice/job"
"github.com/vmware/harbor/src/jobservice/utils"
)
// ReplicationJob handles /api/replicationJobs /api/replicationJobs/:id/log
@ -171,7 +171,13 @@ func (rj *ReplicationJob) GetLog() {
rj.RenderError(http.StatusBadRequest, "Invalid job id")
return
}
logFile := utils.GetJobLogPath(jid)
logFile, err := utils.GetJobLogPath(jid)
if err != nil {
log.Errorf("failed to get log path of job %s: %v", idStr, err)
rj.RenderError(http.StatusInternalServerError,
http.StatusText(http.StatusInternalServerError))
return
}
rj.Ctx.Output.Download(logFile)
}
@ -188,7 +194,7 @@ func getRepoList(projectID int64) ([]string, error) {
return repositories, err
}
req.AddCookie(&http.Cookie{Name: models.UISecretCookie, Value: config.UISecret()})
req.AddCookie(&http.Cookie{Name: models.UISecretCookie, Value: config.JobserviceSecret()})
resp, err := client.Do(req)
if err != nil {

View File

@ -16,121 +16,144 @@
package config
import (
"fmt"
"os"
"strconv"
"github.com/astaxie/beego"
comcfg "github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
)
const defaultMaxWorkers int = 10
const (
defaultKeyPath string = "/etc/jobservice/key"
)
var maxJobWorkers int
var localUIURL string
var localRegURL string
var logDir string
var uiSecret string
var secretKey string
var verifyRemoteCert string
var (
mg *comcfg.Manager
keyProvider comcfg.KeyProvider
)
func init() {
maxWorkersEnv := os.Getenv("MAX_JOB_WORKERS")
maxWorkers64, err := strconv.ParseInt(maxWorkersEnv, 10, 32)
maxJobWorkers = int(maxWorkers64)
// Init configurations
func Init() error {
//init key provider
initKeyProvider()
adminServerURL := os.Getenv("ADMIN_SERVER_URL")
if len(adminServerURL) == 0 {
adminServerURL = "http://adminserver"
}
mg = comcfg.NewManager(adminServerURL, JobserviceSecret(), true)
if err := mg.Init(); err != nil {
return err
}
if _, err := mg.Load(); err != nil {
return err
}
return nil
}
func initKeyProvider() {
path := os.Getenv("KEY_PATH")
if len(path) == 0 {
path = defaultKeyPath
}
log.Infof("key path: %s", path)
keyProvider = comcfg.NewFileKeyProvider(path)
}
// VerifyRemoteCert returns bool value.
func VerifyRemoteCert() (bool, error) {
cfg, err := mg.Get()
if err != nil {
log.Warningf("Failed to parse max works setting, error: %v, the default value: %d will be used", err, defaultMaxWorkers)
maxJobWorkers = defaultMaxWorkers
return true, err
}
return cfg[comcfg.VerifyRemoteCert].(bool), nil
}
localRegURL = os.Getenv("REGISTRY_URL")
if len(localRegURL) == 0 {
localRegURL = "http://registry:5000"
}
localUIURL = os.Getenv("UI_URL")
if len(localUIURL) == 0 {
localUIURL = "http://ui"
}
logDir = os.Getenv("LOG_DIR")
if len(logDir) == 0 {
logDir = "/var/log"
}
f, err := os.Open(logDir)
defer f.Close()
// Database ...
func Database() (*models.Database, error) {
cfg, err := mg.Get()
if err != nil {
panic(err)
}
finfo, err := f.Stat()
if err != nil {
panic(err)
}
if !finfo.IsDir() {
panic(fmt.Sprintf("%s is not a direcotry", logDir))
return nil, err
}
database := &models.Database{}
database.Type = cfg[comcfg.DatabaseType].(string)
mysql := &models.MySQL{}
mysql.Host = cfg[comcfg.MySQLHost].(string)
mysql.Port = int(cfg[comcfg.MySQLPort].(float64))
mysql.Username = cfg[comcfg.MySQLUsername].(string)
mysql.Password = cfg[comcfg.MySQLPassword].(string)
mysql.Database = cfg[comcfg.MySQLDatabase].(string)
database.MySQL = mysql
sqlite := &models.SQLite{}
sqlite.File = cfg[comcfg.SQLiteFile].(string)
database.SQLite = sqlite
uiSecret = os.Getenv("UI_SECRET")
if len(uiSecret) == 0 {
panic("UI Secret is not set")
}
verifyRemoteCert = os.Getenv("VERIFY_REMOTE_CERT")
if len(verifyRemoteCert) == 0 {
verifyRemoteCert = "on"
}
configPath := os.Getenv("CONFIG_PATH")
if len(configPath) != 0 {
log.Infof("Config path: %s", configPath)
beego.LoadAppConfig("ini", configPath)
}
secretKey = os.Getenv("SECRET_KEY")
if len(secretKey) != 16 {
panic("The length of secretkey has to be 16 characters!")
}
log.Debugf("config: maxJobWorkers: %d", maxJobWorkers)
log.Debugf("config: localUIURL: %s", localUIURL)
log.Debugf("config: localRegURL: %s", localRegURL)
log.Debugf("config: verifyRemoteCert: %s", verifyRemoteCert)
log.Debugf("config: logDir: %s", logDir)
log.Debugf("config: uiSecret: ******")
return database, nil
}
// MaxJobWorkers ...
func MaxJobWorkers() int {
return maxJobWorkers
func MaxJobWorkers() (int, error) {
cfg, err := mg.Get()
if err != nil {
return 0, err
}
return int(cfg[comcfg.MaxJobWorkers].(float64)), nil
}
// LocalUIURL returns the local ui url, job service will use this URL to call API hosted on ui process
func LocalUIURL() string {
return localUIURL
return "http://ui"
}
// LocalRegURL returns the local registry url, job service will use this URL to pull image from the registry
func LocalRegURL() string {
return localRegURL
func LocalRegURL() (string, error) {
cfg, err := mg.Get()
if err != nil {
return "", err
}
return cfg[comcfg.RegistryURL].(string), nil
}
// LogDir returns the absolute path to which the log file will be written
func LogDir() string {
return logDir
}
// UISecret will return the value of secret cookie for jobsevice to call UI API.
func UISecret() string {
return uiSecret
func LogDir() (string, error) {
cfg, err := mg.Get()
if err != nil {
return "", err
}
return cfg[comcfg.JobLogDir].(string), nil
}
// SecretKey will return the secret key for encryption/decryption password in target.
func SecretKey() string {
return secretKey
func SecretKey() (string, error) {
return keyProvider.Get(nil)
}
// VerifyRemoteCert return the flag to tell jobservice whether or not verify the cert of remote registry
func VerifyRemoteCert() bool {
return verifyRemoteCert != "off"
// UISecret returns a secret to mark UI when communicate with other
// component
func UISecret() string {
return os.Getenv("UI_SECRET")
}
// JobserviceSecret returns a secret to mark Jobservice when communicate with
// other component
func JobserviceSecret() string {
return os.Getenv("JOBSERVICE_SECRET")
}
// ExtEndpoint ...
func ExtEndpoint() (string, error) {
cfg, err := mg.Get()
if err != nil {
return "", err
}
return cfg[comcfg.ExtEndpoint].(string), nil
}
// InternalTokenServiceEndpoint ...
func InternalTokenServiceEndpoint() string {
return "http://ui/service/token"
}

View File

@ -1,9 +1,84 @@
/*
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"
"github.com/vmware/harbor/src/common/utils/test"
)
func TestMain(t *testing.T) {
}
// test functions under package jobservice/config
func TestConfig(t *testing.T) {
server, err := test.NewAdminserver(nil)
if err != nil {
t.Fatalf("failed to create a mock admin server: %v", err)
}
defer server.Close()
if err := os.Setenv("ADMIN_SERVER_URL", server.URL); err != nil {
t.Fatalf("failed to set env %s: %v", "ADMIN_SERVER_URL", err)
}
secretKeyPath := "/tmp/secretkey"
_, err = test.GenerateKey(secretKeyPath)
if err != nil {
t.Errorf("failed to generate secret key: %v", err)
return
}
defer os.Remove(secretKeyPath)
if err := os.Setenv("KEY_PATH", secretKeyPath); err != nil {
t.Fatalf("failed to set env %s: %v", "KEY_PATH", err)
}
if err := Init(); err != nil {
t.Fatalf("failed to initialize configurations: %v", err)
}
if _, err := VerifyRemoteCert(); err != nil {
t.Fatalf("failed to get verify remote cert: %v", err)
}
if _, err := Database(); err != nil {
t.Fatalf("failed to get database settings: %v", err)
}
if _, err := MaxJobWorkers(); err != nil {
t.Fatalf("failed to get max job workers: %v", err)
}
if _, err := LocalRegURL(); err != nil {
t.Fatalf("failed to get registry URL: %v", err)
}
if _, err := LogDir(); err != nil {
t.Fatalf("failed to get log directory: %v", err)
}
if _, err := SecretKey(); err != nil {
t.Fatalf("failed to get secret key: %v", err)
}
if len(InternalTokenServiceEndpoint()) == 0 {
t.Error("the internal token service endpoint is null")
}
if _, err := ExtEndpoint(); err != nil {
t.Fatalf("failed to get ext endpoint: %v", err)
}
}

View File

@ -20,12 +20,12 @@ import (
"sync"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/jobservice/config"
"github.com/vmware/harbor/src/jobservice/replication"
"github.com/vmware/harbor/src/jobservice/utils"
"github.com/vmware/harbor/src/common/models"
uti "github.com/vmware/harbor/src/common/utils"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/jobservice/config"
"github.com/vmware/harbor/src/jobservice/replication"
"github.com/vmware/harbor/src/jobservice/utils"
)
// RepJobParm wraps the parm of a job
@ -184,14 +184,17 @@ func (sm *SM) Init() {
}
// Reset resets the state machine so it will start handling another job.
func (sm *SM) Reset(jid int64) error {
func (sm *SM) Reset(jid int64) (err error) {
//To ensure the new jobID is visible to the thread to stop the SM
sm.lock.Lock()
sm.JobID = jid
sm.desiredState = ""
sm.lock.Unlock()
sm.Logger = utils.NewLogger(sm.JobID)
sm.Logger, err = utils.NewLogger(sm.JobID)
if err != nil {
return
}
//init parms
job, err := dao.GetRepJob(sm.JobID)
if err != nil {
@ -207,13 +210,22 @@ func (sm *SM) Reset(jid int64) error {
if policy == nil {
return fmt.Errorf("The policy doesn't exist in DB, policy id:%d", job.PolicyID)
}
regURL, err := config.LocalRegURL()
if err != nil {
return err
}
verify, err := config.VerifyRemoteCert()
if err != nil {
return err
}
sm.Parms = &RepJobParm{
LocalRegURL: config.LocalRegURL(),
LocalRegURL: regURL,
Repository: job.Repository,
Tags: job.TagList,
Enabled: policy.Enabled,
Operation: job.Operation,
Insecure: !config.VerifyRemoteCert(),
Insecure: !verify,
}
if policy.Enabled == 0 {
//worker will cancel this job
@ -231,7 +243,11 @@ func (sm *SM) Reset(jid int64) error {
pwd := target.Password
if len(pwd) != 0 {
pwd, err = uti.ReversibleDecrypt(pwd, config.SecretKey())
key, err := config.SecretKey()
if err != nil {
return err
}
pwd, err = uti.ReversibleDecrypt(pwd, key)
if err != nil {
return fmt.Errorf("failed to decrypt password: %v", err)
}
@ -269,7 +285,7 @@ func addTestTransition(sm *SM) error {
}
func addImgTransferTransition(sm *SM) {
base := replication.InitBaseHandler(sm.Parms.Repository, sm.Parms.LocalRegURL, config.UISecret(),
base := replication.InitBaseHandler(sm.Parms.Repository, sm.Parms.LocalRegURL, config.JobserviceSecret(),
sm.Parms.TargetURL, sm.Parms.TargetUsername, sm.Parms.TargetPassword,
sm.Parms.Insecure, sm.Parms.Tags, sm.Logger)

View File

@ -17,9 +17,9 @@ package job
import (
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/jobservice/config"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/jobservice/config"
)
type workerPool struct {
@ -111,17 +111,22 @@ func NewWorker(id int) *Worker {
}
// InitWorkerPool create workers according to configuration.
func InitWorkerPool() {
WorkerPool = &workerPool{
workerChan: make(chan *Worker, config.MaxJobWorkers()),
workerList: make([]*Worker, 0, config.MaxJobWorkers()),
func InitWorkerPool() error {
n, err := config.MaxJobWorkers()
if err != nil {
return err
}
for i := 0; i < config.MaxJobWorkers(); i++ {
WorkerPool = &workerPool{
workerChan: make(chan *Worker, n),
workerList: make([]*Worker, 0, n),
}
for i := 0; i < n; i++ {
worker := NewWorker(i)
WorkerPool.workerList = append(WorkerPool.workerList, worker)
worker.Start()
log.Debugf("worker %d started", worker.ID)
}
return nil
}
// Dispatch will listen to the jobQueue of job service and try to pick a free worker from the worker pool and assign the job to it.

View File

@ -16,15 +16,32 @@
package main
import (
"os"
"github.com/astaxie/beego"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/jobservice/job"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/jobservice/config"
"github.com/vmware/harbor/src/jobservice/job"
)
func main() {
dao.InitDatabase()
log.Info("initializing configurations...")
if err := config.Init(); err != nil {
log.Fatalf("failed to initialize configurations: %v", err)
}
log.Info("configurations initialization completed")
database, err := config.Database()
if err != nil {
log.Fatalf("failed to get database configurations: %v", err)
}
if err := dao.InitDatabase(database); err != nil {
log.Fatalf("failed to initialize database: %v", err)
}
initRouters()
job.InitWorkerPool()
go job.Dispatch()
@ -48,3 +65,11 @@ func resumeJobs() {
log.Warningf("Failed to jobs to resume, error: %v", err)
}
}
func init() {
configPath := os.Getenv("CONFIG_PATH")
if len(configPath) != 0 {
log.Infof("Config path: %s", configPath)
beego.LoadAppConfig("ini", configPath)
}
}

View File

@ -33,6 +33,7 @@ import (
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/common/utils/registry"
"github.com/vmware/harbor/src/common/utils/registry/auth"
"github.com/vmware/harbor/src/jobservice/config"
)
const (
@ -137,7 +138,7 @@ func (i *Initializer) enter() (string, error) {
c := &http.Cookie{Name: models.UISecretCookie, Value: i.srcSecret}
srcCred := auth.NewCookieCredential(c)
srcClient, err := newRepositoryClient(i.srcURL, i.insecure, srcCred,
i.repository, "repository", i.repository, "pull", "push", "*")
config.InternalTokenServiceEndpoint(), i.repository, "repository", i.repository, "pull", "push", "*")
if err != nil {
i.logger.Errorf("an error occurred while creating source repository client: %v", err)
return "", err
@ -146,7 +147,7 @@ func (i *Initializer) enter() (string, error) {
dstCred := auth.NewBasicAuthCredential(i.dstUsr, i.dstPwd)
dstClient, err := newRepositoryClient(i.dstURL, i.insecure, dstCred,
i.repository, "repository", i.repository, "pull", "push", "*")
"", i.repository, "repository", i.repository, "pull", "push", "*")
if err != nil {
i.logger.Errorf("an error occurred while creating destination repository client: %v", err)
return "", err
@ -457,10 +458,11 @@ func (m *ManifestPusher) enter() (string, error) {
return StatePullManifest, nil
}
func newRepositoryClient(endpoint string, insecure bool, credential auth.Credential, repository, scopeType, scopeName string,
func newRepositoryClient(endpoint string, insecure bool, credential auth.Credential,
tokenServiceEndpoint, repository, scopeType, scopeName string,
scopeActions ...string) (*registry.Repository, error) {
authorizer := auth.NewStandardTokenAuthorizer(credential, insecure, scopeType, scopeName, scopeActions...)
authorizer := auth.NewStandardTokenAuthorizer(credential, insecure,
tokenServiceEndpoint, scopeType, scopeName, scopeActions...)
store, err := auth.NewAuthorizerStore(endpoint, insecure, authorizer)
if err != nil {

View File

@ -18,16 +18,20 @@ package utils
import (
"fmt"
"github.com/vmware/harbor/src/jobservice/config"
"github.com/vmware/harbor/src/common/utils/log"
"os"
"path/filepath"
"strconv"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/jobservice/config"
)
// NewLogger create a logger for a speicified job
func NewLogger(jobID int64) *log.Logger {
logFile := GetJobLogPath(jobID)
func NewLogger(jobID int64) (*log.Logger, error) {
logFile, err := GetJobLogPath(jobID)
if err != nil {
return nil, err
}
d := filepath.Dir(logFile)
if _, err := os.Stat(d); os.IsNotExist(err) {
err := os.MkdirAll(d, 0660)
@ -40,11 +44,11 @@ func NewLogger(jobID int64) *log.Logger {
log.Errorf("Failed to open log file %s, the log of job %d will be printed to standard output, the error: %v", logFile, jobID, err)
f = os.Stdout
}
return log.New(f, log.NewTextFormatter(), log.InfoLevel)
return log.New(f, log.NewTextFormatter(), log.InfoLevel), nil
}
// GetJobLogPath returns the absolute path in which the job log file is located.
func GetJobLogPath(jobID int64) string {
func GetJobLogPath(jobID int64) (string, error) {
f := fmt.Sprintf("job_%d.log", jobID)
k := jobID / 1000
p := ""
@ -61,6 +65,10 @@ func GetJobLogPath(jobID int64) string {
p = filepath.Join(d, p)
}
p = filepath.Join(config.LogDir(), p, f)
return p
base, err := config.LogDir()
if err != nil {
return "", err
}
p = filepath.Join(base, p, f)
return p, nil
}

365
src/ui/api/config.go Normal file
View File

@ -0,0 +1,365 @@
/*
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 (
"fmt"
"net/http"
"strconv"
"github.com/vmware/harbor/src/common/api"
comcfg "github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/config"
)
var (
// valid keys of configurations which user can modify
validKeys = []string{
comcfg.ExtEndpoint,
comcfg.AUTHMode,
comcfg.DatabaseType,
comcfg.MySQLHost,
comcfg.MySQLPort,
comcfg.MySQLUsername,
comcfg.MySQLPassword,
comcfg.MySQLDatabase,
comcfg.SQLiteFile,
comcfg.SelfRegistration,
comcfg.LDAPURL,
comcfg.LDAPSearchDN,
comcfg.LDAPSearchPwd,
comcfg.LDAPBaseDN,
comcfg.LDAPUID,
comcfg.LDAPFilter,
comcfg.LDAPScope,
comcfg.LDAPTimeout,
comcfg.TokenServiceURL,
comcfg.RegistryURL,
comcfg.EmailHost,
comcfg.EmailPort,
comcfg.EmailUsername,
comcfg.EmailPassword,
comcfg.EmailFrom,
comcfg.EmailSSL,
comcfg.EmailIdentity,
comcfg.ProjectCreationRestriction,
comcfg.VerifyRemoteCert,
comcfg.MaxJobWorkers,
comcfg.TokenExpiration,
comcfg.CfgExpiration,
comcfg.JobLogDir,
comcfg.UseCompressedJS,
comcfg.AdminInitialPassword,
}
numKeys = []string{
comcfg.EmailPort,
comcfg.LDAPScope,
comcfg.LDAPTimeout,
comcfg.MySQLPort,
comcfg.MaxJobWorkers,
comcfg.TokenExpiration,
comcfg.CfgExpiration,
}
boolKeys = []string{
comcfg.EmailSSL,
comcfg.SelfRegistration,
comcfg.VerifyRemoteCert,
comcfg.UseCompressedJS,
}
passwordKeys = []string{
comcfg.AdminInitialPassword,
comcfg.EmailPassword,
comcfg.LDAPSearchPwd,
comcfg.MySQLPassword,
}
)
// ConfigAPI ...
type ConfigAPI struct {
api.BaseAPI
}
// Prepare validates the user
func (c *ConfigAPI) Prepare() {
userID := c.ValidateUser()
isSysAdmin, err := dao.IsAdminRole(userID)
if err != nil {
log.Errorf("failed to check the role of user: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if !isSysAdmin {
c.CustomAbort(http.StatusForbidden, http.StatusText(http.StatusForbidden))
}
}
type value struct {
Value interface{} `json:"value"`
Editable bool `json:"editable"`
}
// Get returns configurations
func (c *ConfigAPI) Get() {
cfg, err := config.GetSystemCfg()
if err != nil {
log.Errorf("failed to get configurations: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
m, err := convertForGet(cfg)
if err != nil {
log.Errorf("failed to convert configurations: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
c.Data["json"] = m
c.ServeJSON()
}
// Put updates configurations
func (c *ConfigAPI) Put() {
m := map[string]string{}
c.DecodeJSONReq(&m)
cfg := map[string]string{}
for _, k := range validKeys {
if v, ok := m[k]; ok {
cfg[k] = v
}
}
isSysErr, err := validateCfg(cfg)
if err != nil {
if isSysErr {
log.Errorf("failed to validate configurations: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
c.CustomAbort(http.StatusBadRequest, err.Error())
}
if value, ok := cfg[comcfg.AUTHMode]; ok {
mode, err := config.AuthMode()
if err != nil {
log.Errorf("failed to get auth mode: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if mode != value {
flag, err := authModeCanBeModified()
if err != nil {
log.Errorf("failed to determine whether auth mode can be modified: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if !flag {
c.CustomAbort(http.StatusBadRequest,
fmt.Sprintf("%s can not be modified as new users have been inserted into database",
comcfg.AUTHMode))
}
}
}
result, err := convertForPut(cfg)
if err != nil {
log.Errorf("failed to convert configurations: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if err := config.Upload(result); err != nil {
log.Errorf("failed to upload configurations: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if err := config.Load(); err != nil {
log.Errorf("failed to load configurations: %v", err)
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
}
func validateCfg(c map[string]string) (bool, error) {
isSysErr := false
mode, err := config.AuthMode()
if err != nil {
isSysErr = true
return isSysErr, err
}
if value, ok := c[comcfg.AUTHMode]; ok {
if value != comcfg.DBAuth && value != comcfg.LDAPAuth {
return isSysErr, fmt.Errorf("invalid %s, shoud be %s or %s", comcfg.AUTHMode, comcfg.DBAuth, comcfg.LDAPAuth)
}
mode = value
}
if mode == comcfg.LDAPAuth {
ldap, err := config.LDAP()
if err != nil {
isSysErr = true
return isSysErr, err
}
if len(ldap.URL) == 0 {
if _, ok := c[comcfg.LDAPURL]; !ok {
return isSysErr, fmt.Errorf("%s is missing", comcfg.LDAPURL)
}
}
if len(ldap.BaseDN) == 0 {
if _, ok := c[comcfg.LDAPBaseDN]; !ok {
return isSysErr, fmt.Errorf("%s is missing", comcfg.LDAPBaseDN)
}
}
if len(ldap.UID) == 0 {
if _, ok := c[comcfg.LDAPUID]; !ok {
return isSysErr, fmt.Errorf("%s is missing", comcfg.LDAPUID)
}
}
if ldap.Scope == 0 {
if _, ok := c[comcfg.LDAPScope]; !ok {
return isSysErr, fmt.Errorf("%s is missing", comcfg.LDAPScope)
}
}
}
if ldapURL, ok := c[comcfg.LDAPURL]; ok && len(ldapURL) == 0 {
return isSysErr, fmt.Errorf("%s is empty", comcfg.LDAPURL)
}
if baseDN, ok := c[comcfg.LDAPBaseDN]; ok && len(baseDN) == 0 {
return isSysErr, fmt.Errorf("%s is empty", comcfg.LDAPBaseDN)
}
if uID, ok := c[comcfg.LDAPUID]; ok && len(uID) == 0 {
return isSysErr, fmt.Errorf("%s is empty", comcfg.LDAPUID)
}
if scope, ok := c[comcfg.LDAPScope]; ok &&
scope != comcfg.LDAPScopeBase &&
scope != comcfg.LDAPScopeOnelevel &&
scope != comcfg.LDAPScopeSubtree {
return isSysErr, fmt.Errorf("invalid %s, should be %s, %s or %s",
comcfg.LDAPScope,
comcfg.LDAPScopeBase,
comcfg.LDAPScopeOnelevel,
comcfg.LDAPScopeSubtree)
}
for _, k := range boolKeys {
v, ok := c[k]
if !ok {
continue
}
if v != "0" && v != "1" {
return isSysErr, fmt.Errorf("%s should be %s or %s",
k, "0", "1")
}
}
for _, k := range numKeys {
v, ok := c[k]
if !ok {
continue
}
n, err := strconv.Atoi(v)
if err != nil || n < 0 {
return isSysErr, fmt.Errorf("invalid %s: %s", k, v)
}
if (k == comcfg.EmailPort ||
k == comcfg.MySQLPort) && n > 65535 {
return isSysErr, fmt.Errorf("invalid %s: %s", k, v)
}
}
if crt, ok := c[comcfg.ProjectCreationRestriction]; ok &&
crt != comcfg.ProCrtRestrEveryone &&
crt != comcfg.ProCrtRestrAdmOnly {
return isSysErr, fmt.Errorf("invalid %s, should be %s or %s",
comcfg.ProjectCreationRestriction,
comcfg.ProCrtRestrAdmOnly,
comcfg.ProCrtRestrEveryone)
}
return isSysErr, nil
}
//convert map[string]string to map[string]interface{}
func convertForPut(m map[string]string) (map[string]interface{}, error) {
cfg := map[string]interface{}{}
for k, v := range m {
cfg[k] = v
}
for _, k := range numKeys {
if _, ok := cfg[k]; !ok {
continue
}
v, err := strconv.Atoi(cfg[k].(string))
if err != nil {
return nil, err
}
cfg[k] = v
}
for _, k := range boolKeys {
if _, ok := cfg[k]; !ok {
continue
}
cfg[k] = cfg[k] == "1"
}
return cfg, nil
}
// delete sensitive attrs and add editable field to every attr
func convertForGet(cfg map[string]interface{}) (map[string]*value, error) {
result := map[string]*value{}
for _, k := range passwordKeys {
if _, ok := cfg[k]; ok {
delete(cfg, k)
}
}
for k, v := range cfg {
result[k] = &value{
Value: v,
Editable: true,
}
}
flag, err := authModeCanBeModified()
if err != nil {
return nil, err
}
result[comcfg.AUTHMode].Editable = flag
return result, nil
}
func authModeCanBeModified() (bool, error) {
return dao.AuthModeCanBeModified()
}

70
src/ui/api/config_test.go Normal file
View File

@ -0,0 +1,70 @@
/*
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 (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/harbor/src/common/config"
)
func TestGetConfig(t *testing.T) {
fmt.Println("Testing getting configurations")
assert := assert.New(t)
apiTest := newHarborAPI()
//case 1: get configurations without admin role
code, _, err := apiTest.GetConfig(*testUser)
if err != nil {
t.Fatalf("failed to get configurations: %v", err)
}
assert.Equal(401, code, "the status code of getting configurations with non-admin user should be 401")
//case 2: get configurations with admin role
code, cfg, err := apiTest.GetConfig(*admin)
if err != nil {
t.Fatalf("failed to get configurations: %v", err)
}
if !assert.Equal(200, code, "the status code of getting configurations with admin user should be 200") {
return
}
mode := cfg[config.AUTHMode].Value.(string)
assert.Equal(config.DBAuth, mode, fmt.Sprintf("the auth mode should be %s", config.DBAuth))
}
func TestPutConfig(t *testing.T) {
fmt.Println("Testing modifying configurations")
assert := assert.New(t)
apiTest := newHarborAPI()
cfg := map[string]string{
config.VerifyRemoteCert: "0",
}
code, err := apiTest.PutConfig(*admin, cfg)
if err != nil {
t.Fatalf("failed to get configurations: %v", err)
}
if !assert.Equal(200, code, "the status code of modifying configurations with admin user should be 200") {
return
}
}

View File

@ -14,6 +14,7 @@ import (
"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/ui/config"
"github.com/vmware/harbor/tests/apitests/apilib"
// "strconv"
// "strings"
@ -57,7 +58,14 @@ type usrInfo struct {
}
func init() {
dao.InitDatabase()
if err := config.Init(); err != nil {
log.Fatalf("failed to initialize configurations: %v", err)
}
database, err := config.Database()
if err != nil {
log.Fatalf("failed to get database configurations: %v", err)
}
dao.InitDatabase(database)
_, file, _, _ := runtime.Caller(1)
apppath, _ := filepath.Abs(filepath.Dir(filepath.Join(file, ".."+string(filepath.Separator))))
beego.BConfig.WebConfig.Session.SessionOn = true
@ -90,6 +98,8 @@ func init() {
beego.Router("/api/policies/replication/:id([0-9]+)/enablement", &RepPolicyAPI{}, "put:UpdateEnablement")
beego.Router("/api/systeminfo/volumes", &SystemInfoAPI{}, "get:GetVolumeInfo")
beego.Router("/api/systeminfo/getcert", &SystemInfoAPI{}, "get:GetCert")
beego.Router("/api/ldap/ping", &LdapAPI{}, "post:Ping")
beego.Router("/api/configurations", &ConfigAPI{})
_ = updateInitPassword(1, "Harbor12345")
@ -512,7 +522,7 @@ func (a testapi) GetReposTop(authInfo usrInfo, count string) (int, error) {
//-------------------------Targets Test---------------------------------------//
//Create a new replication target
func (a testapi) AddTargets(authInfo usrInfo, repTarget apilib.RepTargetPost) (int, error) {
func (a testapi) AddTargets(authInfo usrInfo, repTarget apilib.RepTargetPost) (int, string, error) {
_sling := sling.New().Post(a.basePath)
path := "/api/targets"
@ -520,8 +530,8 @@ func (a testapi) AddTargets(authInfo usrInfo, repTarget apilib.RepTargetPost) (i
_sling = _sling.Path(path)
_sling = _sling.BodyJSON(repTarget)
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, err
httpStatusCode, body, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, string(body), err
}
//List filters targets by name
@ -897,3 +907,39 @@ func (a testapi) CertGet(authInfo usrInfo) (int, []byte, error) {
httpStatusCode, body, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, body, err
}
//Post ldap test
func (a testapi) LdapPost(authInfo usrInfo, ldapConf apilib.LdapConf) (int, error) {
_sling := sling.New().Post(a.basePath)
// create path and map variables
path := "/api/ldap/ping"
_sling = _sling.Path(path)
// body params
_sling = _sling.BodyJSON(ldapConf)
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
return httpStatusCode, err
}
func (a testapi) GetConfig(authInfo usrInfo) (int, map[string]*value, error) {
_sling := sling.New().Base(a.basePath).Get("/api/configurations")
cfg := map[string]*value{}
code, body, err := request(_sling, jsonAcceptHeader, authInfo)
if err == nil && code == 200 {
err = json.Unmarshal(body, &cfg)
}
return code, cfg, err
}
func (a testapi) PutConfig(authInfo usrInfo, cfg map[string]string) (int, error) {
_sling := sling.New().Base(a.basePath).Put("/api/configurations").BodyJSON(cfg)
code, _, err := request(_sling, jsonAcceptHeader, authInfo)
return code, err
}

159
src/ui/api/ldap.go Normal file
View File

@ -0,0 +1,159 @@
/*
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 (
"fmt"
"net/http"
"strconv"
"strings"
"time"
"crypto/tls"
"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"
goldap "gopkg.in/ldap.v2"
)
// LdapAPI handles requesst to /api/ldap/ping /api/ldap/search
type LdapAPI struct {
api.BaseAPI
}
var ldapConfs models.LdapConf
// Prepare ...
func (l *LdapAPI) Prepare() {
userID := l.ValidateUser()
isSysAdmin, err := dao.IsAdminRole(userID)
if err != nil {
log.Errorf("error occurred in IsAdminRole: %v", err)
l.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if !isSysAdmin {
l.CustomAbort(http.StatusForbidden, http.StatusText(http.StatusForbidden))
}
}
// Ping ...
func (l *LdapAPI) Ping() {
l.DecodeJSONReqAndValidate(&ldapConfs)
err := validateLdapReq(ldapConfs)
if err != nil {
log.Errorf("Invalid ldap request, error: %v", err)
l.RenderError(http.StatusBadRequest, fmt.Sprintf("invalid ldap request: %v", err))
return
}
err = connectTest(ldapConfs)
if err != nil {
log.Errorf("Ldap connect fail, error: %v", err)
l.RenderError(http.StatusBadRequest, fmt.Sprintf("ldap connect fail: %v", err))
return
}
}
func validateLdapReq(ldapConfs models.LdapConf) error {
ldapURL := ldapConfs.LdapURL
if ldapURL == "" {
return fmt.Errorf("can not get any available LDAP_URL")
}
log.Debug("ldapURL:", ldapURL)
ldapConnectionTimeout := ldapConfs.LdapConnectionTimeout
log.Debug("ldapConnectionTimeout:", ldapConnectionTimeout)
return nil
}
func connectTest(ldapConfs models.LdapConf) error {
var ldap *goldap.Conn
var protocol, hostport string
var host, port string
var err error
ldapURL := ldapConfs.LdapURL
// This routine keeps compability with the old format used on harbor.cfg
if strings.Contains(ldapURL, "://") {
splitLdapURL := strings.Split(ldapURL, "://")
protocol, hostport = splitLdapURL[0], splitLdapURL[1]
if !((protocol == "ldap") || (protocol == "ldaps")) {
return fmt.Errorf("unknown ldap protocl")
}
} else {
hostport = ldapURL
protocol = "ldap"
}
// This tries to detect the used port, if not defined
if strings.Contains(hostport, ":") {
splitHostPort := strings.Split(hostport, ":")
host, port = splitHostPort[0], splitHostPort[1]
_, error := strconv.Atoi(splitHostPort[1])
if error != nil {
return fmt.Errorf("illegal url format")
}
} else {
host = hostport
switch protocol {
case "ldap":
port = "389"
case "ldaps":
port = "636"
}
}
// Sets a Dial Timeout for LDAP
connectionTimeout := ldapConfs.LdapConnectionTimeout
goldap.DefaultTimeout = time.Duration(connectionTimeout) * time.Second
switch protocol {
case "ldap":
ldap, err = goldap.Dial("tcp", fmt.Sprintf("%s:%s", host, port))
case "ldaps":
ldap, err = goldap.DialTLS("tcp", fmt.Sprintf("%s:%s", host, port), &tls.Config{InsecureSkipVerify: true})
}
if err != nil {
return err
}
defer ldap.Close()
ldapSearchDn := ldapConfs.LdapSearchDn
if ldapSearchDn != "" {
log.Debug("Search DN: ", ldapSearchDn)
ldapSearchPassword := ldapConfs.LdapSearchPassword
err = ldap.Bind(ldapSearchDn, ldapSearchPassword)
if err != nil {
log.Debug("Bind search dn error", err)
return err
}
}
return nil
}

95
src/ui/api/ldap_test.go Normal file
View File

@ -0,0 +1,95 @@
package api
import (
"fmt"
"github.com/stretchr/testify/assert"
"github.com/vmware/harbor/tests/apitests/apilib"
"testing"
)
var ldapConf apilib.LdapConf
func TestLdapPost(t *testing.T) {
fmt.Println("Testing ldap post")
assert := assert.New(t)
apiTest := newHarborAPI()
//case 1: ping ldap server without admin role
CommonAddUser()
code, err := apiTest.LdapPost(*testUser, ldapConf)
if err != nil {
t.Error("Error occured while ping ldap server")
t.Log(err)
} else {
assert.Equal(403, code, "Ping ldap server status should be 403")
}
//case 2: ping ldap server with admin role, but empty ldapConf
code, err = apiTest.LdapPost(*admin, ldapConf)
if err != nil {
t.Error("Error occured while ping ldap server")
t.Log(err)
} else {
assert.Equal(400, code, "Ping ldap server status should be 400")
}
//case 3: ping ldap server with admin role, but bad format of ldapConf
ldapConf.LdapURL = "http://127.0.0.1"
code, err = apiTest.LdapPost(*admin, ldapConf)
if err != nil {
t.Error("Error occured while ping ldap server")
t.Log(err)
} else {
assert.Equal(400, code, "Ping ldap server status should be 400")
}
//case 4: ping ldap server with admin role, but bad format of ldapConf
ldapConf.LdapURL = "127.0.0.1:sss"
code, err = apiTest.LdapPost(*admin, ldapConf)
if err != nil {
t.Error("Error occured while ping ldap server")
t.Log(err)
} else {
assert.Equal(400, code, "Ping ldap server status should be 400")
}
//case 5: ping ldap server with admin role, ldap protocol, without port
ldapConf.LdapURL = "127.0.0.1"
code, err = apiTest.LdapPost(*admin, ldapConf)
if err != nil {
t.Error("Error occured while ping ldap server")
t.Log(err)
} else {
assert.Equal(200, code, "Ping ldap server status should be 200")
}
//not success, will try later
/*
//case 6: ping ldap server with admin role, ldaps protocol without port
ldapConf.LdapURL = "ldaps://127.0.0.1"
code, err = apiTest.LdapPost(*admin, ldapConf)
if err != nil {
t.Error("Error occured while ping ldap server")
t.Log(err)
} else {
assert.Equal(200, code, "Ping ldap server status should be 200")
}*/
//case 7: ping ldap server with admin role, ldap protocol, port, ldapSearchDn, but wrong password
ldapConf.LdapURL = "ldap://127.0.0.1:389"
ldapConf.LdapSearchDn = "cn=admin,dc=example,dc=org"
code, err = apiTest.LdapPost(*admin, ldapConf)
if err != nil {
t.Error("Error occured while ping ldap server")
t.Log(err)
} else {
assert.Equal(400, code, "Ping ldap server status should be 400")
}
//case 8: ping ldap server with admin role, ldap protocol, port, ldapSearchDn, right password
ldapConf.LdapURL = "ldap://127.0.0.1:389"
ldapConf.LdapSearchDn = "cn=admin,dc=example,dc=org"
ldapConf.LdapSearchPassword = "admin"
code, err = apiTest.LdapPost(*admin, ldapConf)
if err != nil {
t.Error("Error occured while ping ldap server")
t.Log(err)
} else {
assert.Equal(200, code, "Ping ldap server status should be 200")
}
CommonDelUser()
}

View File

@ -78,7 +78,13 @@ func (p *ProjectAPI) Post() {
if err != nil {
log.Errorf("Failed to check admin role: %v", err)
}
if !isSysAdmin && config.OnlyAdminCreateProject() {
onlyAdmin, err := config.OnlyAdminCreateProject()
if err != nil {
log.Errorf("failed to determine whether only admin can create projects: %v", err)
p.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if !isSysAdmin && onlyAdmin {
log.Errorf("Only sys admin can create project")
p.RenderError(http.StatusForbidden, "Only system admin can create project")
return

View File

@ -66,7 +66,7 @@ func (ra *RepositoryAPI) Get() {
if project.Public == 0 {
var userID int
if svc_utils.VerifySecret(ra.Ctx.Request) {
if svc_utils.VerifySecret(ra.Ctx.Request, config.JobserviceSecret()) {
userID = 1
} else {
userID = ra.ValidateUser()
@ -361,11 +361,19 @@ func (ra *RepositoryAPI) GetManifests() {
}
func (ra *RepositoryAPI) initRepositoryClient(repoName string) (r *registry.Repository, err error) {
endpoint := config.InternalRegistryURL()
endpoint, err := config.RegistryURL()
if err != nil {
return nil, err
}
verify, err := config.VerifyRemoteCert()
if err != nil {
return nil, err
}
username, password, ok := ra.Ctx.Request.BasicAuth()
if ok {
return newRepositoryClient(endpoint, api.GetIsInsecure(), username, password,
return newRepositoryClient(endpoint, !verify, username, password,
repoName, "repository", repoName, "pull", "push", "*")
}
@ -374,7 +382,7 @@ func (ra *RepositoryAPI) initRepositoryClient(repoName string) (r *registry.Repo
return nil, err
}
return cache.NewRepositoryClient(endpoint, api.GetIsInsecure(), username, repoName,
return cache.NewRepositoryClient(endpoint, !verify, username, repoName,
"repository", repoName, "pull", "push", "*")
}
@ -434,7 +442,9 @@ func newRepositoryClient(endpoint string, insecure bool, username, password, rep
scopeActions ...string) (*registry.Repository, error) {
credential := auth.NewBasicAuthCredential(username, password)
authorizer := auth.NewStandardTokenAuthorizer(credential, insecure, scopeType, scopeName, scopeActions...)
authorizer := auth.NewStandardTokenAuthorizer(credential, insecure,
config.InternalTokenServiceEndpoint(), scopeType, scopeName, scopeActions...)
store, err := auth.NewAuthorizerStore(endpoint, insecure, authorizer)
if err != nil {

View File

@ -41,7 +41,12 @@ type TargetAPI struct {
// Prepare validates the user
func (t *TargetAPI) Prepare() {
t.secretKey = config.SecretKey()
var err error
t.secretKey, err = config.SecretKey()
if err != nil {
log.Errorf("failed to get secret key: %v", err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
userID := t.ValidateUser()
isSysAdmin, err := dao.IsAdminRole(userID)
@ -97,7 +102,12 @@ func (t *TargetAPI) Ping() {
password = t.GetString("password")
}
registry, err := newRegistryClient(endpoint, api.GetIsInsecure(), username, password,
verify, err := config.VerifyRemoteCert()
if err != nil {
log.Errorf("failed to check whether insecure or not: %v", err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
registry, err := newRegistryClient(endpoint, !verify, username, password,
"", "", "")
if err != nil {
// timeout, dns resolve error, connection refused, etc.
@ -330,7 +340,9 @@ func (t *TargetAPI) Delete() {
func newRegistryClient(endpoint string, insecure bool, username, password, scopeType, scopeName string,
scopeActions ...string) (*registry.Registry, error) {
credential := auth.NewBasicAuthCredential(username, password)
authorizer := auth.NewStandardTokenAuthorizer(credential, insecure, scopeType, scopeName, scopeActions...)
authorizer := auth.NewStandardTokenAuthorizer(credential, insecure,
"", scopeType, scopeName, scopeActions...)
store, err := auth.NewAuthorizerStore(endpoint, insecure, authorizer)
if err != nil {

View File

@ -30,17 +30,18 @@ func TestTargetsPost(t *testing.T) {
//-------------------case 1 : response code = 201------------------------//
fmt.Println("case 1 : response code = 201")
httpStatusCode, err = apiTest.AddTargets(*admin, *repTargets)
httpStatusCode, body, err := apiTest.AddTargets(*admin, *repTargets)
if err != nil {
t.Error("Error whihle add targets", err.Error())
t.Log(err)
} else {
assert.Equal(int(201), httpStatusCode, "httpStatusCode should be 201")
t.Log(body)
}
//-----------case 2 : response code = 409,name is already used-----------//
fmt.Println("case 2 : response code = 409,name is already used")
httpStatusCode, err = apiTest.AddTargets(*admin, *repTargets)
httpStatusCode, _, err = apiTest.AddTargets(*admin, *repTargets)
if err != nil {
t.Error("Error whihle add targets", err.Error())
t.Log(err)
@ -51,7 +52,7 @@ func TestTargetsPost(t *testing.T) {
//-----------case 3 : response code = 409,name is already used-----------//
fmt.Println("case 3 : response code = 409,endPoint is already used")
repTargets.Username = "errName"
httpStatusCode, err = apiTest.AddTargets(*admin, *repTargets)
httpStatusCode, _, err = apiTest.AddTargets(*admin, *repTargets)
if err != nil {
t.Error("Error whihle add targets", err.Error())
t.Log(err)
@ -61,7 +62,7 @@ func TestTargetsPost(t *testing.T) {
//--------case 4 : response code = 401,User need to log in first.--------//
fmt.Println("case 4 : response code = 401,User need to log in first.")
httpStatusCode, err = apiTest.AddTargets(*unknownUsr, *repTargets)
httpStatusCode, _, err = apiTest.AddTargets(*unknownUsr, *repTargets)
if err != nil {
t.Error("Error whihle add targets", err.Error())
t.Log(err)

View File

@ -46,10 +46,21 @@ type passwordReq struct {
// Prepare validates the URL and parms
func (ua *UserAPI) Prepare() {
mode, err := config.AuthMode()
if err != nil {
log.Errorf("failed to get auth mode: %v", err)
ua.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
ua.AuthMode = config.AuthMode()
ua.AuthMode = mode
ua.SelfRegistration = config.SelfRegistration()
self, err := config.SelfRegistration()
if err != nil {
log.Errorf("failed to get self registration: %v", err)
ua.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
ua.SelfRegistration = self
if ua.Ctx.Input.IsPost() {
sessionUserID := ua.GetSession("userId")
@ -82,7 +93,6 @@ func (ua *UserAPI) Prepare() {
}
}
var err error
ua.IsAdmin, err = dao.IsAdminRole(ua.currentUserID)
if err != nil {
log.Errorf("Error occurred in IsAdminRole:%v", err)
@ -234,7 +244,7 @@ func (ua *UserAPI) Delete() {
return
}
if config.AuthMode() == "ldap_auth" {
if ua.AuthMode == "ldap_auth" {
ua.CustomAbort(http.StatusForbidden, "user can not be deleted in LDAP authentication mode")
}

View File

@ -20,11 +20,9 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"sort"
"strings"
"time"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
@ -242,7 +240,7 @@ func addAuthentication(req *http.Request) {
// SyncRegistry syncs the repositories of registry with database.
func SyncRegistry() error {
log.Debugf("Start syncing repositories from registry to DB... ")
log.Infof("Start syncing repositories from registry to DB... ")
reposInRegistry, err := catalog()
if err != nil {
@ -304,7 +302,7 @@ func SyncRegistry() error {
}
}
log.Debugf("Sync repositories from registry to DB is done.")
log.Infof("Sync repositories from registry to DB is done.")
return nil
}
@ -350,7 +348,10 @@ func diffRepos(reposInRegistry []string, reposInDB []string) ([]string, []string
}
// TODO remove the workaround when the bug of registry is fixed
endpoint := config.InternalRegistryURL()
endpoint, err := config.RegistryURL()
if err != nil {
return needsAdd, needsDel, err
}
client, err := cache.NewRepositoryClient(endpoint, true,
"admin", repoInR, "repository", repoInR)
if err != nil {
@ -372,7 +373,10 @@ func diffRepos(reposInRegistry []string, reposInDB []string) ([]string, []string
j++
} else {
// TODO remove the workaround when the bug of registry is fixed
endpoint := config.InternalRegistryURL()
endpoint, err := config.RegistryURL()
if err != nil {
return needsAdd, needsDel, err
}
client, err := cache.NewRepositoryClient(endpoint, true,
"admin", repoInR, "repository", repoInR)
if err != nil {
@ -422,32 +426,18 @@ func projectExists(repository string) (bool, error) {
}
func initRegistryClient() (r *registry.Registry, err error) {
endpoint := config.InternalRegistryURL()
addr := endpoint
if strings.Contains(endpoint, "/") {
addr = endpoint[strings.LastIndex(endpoint, "/")+1:]
endpoint, err := config.RegistryURL()
if err != nil {
return nil, err
}
ch := make(chan int, 1)
go func() {
var err error
var c net.Conn
for {
c, err = net.DialTimeout("tcp", addr, 20*time.Second)
if err == nil {
c.Close()
ch <- 1
} else {
log.Errorf("failed to connect to registry client, retry after 2 seconds :%v", err)
time.Sleep(2 * time.Second)
}
}
}()
select {
case <-ch:
case <-time.After(60 * time.Second):
panic("Failed to connect to registry client after 60 seconds")
addr := endpoint
if strings.Contains(endpoint, "://") {
addr = strings.Split(endpoint, "://")[1]
}
if err := utils.TestTCPConn(addr, 60, 2); err != nil {
return nil, err
}
registryClient, err := cache.NewRegistryClient(endpoint, true, "admin",

View File

@ -50,7 +50,10 @@ func Register(name string, authenticator Authenticator) {
// Login authenticates user credentials based on setting.
func Login(m models.AuthModel) (*models.User, error) {
var authMode = config.AuthMode()
authMode, err := config.AuthMode()
if err != nil {
return nil, err
}
if authMode == "" || m.Principal == "admin" {
authMode = "db_auth"
}

View File

@ -16,18 +16,15 @@
package ldap
import (
"crypto/tls"
"errors"
"fmt"
"strconv"
"strings"
"time"
"crypto/tls"
"github.com/vmware/harbor/src/common/utils/log"
"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/vmware/harbor/src/ui/config"
@ -41,9 +38,8 @@ const metaChars = "&|!=~*<>()"
// Connect checks the LDAP configuration directives, and connects to the LDAP URL
// Returns an LDAP connection
func Connect() (*goldap.Conn, error) {
ldapURL := config.LDAP().URL
func Connect(settings *models.LDAP) (*goldap.Conn, error) {
ldapURL := settings.URL
if ldapURL == "" {
return nil, errors.New("can not get any available LDAP_URL")
}
@ -70,13 +66,10 @@ func Connect() (*goldap.Conn, error) {
}
// Sets a Dial Timeout for LDAP
cTimeout := config.LDAP().ConnectTimeout
connectTimeout, _ := strconv.Atoi(cTimeout)
goldap.DefaultTimeout = time.Duration(connectTimeout) * time.Second
goldap.DefaultTimeout = time.Duration(settings.Timeout) * time.Second
var ldap *goldap.Conn
var err error
switch protocol {
case "ldap":
ldap, err = goldap.Dial("tcp", fmt.Sprintf("%s:%s", host, port))
@ -104,21 +97,26 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
}
}
ldap, err := Connect()
settings, err := config.LDAP()
if err != nil {
return nil, err
}
ldapBaseDn := config.LDAP().BaseDn
ldap, err := Connect(settings)
if err != nil {
return nil, err
}
ldapBaseDn := settings.BaseDN
if ldapBaseDn == "" {
return nil, errors.New("can not get any available LDAP_BASE_DN")
}
log.Debug("baseDn:", ldapBaseDn)
ldapSearchDn := config.LDAP().SearchDn
ldapSearchDn := settings.SearchDN
if ldapSearchDn != "" {
log.Debug("Search DN: ", ldapSearchDn)
ldapSearchPwd := config.LDAP().SearchPwd
ldapSearchPwd := settings.SearchPassword
err = ldap.Bind(ldapSearchDn, ldapSearchPwd)
if err != nil {
log.Debug("Bind search dn error", err)
@ -126,8 +124,8 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
}
}
attrName := config.LDAP().UID
filter := config.LDAP().Filter
attrName := settings.UID
filter := settings.Filter
if filter != "" {
filter = "(&" + filter + "(" + attrName + "=" + m.Principal + "))"
} else {
@ -135,11 +133,11 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
}
log.Debug("one or more filter", filter)
ldapScope := config.LDAP().Scope
ldapScope := settings.Scope
var scope int
if ldapScope == "1" {
if ldapScope == 1 {
scope = goldap.ScopeBaseObject
} else if ldapScope == "2" {
} else if ldapScope == 2 {
scope = goldap.ScopeSingleLevel
} else {
scope = goldap.ScopeWholeSubtree

View File

@ -13,145 +13,243 @@
limitations under the License.
*/
// Package config provides methods to get configurations required by code in src/ui
package config
import (
"strconv"
"strings"
"encoding/json"
"os"
commonConfig "github.com/vmware/harbor/src/common/config"
comcfg "github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/models"
"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
ConnectTimeout string
}
const defaultKeyPath string = "/etc/ui/key"
type uiParser struct{}
var (
mg *comcfg.Manager
keyProvider comcfg.KeyProvider
)
// 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"],
ConnectTimeout: raw["LDAP_CONNECT_TIMEOUT"],
}
config["ldap"] = setting
// Init configurations
func Init() error {
//init key provider
initKeyProvider()
adminServerURL := os.Getenv("ADMIN_SERVER_URL")
if len(adminServerURL) == 0 {
adminServerURL = "http://adminserver"
}
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
}
log.Debugf("admin server URL: %s", adminServerURL)
mg = comcfg.NewManager(adminServerURL, UISecret(), true)
if err := mg.Init(); err != nil {
return err
}
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
if _, err := mg.Load(); err != nil {
return err
}
return nil
}
var uiConfig *commonConfig.Config
func initKeyProvider() {
path := os.Getenv("KEY_PATH")
if len(path) == 0 {
path = defaultKeyPath
}
log.Infof("key path: %s", path)
func init() {
uiKeys := []string{"AUTH_MODE", "LDAP_URL", "LDAP_BASE_DN", "LDAP_SEARCH_DN", "LDAP_SEARCH_PWD", "LDAP_UID", "LDAP_FILTER", "LDAP_SCOPE", "LDAP_CONNECT_TIMEOUT", "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)
}
keyProvider = comcfg.NewFileKeyProvider(path)
}
// Reload ...
func Reload() error {
return uiConfig.Load()
// Load configurations
func Load() error {
_, err := mg.Load()
return err
}
// Upload uploads all system configutations to admin server
func Upload(cfg map[string]interface{}) error {
b, err := json.Marshal(cfg)
if err != nil {
return err
}
return mg.Upload(b)
}
// GetSystemCfg returns the system configurations
func GetSystemCfg() (map[string]interface{}, error) {
raw, err := mg.Loader.Load()
if err != nil {
return nil, err
}
c, err := mg.Parser.Parse(raw)
if err != nil {
return nil, err
}
return c, nil
}
// AuthMode ...
func AuthMode() string {
return uiConfig.Config["auth_mode"].(string)
func AuthMode() (string, error) {
cfg, err := mg.Get()
if err != nil {
return "", err
}
return cfg[comcfg.AUTHMode].(string), nil
}
// LDAP returns the setting of ldap server
func LDAP() LDAPSetting {
return uiConfig.Config["ldap"].(LDAPSetting)
func LDAP() (*models.LDAP, error) {
cfg, err := mg.Get()
if err != nil {
return nil, err
}
ldap := &models.LDAP{}
ldap.URL = cfg[comcfg.LDAPURL].(string)
ldap.SearchDN = cfg[comcfg.LDAPSearchDN].(string)
ldap.SearchPassword = cfg[comcfg.LDAPSearchPwd].(string)
ldap.BaseDN = cfg[comcfg.LDAPBaseDN].(string)
ldap.UID = cfg[comcfg.LDAPUID].(string)
ldap.Filter = cfg[comcfg.LDAPFilter].(string)
ldap.Scope = int(cfg[comcfg.LDAPScope].(float64))
ldap.Timeout = int(cfg[comcfg.LDAPTimeout].(float64))
return ldap, nil
}
// TokenExpiration returns the token expiration time (in minute)
func TokenExpiration() int {
return uiConfig.Config["token_exp"].(int)
func TokenExpiration() (int, error) {
cfg, err := mg.Get()
if err != nil {
return 0, err
}
return int(cfg[comcfg.TokenExpiration].(float64)), nil
}
// 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)
// ExtEndpoint returns the external URL of Harbor: protocal://host:port
func ExtEndpoint() (string, error) {
cfg, err := mg.Get()
if err != nil {
return "", err
}
return cfg[comcfg.ExtEndpoint].(string), nil
}
// SecretKey returns the secret key to encrypt the password of target
func SecretKey() string {
return uiConfig.Config["secret_key"].(string)
func SecretKey() (string, error) {
return keyProvider.Get(nil)
}
// SelfRegistration returns the enablement of self registration
func SelfRegistration() bool {
return uiConfig.Config["self_registration"].(bool)
func SelfRegistration() (bool, error) {
cfg, err := mg.Get()
if err != nil {
return false, err
}
return cfg[comcfg.SelfRegistration].(bool), nil
}
// InternalRegistryURL returns registry URL for internal communication between Harbor containers
func InternalRegistryURL() string {
return uiConfig.Config["internal_registry_url"].(string)
// RegistryURL ...
func RegistryURL() (string, error) {
cfg, err := mg.Get()
if err != nil {
return "", err
}
return cfg[comcfg.RegistryURL].(string), nil
}
// InternalJobServiceURL returns jobservice URL for internal communication between Harbor containers
func InternalJobServiceURL() string {
return uiConfig.Config["internal_jobservice_url"].(string)
return "http://jobservice"
}
// InternalTokenServiceEndpoint returns token service endpoint for internal communication between Harbor containers
func InternalTokenServiceEndpoint() string {
return "http://ui/service/token"
}
// InitialAdminPassword returns the initial password for administrator
func InitialAdminPassword() string {
return uiConfig.Config["admin_password"].(string)
func InitialAdminPassword() (string, error) {
cfg, err := mg.Get()
if err != nil {
return "", err
}
return cfg[comcfg.AdminInitialPassword].(string), nil
}
// OnlyAdminCreateProject returns the flag to restrict that only sys admin can create project
func OnlyAdminCreateProject() bool {
return uiConfig.Config["admin_create_project"].(bool)
func OnlyAdminCreateProject() (bool, error) {
cfg, err := mg.Get()
if err != nil {
return true, err
}
return cfg[comcfg.ProjectCreationRestriction].(string) == comcfg.ProCrtRestrAdmOnly, nil
}
// VerifyRemoteCert returns bool value.
func VerifyRemoteCert() (bool, error) {
cfg, err := mg.Get()
if err != nil {
return true, err
}
return cfg[comcfg.VerifyRemoteCert].(bool), nil
}
// Email returns email server settings
func Email() (*models.Email, error) {
cfg, err := mg.Get()
if err != nil {
return nil, err
}
email := &models.Email{}
email.Host = cfg[comcfg.EmailHost].(string)
email.Port = int(cfg[comcfg.EmailPort].(float64))
email.Username = cfg[comcfg.EmailUsername].(string)
email.Password = cfg[comcfg.EmailPassword].(string)
email.SSL = cfg[comcfg.EmailSSL].(bool)
email.From = cfg[comcfg.EmailFrom].(string)
email.Identity = cfg[comcfg.EmailIdentity].(string)
return email, nil
}
// Database returns database settings
func Database() (*models.Database, error) {
cfg, err := mg.Get()
if err != nil {
return nil, err
}
database := &models.Database{}
database.Type = cfg[comcfg.DatabaseType].(string)
mysql := &models.MySQL{}
mysql.Host = cfg[comcfg.MySQLHost].(string)
mysql.Port = int(cfg[comcfg.MySQLPort].(float64))
mysql.Username = cfg[comcfg.MySQLUsername].(string)
mysql.Password = cfg[comcfg.MySQLPassword].(string)
mysql.Database = cfg[comcfg.MySQLDatabase].(string)
database.MySQL = mysql
sqlite := &models.SQLite{}
sqlite.File = cfg[comcfg.SQLiteFile].(string)
database.SQLite = sqlite
return database, nil
}
// UISecret returns a secret to mark UI when communicate with
// other component
func UISecret() string {
return os.Getenv("UI_SECRET")
}
// JobserviceSecret returns a secret to mark Jobservice when communicate with
// other component
func JobserviceSecret() string {
return os.Getenv("JOBSERVICE_SECRET")
}

View File

@ -17,131 +17,107 @@ package config
import (
"os"
"testing"
"github.com/vmware/harbor/src/common/utils/test"
)
var (
auth = "ldap_auth"
ldap = LDAPSetting{
"ldap://test.ldap.com",
"ou=people",
"dc=whatever,dc=org",
"1234567",
"cn",
"uid",
"2",
"5",
}
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("LDAP_CONNECT_TIMEOUT", ldap.ConnectTimeout)
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()
// test functions under package ui/config
func TestConfig(t *testing.T) {
server, err := test.NewAdminserver(nil)
if err != nil {
panic(err)
t.Fatalf("failed to create a mock admin server: %v", err)
}
rc := m.Run()
defer server.Close()
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("LDAP_CONNECT_TIMEOUT")
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 err := os.Setenv("ADMIN_SERVER_URL", server.URL); err != nil {
t.Fatalf("failed to set env %s: %v", "ADMIN_SERVER_URL", err)
}
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())
secretKeyPath := "/tmp/secretkey"
_, err = test.GenerateKey(secretKeyPath)
if err != nil {
t.Errorf("failed to generate secret key: %v", err)
return
}
defer os.Remove(secretKeyPath)
if err := os.Setenv("KEY_PATH", secretKeyPath); err != nil {
t.Fatalf("failed to set env %s: %v", "KEY_PATH", err)
}
if err := Init(); err != nil {
t.Fatalf("failed to initialize configurations: %v", err)
}
if err := Load(); err != nil {
t.Fatalf("failed to load configurations: %v", err)
}
if err := Upload(map[string]interface{}{}); err != nil {
t.Fatalf("failed to upload configurations: %v", err)
}
if _, err := GetSystemCfg(); err != nil {
t.Fatalf("failed to get system configurations: %v", err)
}
mode, err := AuthMode()
if err != nil {
t.Fatalf("failed to get auth mode: %v", err)
}
if mode != "db_auth" {
t.Errorf("unexpected mode: %s != %s", mode, "db_auth")
}
if _, err := LDAP(); err != nil {
t.Fatalf("failed to get ldap settings: %v", err)
}
if _, err := TokenExpiration(); err != nil {
t.Fatalf("failed to get token expiration: %v", err)
}
if _, err := ExtEndpoint(); err != nil {
t.Fatalf("failed to get domain name: %v", err)
}
if _, err := SecretKey(); err != nil {
t.Fatalf("failed to get secret key: %v", err)
}
if _, err := SelfRegistration(); err != nil {
t.Fatalf("failed to get self registration: %v", err)
}
if _, err := RegistryURL(); err != nil {
t.Fatalf("failed to get registry URL: %v", err)
}
if len(InternalJobServiceURL()) == 0 {
t.Error("the internal job service url is null")
}
if len(InternalTokenServiceEndpoint()) == 0 {
t.Error("the internal token service endpoint is null")
}
if _, err := InitialAdminPassword(); err != nil {
t.Fatalf("failed to get initial admin password: %v", err)
}
if _, err := OnlyAdminCreateProject(); err != nil {
t.Fatalf("failed to get onldy admin create project: %v", err)
}
if _, err := VerifyRemoteCert(); err != nil {
t.Fatalf("failed to get verify remote cert: %v", err)
}
if _, err := Email(); err != nil {
t.Fatalf("failed to get email settings: %v", err)
}
if _, err := Database(); err != nil {
t.Fatalf("failed to get database: %v", err)
}
}

View File

@ -103,7 +103,12 @@ func (b *BaseController) Prepare() {
b.Data["CurLang"] = curLang.Name
b.Data["RestLangs"] = restLangs
authMode := config.AuthMode()
authMode, err := config.AuthMode()
if err != nil {
log.Errorf("failed to get auth mode: %v", err)
b.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if authMode == "" {
authMode = "db_auth"
}
@ -120,9 +125,13 @@ func (b *BaseController) Prepare() {
b.UseCompressedJS = false
}
b.SelfRegistration = config.SelfRegistration()
b.SelfRegistration, err = config.SelfRegistration()
if err != nil {
log.Errorf("failed to get self registration: %v", err)
b.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
b.Data["SelfRegistration"] = config.SelfRegistration()
b.Data["SelfRegistration"] = b.SelfRegistration
sessionUserID := b.GetSession("userId")
if sessionUserID != nil {
@ -235,12 +244,13 @@ func (cc *CommonController) UserExists() {
}
func init() {
//conf/app.conf -> os.Getenv("config_path")
configPath := os.Getenv("CONFIG_PATH")
if len(configPath) != 0 {
log.Infof("Config path: %s", configPath)
beego.LoadAppConfig("ini", configPath)
if err := beego.LoadAppConfig("ini", configPath); err != nil {
log.Errorf("failed to load app config: %v", err)
}
}
beego.AddFuncMap("i18n", i18n.Tr)
@ -263,5 +273,4 @@ func init() {
log.Errorf("Fail to set message file: %s", err.Error())
}
}
}

View File

@ -14,6 +14,8 @@ import (
"github.com/astaxie/beego"
//"github.com/dghubble/sling"
"github.com/stretchr/testify/assert"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/config"
)
//const (
@ -29,6 +31,9 @@ import (
//var admin *usrInfo
func init() {
if err := config.Init(); err != nil {
log.Fatalf("failed to initialize configurations: %v", err)
}
_, file, _, _ := runtime.Caller(1)
apppath, _ := filepath.Abs(filepath.Dir(filepath.Join(file, ".."+string(filepath.Separator))))
@ -63,7 +68,6 @@ func init() {
//Init user Info
//admin = &usrInfo{adminName, adminPwd}
}
// TestMain is a sample to run an endpoint test

View File

@ -6,12 +6,12 @@ import (
"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"
email_util "github.com/vmware/harbor/src/common/utils/email"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/config"
)
type messageDetail struct {
@ -49,7 +49,11 @@ func (cc *CommonController) SendEmail() {
message := new(bytes.Buffer)
harborURL := config.ExtEndpoint()
harborURL, err := config.ExtEndpoint()
if err != nil {
log.Errorf("failed to get domain name: %v", err)
cc.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if harborURL == "" {
harborURL = "localhost"
}
@ -65,14 +69,14 @@ func (cc *CommonController) SendEmail() {
cc.CustomAbort(http.StatusInternalServerError, "internal_error")
}
config, err := beego.AppConfig.GetSection("mail")
emailSettings, err := config.Email()
if err != nil {
log.Errorf("Can not load app.conf: %v", err)
log.Errorf("failed to get email configurations: %v", err)
cc.CustomAbort(http.StatusInternalServerError, "internal_error")
}
mail := utils.Mail{
From: config["from"],
mail := email_util.Mail{
From: emailSettings.From,
To: []string{email},
Subject: cc.Tr("reset_email_subject"),
Message: message.String()}

View File

@ -1,6 +1,8 @@
package controllers
import (
"net/http"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/config"
@ -23,6 +25,11 @@ func (pc *ProjectController) Get() {
isSysAdmin = false
}
}
pc.Data["CanCreate"] = !config.OnlyAdminCreateProject() || isSysAdmin
onlyAdmin, err := config.OnlyAdminCreateProject()
if err != nil {
log.Errorf("failed to determine whether only admin can create projects: %v", err)
pc.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
pc.Data["CanCreate"] = !onlyAdmin || isSysAdmin
pc.Forward("page_title_project", "project.htm")
}

View File

@ -1,6 +1,10 @@
package controllers
import (
"net/http"
"strings"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/config"
)
@ -11,6 +15,11 @@ type RepositoryController struct {
// Get renders repository page
func (rc *RepositoryController) Get() {
rc.Data["HarborRegUrl"] = config.ExtRegistryURL()
url, err := config.ExtEndpoint()
if err != nil {
log.Errorf("failed to get domain name: %v", err)
rc.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
rc.Data["HarborRegUrl"] = strings.Split(url, "://")[1]
rc.Forward("page_title_repository", "repository.htm")
}

View File

@ -64,7 +64,6 @@ func updateInitPassword(userID int, password string) error {
}
func main() {
beego.BConfig.WebConfig.Session.SessionOn = true
//TODO
redisURL := os.Getenv("_REDIS_URL")
@ -72,12 +71,28 @@ func main() {
beego.BConfig.WebConfig.Session.SessionProvider = "redis"
beego.BConfig.WebConfig.Session.SessionProviderConfig = redisURL
}
//
beego.AddTemplateExt("htm")
dao.InitDatabase()
log.Info("initializing configurations...")
if err := config.Init(); err != nil {
log.Fatalf("failed to initialize configurations: %v", err)
}
log.Info("configurations initialization completed")
if err := updateInitPassword(adminUserID, config.InitialAdminPassword()); err != nil {
database, err := config.Database()
if err != nil {
log.Fatalf("failed to get database configuration: %v", err)
}
if err := dao.InitDatabase(database); err != nil {
log.Fatalf("failed to initialize database: %v", err)
}
password, err := config.InitialAdminPassword()
if err != nil {
log.Fatalf("failed to get admin's initia password: %v", err)
}
if err := updateInitPassword(adminUserID, password); err != nil {
log.Error(err)
}
initRouters()

View File

@ -84,9 +84,12 @@ 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/configurations", &api.ConfigAPI{})
beego.Router("/api/systeminfo/volumes", &api.SystemInfoAPI{}, "get:GetVolumeInfo")
beego.Router("/api/systeminfo/getcert", &api.SystemInfoAPI{}, "get:GetCert")
beego.Router("/api/ldap/ping", &api.LdapAPI{}, "post:Ping")
//external service that hosted on harbor process:
beego.Router("/service/notifications", &service.NotificationHandler{})
beego.Router("/service/token", &token.Handler{})

View File

@ -36,13 +36,10 @@ const (
issuer = "registry-token-issuer"
)
var expiration int //minutes
var privateKey string
func init() {
expiration = config.TokenExpiration()
privateKey = "/etc/ui/private_key.pem"
log.Infof("token expiration: %d minutes", expiration)
}
// GetResourceActions ...
@ -92,7 +89,12 @@ func FilterAccess(username string, a *token.ResourceActions) {
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()
registryURL, err := config.ExtEndpoint()
if err != nil {
log.Errorf("failed to get domain name: %v", err)
return
}
registryURL = strings.Split(registryURL, "://")[1]
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)
@ -154,6 +156,11 @@ func MakeToken(username, service string, access []*token.ResourceActions) (token
if err != nil {
return "", 0, nil, err
}
expiration, err := config.TokenExpiration()
if err != nil {
return "", 0, nil, err
}
tk, expiresIn, issuedAt, err := makeTokenCore(issuer, username, service, expiration, access, pk)
if err != nil {
return "", 0, nil, err

View File

@ -19,10 +19,11 @@ import (
"net/http"
"time"
"github.com/vmware/harbor/src/ui/auth"
"github.com/vmware/harbor/src/common/models"
svc_utils "github.com/vmware/harbor/src/ui/service/utils"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/auth"
"github.com/vmware/harbor/src/ui/config"
svc_utils "github.com/vmware/harbor/src/ui/service/utils"
"github.com/astaxie/beego"
"github.com/docker/distribution/registry/auth/token"
@ -45,7 +46,7 @@ func (h *Handler) Get() {
access := GetResourceActions(scopes)
log.Infof("request url: %v", request.URL.String())
if svc_utils.VerifySecret(request) {
if svc_utils.VerifySecret(request, config.JobserviceSecret()) {
log.Debugf("Will grant all access as this request is from job service with legal secret.")
username = "job-service-user"
} else {

View File

@ -14,9 +14,16 @@ import (
"path"
"runtime"
"testing"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/config"
)
func TestMain(m *testing.M) {
if err := config.Init(); err != nil {
log.Fatalf("failed to initialize configurations: %v", err)
}
result := m.Run()
if result != 0 {
os.Exit(result)
@ -73,7 +80,6 @@ func TestMakeToken(t *testing.T) {
pk, crt := getKeyAndCertPath()
//overwrite the config values for testing.
privateKey = pk
expiration = 10
ra := []*token.ResourceActions{&token.ResourceActions{
Type: "repository",
Name: "10.117.4.142/notary-test/hello-world-2",

View File

@ -20,15 +20,13 @@ import (
"net/http"
"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 := config.UISecret()
c, err := r.Cookie("uisecret")
func VerifySecret(r *http.Request, expectedSecret string) bool {
c, err := r.Cookie("secret")
if err != nil {
log.Warningf("Failed to get secret cookie, error: %v", err)
}
return c != nil && c.Value == secret
return c != nil && c.Value == expectedSecret
}

View File

@ -14,7 +14,7 @@
*/
.switch-pane-admin-options {
display: inline;
width: 245px;
width: 340px;
float: right;
list-style-type: none;
}
@ -28,4 +28,8 @@
.switch-pane-admin-options li .active {
border-bottom: 2px solid rgb(0, 84, 190);
font-weight: bold;
}
.inline-help-config {
padding: 6px;
}

View File

@ -12,65 +12,224 @@
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)" autocomplete="off">
<div class="col-md-12">
<h5>System Settings</h5>
<hr/>
</div>
<div class="col-md-12 col-md-off-set-1 main-content">
<div class="form-group">
<label for="hostName" class="col-sm-3 control-label">Host Name:</label>
<div class="col-sm-7">
<input type="text" class="form-control" id="hostName" ng-model="system.hostName" ng-model-options="{ updateOn: 'blur' }" ng-value="vm.system.hostName" name="uHostName" required>
<div ng-messages="form.$dirty && form.uHostName.$error">
<span ng-message="required">Host name is required.</span>
<div class="row">
<ul id="ulTabHeader" class="nav nav-pills nav-stacked col-md-2 col-xs-12">
<li role="presentation"><a href="#auth" aria-controls="auth" role="tab" data-toggle="tab">// 'authentication' | tr //</a></li>
<li role="presentation"><a href="#email" aria-controls="email" role="tab" data-toggle="tab">// 'email_settings' | tr //</a></li>
<li role="presentation"><a href="#system" aria-controls="system" role="tab" data-toggle="tab">// 'system_settings' | tr //</a></li>
</ul>
<!-- Tab panes -->
<div id="tabConfigurations" class="tab-content col-md-10 col-xs-12">
<div role="tabpanel" class="tab-pane" id="auth">
<form name="authForm" class="form-horizontal" no-validate autocomplete="off">
<div class="form-group">
<label for="selAuth" class="col-sm-3 control-label">// 'authentication_mode' | tr //<span ng-if="vm.warning['auth.authMode']">*</span></label>
<div class="col-sm-2">
<select id="selAuth" class="form-control" ng-model="auth.authMode.data" ng-options="r as r.name for r in vm.supportedAuths track by r.value" ng-disabled="!vm.editable['auth.authMode']"></select>
</div>
<div class="inline-help-config">
<inline-help help-title="// 'authentication_mode' | tr //" content="// 'authentication_mode_desc' | tr //"></inline-help>
</div>
</div>
</div>
<div class="form-group" ng-if="auth.authMode.data.value !== 'ldap_auth'">
<label for="selfRegistration" class="col-sm-3 control-label">// 'self_registration' | tr //<span ng-if="vm.warning['auth.selfRegistration']">*</span></label>
<div class="col-sm-5">
<select class="form-control" ng-model="auth.selfRegistration.data" ng-options="r as r.name for r in vm.toggleBooleans track by r.value" ng-disabled="!vm.editable['auth.selfRegistration']"></select>
</div>
<div class="inline-help-config">
<inline-help help-title="// 'self_registration' | tr //" content="// 'self_registration_desc' | tr //"></inline-help>
</div>
</div>
<div class="form-group" ng-if="auth.authMode.data.value === 'ldap_auth'">
<label for="ldapURL" class="col-sm-3 control-label">// 'ldap_url' | tr //<span ng-if="vm.warning['auth.ldapURL']">*</span></label>
<div class="col-sm-5">
<input id="ldapURL" type="text" class="form-control" ng-model="auth.ldapURL.data" ng-disabled="!vm.editable['auth.ldapURL']">
</div>
<div class="inline-help-config">
<inline-help help-title="// 'ldap_url' | tr //" content="// 'ldap_url_desc' | tr //"></inline-help>
</div>
</div>
<div class="form-group" ng-if="auth.authMode.data.value === 'ldap_auth'">
<label for="ldapSearchDN" class="col-sm-3 control-label">// 'ldap_search_dn' | tr //<span ng-if="vm.warning['auth.ldapSearchDN']">*</span></label>
<div class="col-sm-5 clearfix">
<input id="ldapSearchDN" type="text" class="form-control" ng-model="auth.ldapSearchDN.data" ng-disabled="!vm.editable['auth.ldapSearchDN']">
</div>
<div class="inline-help-config">
<inline-help help-title="// 'ldap_search_dn' | tr //" content="// 'ldap_search_dn_desc' | tr //"></inline-help>
</div>
</div>
<div class="form-group" ng-if="auth.authMode.data.value === 'ldap_auth'">
<label for="ldapSearchPassword" class="col-sm-3 control-label">// 'ldap_search_password' | tr //<span ng-if="vm.warning['auth.ldapSearchPassword']">*</span></label>
<div class="col-sm-5">
<input id="ldapSearchPassword" type="password" class="form-control" ng-model="auth.ldapSearchPassword.data" ng-click="vm.clearUp(auth.ldapSearchPassword)" ng-change="vm.hasChanged(auth.ldapSearchPassword)" ng-blur="vm.setMaskPassword(auth.ldapSearchPassword)">
</div>
<div class="inline-help-config">
<inline-help help-title="// 'ldap_search_password' | tr //" content="// 'ldap_search_password_desc' | tr //"></inline-help>
</div>
</div>
<div class="form-group" ng-if="auth.authMode.data.value === 'ldap_auth'">
<label for="ldapBaseDN" class="col-sm-3 control-label">// 'ldap_base_dn' | tr //<span ng-if="vm.warning['auth.ldapBaseDN']">*</span></label>
<div class="col-sm-5">
<input id="ldapBaseDN" type="text" class="form-control" ng-model="auth.ldapBaseDN.data" ng-disabled="!vm.editable['auth.ldapBaseDN']">
</div>
<div class="inline-help-config">
<inline-help help-title="// 'ldap_base_dn' | tr //" content="// 'ldap_base_dn_desc' | tr //"></inline-help>
</div>
</div>
<div class="form-group" ng-if="auth.authMode.data.value === 'ldap_auth'">
<label for="ldapUID" class="col-sm-3 control-label">// 'ldap_uid' | tr //<span ng-if="vm.warning['auth.ldapUID']">*</span></label>
<div class="col-sm-5">
<input id="ldapUID" type="text" class="form-control" ng-model="auth.ldapUID.data" ng-disabled="!vm.editable['auth.ldapUID']">
</div>
<div class="inline-help-config">
<inline-help help-title="// 'ldap_uid' | tr //" content="// 'ldap_uid_desc' | tr //"></inline-help>
</div>
</div>
<div class="form-group" ng-if="auth.authMode.data.value === 'ldap_auth'">
<label for="ldapFilter" class="col-sm-3 control-label">// 'ldap_filter' | tr //<span ng-if="vm.warning['auth.ldapFilter']">*</span></label>
<div class="col-sm-5 clearfix">
<input id="ldapFilter" type="text" class="form-control" ng-model="auth.ldapFilter.data" ng-disabled="!vm.editable['auth.ldapFilter']">
</div>
<div class="inline-help-config">
<inline-help help-title="// 'ldap_filter' | tr //" content="// 'ldap_filter_desc' | tr //"></inline-help>
</div>
</div>
<div class="form-group" ng-if="auth.authMode.data.value === 'ldap_auth'">
<label for="ldapTimeout" class="col-sm-3 control-label">// 'ldap_connection_timeout' | tr //<span ng-if="vm.warning['auth.ldapConnectionTimeout']">*</span></label>
<div class="col-sm-2 clearfix">
<input id="ldapConnectionTimeout" type="number" class="form-control" name="ldapConnectionTimeout" ng-model="auth.ldapConnectionTimeout.data" ng-disabled="!vm.editable['auth.ldapConnectionTimeout']" min="1" max="60" required>
<div class="error-message" ng-messages="authForm.ldapConnectionTimeout.$error" >
<span ng-message="required">// 'timeout_is_required' | tr //</span>
<span ng-message="min">// 'invalid_timeout' | tr //</span>
<span ng-message="max">// 'invalid_timeout' | tr //</span>
</div>
</div>
</div>
<div class="form-group" ng-if="auth.authMode.data.value === 'ldap_auth'">
<label for="selLDAPScope" class="col-sm-3 control-label">// 'ldap_scope' | tr //<span ng-if="vm.warning['auth.ldapScope']">*</span></label>
<div class="col-sm-4">
<select id="selLDAPScope" class="form-control" ng-model="auth.ldapScope.data" ng-options="item for item in [1, 2, 3]" ng-disabled="!vm.editable['auth.ldapScope']"></select>
</div>
<div class="inline-help-config">
<inline-help help-title="// 'ldap_scope' | tr //" content="// 'ldap_scope_desc' | tr //"></inline-help>
</div>
</div>
<div class="form-group" ng-if="auth.authMode.data.value === 'ldap_auth'">
<div class="col-sm-7 col-sm-offset-3">
<span ng-if="vm.isError" class="error-message" >// vm.pingMessage //</span>
<span ng-if="!vm.isError">// vm.pingMessage //</span>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-5">
<div class="pull-right">
<button type="submit" class="btn btn-link" ng-if="vm.changed" ng-click="vm.undo()">// 'undo' | tr //</button>
<button type="button" class="btn btn-default" ng-if="auth.authMode.data.value === 'ldap_auth'" ng-disabled="authForm.$invalid" ng-click="vm.pingLDAP(auth)" loading-progress hide-target="false" toggle-in-progress="vm.pingTIP">// 'test_connection' | tr //</button>
<button type="submit" class="btn btn-success" ng-disabled="authForm.$invalid" ng-click="vm.saveAuthConf(auth)">// 'save' | tr //</button>
</div>
</div>
</div>
</form>
</div>
<div class="form-group">
<label for="urlProtocol" class="col-sm-3 control-label">URL Protocol:</label>
<div class="col-sm-7">
<input type="text" class="form-control" id="urlProtocol" ng-model="system.urlProtocol" ng-model-options="{ updateOn: 'blur' }" ng-value="vm.system.urlProtocol" name="uUrlProtocol" required>
<div ng-messages="form.$dirty && form.uUrlProtocol.$error">
<span ng-message="required">Url protocol is required.</span>
<div role="tabpanel" class="tab-pane" id="email">
<form name="emailForm" class="form-horizontal" novalidate autocomplete="off">
<div class="form-group">
<label for="emailServer" class="col-sm-3 control-label">// 'email_server' | tr //<span ng-if="vm.warning['email.server']">*</span></label>
<div class="col-sm-5">
<input id="emailServer" type="text" class="form-control" ng-model="email.server.data" ng-disabled="!vm.editable['email.server']">
</div>
<div class="inline-help-config">
<inline-help help-title="// 'email_server' | tr //" content="// 'email_server_desc' | tr //"></inline-help>
</div>
</div>
</div>
</div>
<div class="form-group">
<label for="emailServer" class="col-sm-3 control-label">Email server:</label>
<div class="col-sm-7">
<input type="text" class="form-control" id="emailServer" ng-model="system.emailServer" ng-model-options="{ updateOn: 'blur' }" ng-value="vm.system.emailServer" name="uEmailServer" required>
<div ng-messages="form.$dirty && form.uEmailServer.$error">
<span ng-message="required">Email server is required.</span>
<div class="form-group">
<label for="emailServerPort" class="col-sm-3 control-label">// 'email_server_port' | tr //<span ng-if="vm.warning['email.serverPort']">*</span></label>
<div class="col-sm-5">
<input id="emailServerPort" type="number" class="form-control" ng-model="email.serverPort.data" name="emailServerPort" ng-disabled="!vm.editable['email.serverPort']" min="1" max="65535">
<div class="error-message" ng-messages="emailForm.emailServerPort.$error" >
<span ng-message="min">// 'invalid_port_number' | tr //</span>
<span ng-message="max">// 'invalid_port_number' | tr //</span>
</div>
</div>
<div class="inline-help-config">
<inline-help help-title="// 'email_server_port' | tr //" content="// 'email_server_port_desc' | tr //"></inline-help>
</div>
</div>
</div>
<div class="form-group">
<label for="emailUsername" class="col-sm-3 control-label">// 'email_username' | tr //<span ng-if="vm.warning['email.username']">*</span></label>
<div class="col-sm-5">
<input id="emailServerPort" type="text" class="form-control" ng-model="email.username.data" ng-disabled="!vm.editable['email.username']">
</div>
<div class="inline-help-config">
<inline-help help-title="// 'email_username' | tr //" content="// 'email_username_desc' | tr //"></inline-help>
</div>
</div>
<div class="form-group">
<label for="emailPassword" class="col-sm-3 control-label">// 'email_password' | tr //<span ng-if="vm.warning['email.password']">*</span></label>
<div class="col-sm-5">
<input id="emailPassword" type="password" class="form-control" ng-model="email.password.data" ng-click="vm.clearUp(email.password)" ng-change="vm.hasChanged(email.password)" ng-blur="vm.setMaskPassword(email.password)">
</div>
<div class="inline-help-config">
<inline-help help-title="// 'email_password' | tr //" content="// 'email_password_desc' | tr //"></inline-help>
</div>
</div>
<div class="form-group">
<label for="emailFrom" class="col-sm-3 control-label">// 'email_from' | tr //<span ng-if="vm.warning['email.from']">*</span></label>
<div class="col-sm-5">
<input id="emailFrom" type="text" class="form-control" ng-model="email.from.data" ng-disabled="!vm.editable['email.from']">
</div>
<div class="inline-help-config">
<inline-help help-title="// 'email_from' | tr //" content="// 'email_from_desc' | tr //"></inline-help>
</div>
</div>
<div class="form-group">
<label for="emailSSL" class="col-sm-3 control-label">// 'email_ssl' | tr //<span ng-if="vm.warning['email.SSL']">*</span></label>
<div class="col-sm-5">
<select class="form-control" ng-model="email.SSL.data" ng-options="r as r.name for r in vm.toggleBooleans track by r.value" ng-disabled="!vm.editable['email.SSL']"></select>
</div>
<div class="inline-help-config">
<inline-help help-title="// 'email_ssl' | tr //" content="// 'email_ssl_desc' | tr //"></inline-help>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-5">
<div class="pull-right">
<button type="submit" class="btn btn-link" ng-if="vm.changed" ng-click="vm.undo()">// 'undo' | tr //</button>
<button type="submit" class="btn btn-success" ng-disabled="emailForm.$invalid" ng-click="vm.saveEmailConf(email)">// 'save' | tr //</button>
</div>
</div>
</div>
</form>
</div>
<div class="form-group">
<label for="ldapUrl" class="col-sm-3 control-label">LDAP URL:</label>
<div class="col-sm-7">
<input type="text" class="form-control" id="ldapUrl" ng-model="system.ldapUrl" ng-model-options="{ updateOn: 'blur' }" ng-value="vm.system.ldapUrl" name="uLdapUrl" required>
<div ng-messages="form.$dirty && form.uLdapUrl.$error">
<span ng-message="required">LDAP URL is required.</span>
<div role="tabpanel" class="tab-pane" id="system">
<form name="systemForm" class="form-horizontal" no-validate autocomplete="off">
<div class="form-group">
<label for="projectCreationRestriction" class="col-sm-3 control-label">// 'project_creation_restriction' | tr //<span ng-if="vm.warning['system.projectCreationRestriction']">*</span></label>
<div class="col-sm-5">
<select id="projectCreationRestriction" class="form-control" ng-model="system.projectCreationRestriction.data" ng-options="r as r.name for r in vm.toggleCustoms track by r.value" ng-disabled="!vm.editable['system.projectCreationRestriction']"></select>
</div>
<div class="inline-help-config">
<inline-help help-title="// 'project_creation_restriction' | tr //" content="// 'project_creation_restriction_desc' | tr //"></inline-help>
</div>
</div>
</div>
<div class="form-group">
<label for="verifyRemoteCert" class="col-sm-3 control-label">// 'verify_remote_cert' | tr //<span ng-if="vm.warning['system.verifyRemoteCert']">*</span></label>
<div class="col-sm-5">
<select id="verifyRemoteCert" class="form-control" ng-model="system.verifyRemoteCert.data" ng-options="r as r.name for r in vm.toggleBooleans track by r.value" ng-disabled="!vm.editable['system.verifyRemoteCert']"></select>
</div>
<div class="inline-help-config">
<inline-help help-title="// 'verify_remote_cert' | tr //" content="// 'verify_remote_cert_desc' | tr //"></inline-help>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-5">
<div class="pull-right">
<button type="submit" class="btn btn-link" ng-if="vm.changed" ng-click="vm.undo()">// 'undo' | tr //</button>
<button type="submit" class="btn btn-success" ng-disabled="systemForm.$invalid" ng-click="vm.saveSystemConf(system)">// 'save' | tr //</button>
</div>
</div>
</div>
</form>
</div>
</div>
<div class="col-md-12">
<h5>Registration</h5>
<hr/>
</div>
<div class="col-md-12 col-md-off-set-1 main-content">
<div class="form-group">
<label for="registration" class="col-sm-3 control-label">Registration:</label>
<div class="col-sm-7">
<select class="form-control" ng-model="vm.currentRegistration" ng-options="r as r.name for r in vm.registrationOptions track by r.value" ng-click="vm.selectRegistration()"></select>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-7 col-md-10">
<input type="submit" class="btn btn-primary" ng-disabled="form.$invalid" value="Save">
<input type="submit" class="btn btn-default" ng-click="vm.cancel()" value="Cancel">
</div>
</div>
</div>
</form>
</div>

View File

@ -18,51 +18,365 @@
angular
.module('harbor.system.management')
.constant('defaultPassword', '12345678')
.directive('configuration', configuration);
ConfigurationController.$inject = [];
ConfigurationController.$inject = ['$scope', 'ConfigurationService', 'defaultPassword', '$filter', 'trFilter'];
function ConfigurationController() {
function ConfigurationController($scope, ConfigurationService, defaultPassword, $filter, trFilter) {
var vm = this;
vm.registrationOptions = [
{
'name': 'on',
'value': true
vm.toggleBooleans = [
{
'name': 'True',
'value': true
},
{
'name': 'off',
'name': 'False',
'value': false
}
];
vm.currentRegistration = {
'name': 'on',
'value': true
vm.toggleCustoms = [
{
'name': 'Admin Only',
'value': 'adminonly',
},
{
'name': 'Everyone',
'value': 'everyone'
}
];
vm.supportedAuths = [
{
'name': 'DB auth',
'value': 'db_auth'
},
{
'name': 'LDAP auth',
'value': 'ldap_auth'
}
];
var confKeyDefinitions = {
'auth_mode': { type: 'auth', attr: 'authMode' },
'self_registration': { type: 'auth', attr: 'selfRegistration' },
'ldap_url': { type: 'auth', attr: 'ldapURL' },
'ldap_search_dn': { type: 'auth', attr: 'ldapSearchDN' },
'ldap_search_password': { type: 'auth', attr: 'ldapSearchPassword' },
'ldap_base_dn': { type: 'auth', attr: 'ldapBaseDN' },
'ldap_uid': { type: 'auth', attr: 'ldapUID' },
'ldap_filter': { type: 'auth', attr: 'ldapFilter' },
'ldap_timeout': { type: 'auth', attr: 'ldapConnectionTimeout' },
'ldap_scope': { type: 'auth', attr: 'ldapScope' },
'email_host': { type: 'email', attr: 'server' },
'email_port': { type: 'email', attr: 'serverPort' },
'email_username': { type: 'email', attr: 'username' },
'email_password': { type: 'email', attr: 'password' },
'email_from': { type: 'email', attr: 'from' },
'email_ssl': { type: 'email', attr: 'SSL' },
'project_creation_restriction': { type: 'system', attr: 'projectCreationRestriction' },
'verify_remote_cert': { type: 'system', attr: 'verifyRemoteCert' }
};
vm.changeSettings = changeSettings;
$scope.auth = {};
$scope.email = {};
$scope.system = {};
vm.selectRegistration = selectRegistration;
vm.retrieve = retrieve;
function selectRegistration() {
vm.saveAuthConf = saveAuthConf;
vm.saveEmailConf = saveEmailConf;
vm.saveSystemConf = saveSystemConf;
vm.gatherUpdateItems = gatherUpdateItems;
vm.clearUp = clearUp;
vm.hasChanged = hasChanged;
vm.setMaskPassword = setMaskPassword;
vm.undo = undo;
vm.pingLDAP = pingLDAP;
vm.pingTIP = false;
vm.isError = false;
vm.pingMessage = '';
vm.retrieve();
function retrieve() {
vm.ldapSearchPasswordChanged = false;
vm.emailPasswordChanged = false;
vm.changedItems = {};
vm.updatedItems = {};
vm.warning = {};
vm.editable = {};
ConfigurationService
.get()
.then(getConfigurationSuccess, getConfigurationFailed);
}
function getConfigurationSuccess(response) {
var data = response.data || [];
for(var key in data) {
var mappedDef = keyMapping(key);
if(mappedDef) {
$scope[mappedDef['type']][mappedDef['attr']] = { 'target': mappedDef['type'] + '.' + mappedDef['attr'], 'data': valueMapping(data[key]['value']) };
$scope.$watch(mappedDef['type'] + '.' + mappedDef['attr'], onChangedCallback, true);
$scope[mappedDef['type']][mappedDef['attr']]['origin'] = { 'target': mappedDef['type'] + '.' + mappedDef['attr'], 'data': valueMapping(data[key]['value']) };
vm.editable[mappedDef['type'] + '.' + mappedDef['attr']] = data[key]['editable'];
}
}
$scope.auth.ldapSearchPassword = { 'target': 'auth.ldapSearchPassword', 'data': defaultPassword};
$scope.email.password = { 'target': 'email.password', 'data': defaultPassword};
$scope.$watch('auth.ldapSearchPassword', onChangedCallback, true);
$scope.$watch('email.password', onChangedCallback, true);
$scope.auth.ldapSearchPassword.actual = { 'target': 'auth.ldapSearchPassword', 'data': ''};
$scope.email.password.actual = { 'target': 'email.password', 'data': ''};
}
function keyMapping(confKey) {
for (var key in confKeyDefinitions) {
if (confKey === key) {
return confKeyDefinitions[key];
}
}
return null;
}
function changeSettings(system) {
console.log(system);
function valueMapping(value) {
switch(value) {
case true:
return vm.toggleBooleans[0];
case false:
return vm.toggleBooleans[1];
case 'db_auth':
return vm.supportedAuths[0];
case 'ldap_auth':
return vm.supportedAuths[1];
case 'adminonly':
return vm.toggleCustoms[0];
case 'everyone':
return vm.toggleCustoms[1];
default:
return value;
}
}
function onChangedCallback(current, previous) {
if(!angular.equals(current, previous)) {
var compositeKey = current.target.split(".");
vm.changed = false;
var changedData = {};
switch(current.target) {
case 'auth.ldapSearchPassword':
if(vm.ldapSearchPasswordChanged) {
vm.changed = true;
changedData = $scope.auth.ldapSearchPassword.actual.data;
}
break;
case 'email.password':
if(vm.emailPasswordChanged) {
vm.changed = true;
changedData = $scope.email.password.actual.data;
}
break;
default:
if(!angular.equals(current.data, $scope[compositeKey[0]][compositeKey[1]]['origin']['data'])) {
vm.changed = true;
changedData = current.data;
}
}
if(vm.changed) {
vm.changedItems[current.target] = changedData;
vm.warning[current.target] = true;
} else {
delete vm.changedItems[current.target];
vm.warning[current.target] = false;
}
}
}
function getConfigurationFailed(response) {
console.log('Failed to get configurations.');
}
function updateConfigurationSuccess(response) {
$scope.$emit('modalTitle', $filter('tr')('update_configuration_title', []));
$scope.$emit('modalMessage', $filter('tr')('successful_update_configuration', []));
var emitInfo = {
'confirmOnly': true,
'contentType': 'text/plain',
'action' : function() {
vm.retrieve();
}
};
$scope.$emit('raiseInfo', emitInfo);
console.log('Updated system configuration successfully.');
}
function updateConfigurationFailed() {
$scope.$emit('modalTitle', $filter('tr')('update_configuration_title', []));
$scope.$emit('modalMessage', $filter('tr')('failed_to_update_configuration', []));
$scope.$emit('raiseError', true);
console.log('Failed to update system configurations.');
}
function gatherUpdateItems() {
vm.updatedItems = {};
for(var key in confKeyDefinitions) {
var value = confKeyDefinitions[key];
var compositeKey = value.type + '.' + value.attr;
for(var itemKey in vm.changedItems) {
var item = vm.changedItems[itemKey];
if (compositeKey === itemKey) {
(typeof item === 'object' && item) ? vm.updatedItems[key] = ((typeof item.value === 'boolean') ? Number(item.value) + '' : item.value) : vm.updatedItems[key] = String(item) || '';
}
}
}
}
function saveAuthConf(auth) {
vm.gatherUpdateItems();
console.log('auth changed:' + angular.toJson(vm.updatedItems));
ConfigurationService
.update(vm.updatedItems)
.then(updateConfigurationSuccess, updateConfigurationFailed);
}
function saveEmailConf(email) {
vm.gatherUpdateItems();
console.log('email changed:' + angular.toJson(vm.updatedItems));
ConfigurationService
.update(vm.updatedItems)
.then(updateConfigurationSuccess, updateConfigurationFailed);
}
function saveSystemConf(system) {
vm.gatherUpdateItems();
console.log('system changed:' + angular.toJson(vm.updatedItems));
ConfigurationService
.update(vm.updatedItems)
.then(updateConfigurationSuccess, updateConfigurationFailed);
}
function clearUp(input) {
switch(input.target) {
case 'auth.ldapSearchPassword':
$scope.auth.ldapSearchPassword.data = '';
break;
case 'email.password':
$scope.email.password.data = '';
break;
}
}
function hasChanged(input) {
switch(input.target) {
case 'auth.ldapSearchPassword':
vm.ldapSearchPasswordChanged = true;
$scope.auth.ldapSearchPassword.actual.data = input.data;
break;
case 'email.password':
vm.emailPasswordChanged = true;
$scope.email.password.actual.data = input.data;
break;
}
}
function setMaskPassword(input) {
input.data = defaultPassword;
}
function undo() {
vm.retrieve();
}
function pingLDAP(auth) {
var keyset = [
{'name': 'ldapURL' , 'attr': 'ldap_url'},
{'name': 'ldapSearchDN', 'attr': 'ldap_search_dn'},
{'name': 'ldapSearchPassword' , 'attr': 'ldap_search_password'},
{'name': 'ldapConnectionTimeout', 'attr': 'ldap_connection_timeout'}
];
var ldapConf = {};
for(var i = 0; i < keyset.length; i++) {
var key = keyset[i];
var value;
if(key.name === 'ldapSearchPassword') {
value = auth[key.name]['actual']['data'];
}else {
value = auth[key.name]['data'];
}
ldapConf[key.attr] = value;
}
vm.pingMessage = $filter('tr')('pinging_target');
vm.pingTIP = true;
vm.isError = false;
ConfigurationService
.pingLDAP(ldapConf)
.then(pingLDAPSuccess, pingLDAPFailed);
}
function pingLDAPSuccess(response) {
vm.pingTIP = false;
vm.pingMessage = $filter('tr')('successful_ping_target');
}
function pingLDAPFailed(response) {
vm.isError = true;
vm.pingTIP = false;
vm.pingMessage = $filter('tr')('failed_to_ping_target');
console.log('Failed to ping LDAP target:' + response.data);
}
}
function configuration() {
configuration.$inject = ['$filter', 'trFilter'];
function configuration($filter, trFilter) {
var directive = {
'restrict': 'E',
'templateUrl': '/static/resources/js/components/system-management/configuration.directive.html',
'scope': true,
'link': link,
'controller': ConfigurationController,
'controllerAs': 'vm',
'bindToController': true
};
return directive;
function link(scope, element, attrs, ctrl) {
element.find('#ulTabHeader a').on('click', function(e) {
e.preventDefault();
ctrl.gatherUpdateItems();
if(!angular.equals(ctrl.updatedItems, {})) {
var emitInfo = {
'confirmOnly': true,
'contentType': 'text/html',
'action' : function() {
return;
}
};
scope.$emit('modalTitle', $filter('tr')('caution'));
scope.$emit('modalMessage', $filter('tr')('please_save_changes'));
scope.$emit('raiseInfo', emitInfo);
scope.$apply();
e.stopPropagation();
}else{
$(this).tab('show');
}
});
element.find('#ulTabHeader a:first').trigger('click');
}
}
})();

View File

@ -29,6 +29,7 @@
switch(currentTarget) {
case 'destinations':
case 'replication':
case 'configuration':
$location.path('/' + currentTarget);
vm.target = currentTarget;
break;

View File

@ -12,10 +12,8 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<ul class="switch-pane-admin-options" role="tablist">
<ul class="switch-pane-admin-options" role="tablist" ng-style="vm.customPos">
<li><a tag="destinations" href="#/destinations">// 'destination' | tr //</a><span class="gutter">|</span></li>
<li><a tag="replication" href="#/replication">// 'replication' | tr //</a><span class="gutter">
<!--
<li><a tag="configuration" href="#/configuration">Configuration</a></li>
-->
<li><a tag="replication" href="#/replication">// 'replication' | tr //</a><span class="gutter">|</span>
<li><a tag="configuration" href="#/configuration">// 'configuration' | tr //</a></li>
</ul>

View File

@ -27,7 +27,9 @@
vm.path = $location.path();
}
function navigationAdminOptions() {
navigationAdminOptions.$inject = ['I18nService'];
function navigationAdminOptions(I18nService) {
var directive = {
'restrict': 'E',
'templateUrl': '/static/resources/js/layout/navigation/navigation-admin-options.directive.html',
@ -44,7 +46,14 @@
function link(scope, element, attrs, ctrl) {
var visited = ctrl.path.substring(1);
console.log('visited:' + visited);
var lang = I18nService().getCurrentLanguage();
ctrl.customPos = {};
if(lang === 'zh-CN') {
ctrl.customPos = {'position': 'relative', 'left': '14%'};
}
if(visited) {
element.find('a[tag="' + visited + '"]').addClass('active');
}else{

View File

@ -35,7 +35,9 @@
vm.path = $location.path();
}
function navigationDetails() {
navigationDetails.$inject = ['I18nService'];
function navigationDetails(I18nService) {
var directive = {
restrict: 'E',
templateUrl: '/navigation_detail?timestamp=' + new Date().getTime(),
@ -53,6 +55,13 @@
function link(scope, element, attrs, ctrl) {
var lang = I18nService().getCurrentLanguage();
ctrl.customPos = {};
if(lang === 'zh-CN') {
ctrl.customPos = {'position': 'relative', 'left': '8%'};
}
var visited = ctrl.path.substring(1);
if(visited) {

View File

@ -289,5 +289,57 @@ var locale_messages = {
'confirm_to_toggle_enabled_policy': 'After enabling the replication policy, all repositories under the project will be replicated to the destination registry. Please confirm to continue.',
'confirm_to_toggle_disabled_policy_title': 'Disable Policy',
'confirm_to_toggle_disabled_policy': 'After disabling the policy, all unfinished replication jobs of this policy will be stopped and canceled. Please confirm to continue.',
'begin_date_is_later_than_end_date': 'Begin date should not be later than end date.'
'begin_date_is_later_than_end_date': 'Begin date should not be later than end date.',
'configuration': 'Configuration',
'authentication': 'Authentication',
'email_settings': 'Email Settings',
'system_settings': 'System Settings',
'authentication_mode': 'Authentication Mode',
'authentication_mode_desc': '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.',
'self_registration': 'Self Registration',
'self_registration_desc': '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.',
'ldap_url': 'LDAP URL',
'ldap_url_desc': 'The URL of an LDAP/AD server.',
'ldap_search_dn': 'LDAP Search DN',
'ldap_search_dn_desc': 'A user\'s DN who has the permission to search the LDAP/AD server. Leave blank if your LDAP/AD server supports anonymous search, otherwise you should configure this DN and LDAP Search Password.',
'ldap_search_password': 'LDAP Search Password',
'ldap_search_password_desc': 'The password of the user for LDAP search. Leave blank if your LDAP/AD server supports anonymous search.',
'ldap_base_dn': 'LDAP Base DN',
'ldap_base_dn_desc': 'The base DN of a node from which to look up a user for authentication. The search scope includes subtree of the node.',
'ldap_uid': 'LDAP UID',
'ldap_uid_desc': '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.',
'ldap_filter': 'LDAP Filter',
'ldap_filter_desc': 'Search filter for LDAP/AD, make sure the syntax of the filter is correct.',
'ldap_connection_timeout': 'LDAP Connection Timeout(sec.)',
'ldap_scope': 'LDAP Scope',
'ldap_scope_desc': 'The scope to search for users.(1-LDAP_SCOPE_BASE, 2-LDAP_SCOPE_ONELEVEL, 3-LDAP_SCOPE_SUBTREE)',
'email_server': 'Email Server',
'email_server_desc': 'The mail server to send out emails to reset password.',
'email_server_port': 'Email Server Port',
'email_server_port_desc': 'The port of mail server.',
'email_username': 'Email Username',
'email_username_desc': 'The user from whom the password reset email is sent. Usually this is a system email address.',
'email_password': 'Email Password',
'email_password_desc': 'The password of the user from whom the password reset email is sent.',
'email_from': 'Email From',
'email_from_desc': 'The name of the email sender.',
'email_ssl': 'Email SSL',
'email_ssl_desc': 'Whether to enable secure mail transmission.',
'project_creation_restriction': 'Project Creation Restriction',
'project_creation_restriction_desc': '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.',
'verify_remote_cert': 'Verify Remote Cert.',
'verify_remote_cert_desc': 'Determine whether the image replication should verify the certificate of a remote Harbor registry. Set this flag to off when the remote registry uses a self-signed or untrusted certificate.',
'max_job_workers': 'Max Job Workers',
'max_job_workers_desc': 'Maximum number of job workers in job service.',
'please_save_changes': 'Please save changes before leaving this page.',
'undo': 'Undo',
'invalid_port_number': 'Invalid port number.',
'max_job_workers_is_required': 'Max job workers number is required.',
'timeout_is_required': 'Timeout value is required.',
'invalid_timeout': 'Invalid timeout value.',
'ldap_scope_is_required': 'Scope is required.',
'invalid_ldap_scope': 'Invalid Scope value.',
'update_configuration_title': 'Update Configuration(s)',
'successful_update_configuration': 'Configuration(s) updated successfully.',
'failed_to_update_configuration': 'Failed to update configuration.'
};

View File

@ -289,5 +289,57 @@ var locale_messages = {
'confirm_to_toggle_enabled_policy': '启用策略后,该项目下的所有镜像仓库将复制到目标实例。请确认继续。',
'confirm_to_toggle_disabled_policy_title': '停用策略',
'confirm_to_toggle_disabled_policy': '停用策略后,所有未完成的复制任务将被终止和取消。请确认继续。',
'begin_date_is_later_than_end_date': '起始日期不能晚于结束日期。'
'begin_date_is_later_than_end_date': '起始日期不能晚于结束日期。',
'configuration': '设置',
'authentication': '认证设置',
'email_settings': '邮箱设置',
'system_settings': '系统设置',
'authentication_mode': '认证模式',
'authentication_mode_desc': '默认的认证模式是: 数据库认证。 当用户信息存储在LDAP或AD服务器时应设置为LDAP认证模式。 注意:该选项只能被设置一次。',
'self_registration': '自注册',
'self_registration_desc': '确定是否允许或禁止用户自注册。 关闭该项会禁用Harbor用户注册。 当用户数据存储在LDAP或AD时该选项无效。 ',
'ldap_url': 'LDAP URL',
'ldap_url_desc': 'LDAP/AD服务URL。',
'ldap_search_dn': 'LDAP Search DN',
'ldap_search_dn_desc': '提供一个拥有检索LDAP/AD权限的用户DN。 如果LDAP/AD允许匿名访问该项可以置空 否则需提供用户DN和密码。',
'ldap_search_password': 'LDAP Search 密码',
'ldap_search_password_desc': '检索LDAP的用户密码。 如果LDAP/AD允许匿名访问该项可以置空。',
'ldap_base_dn': 'LDAP Base DN',
'ldap_base_dn_desc': '用于查询认证用户的基本DN节点。 检索范围包含子树节点。',
'ldap_uid': 'LDAP UID',
'ldap_uid_desc': '该属性用于检索匹配用户, 可以是uid cn, Email, sAMAccountName或是其他属性 取决于LDAP/AD服务。',
'ldap_filter': 'LDAP 过滤器',
'ldap_filter_desc': '用于过滤LDAP/AP检索 请确保正确语法形式。',
'ldap_connection_timeout': 'LDAP连接超时(秒)',
'ldap_scope': 'LDAP检索范围',
'ldap_scope_desc': '指定用户检索范围。(1-LDAP_SCOPE_BASE, 2-LDAP_SCOPE_ONELEVEL, 3-LDAP_SCOPE_SUBTREE)',
'email_server': '邮箱服务地址',
'email_server_desc': '用以发送重置密码的邮箱服务地址。',
'email_server_port': '邮箱服务端口号',
'email_server_port_desc': '邮箱服务端口号',
'email_username': '邮箱用户名',
'email_username_desc': '发送重置密码邮箱的用户。 通常是系统邮箱地址。',
'email_password': '邮箱密码',
'email_password_desc': '发送重置密码邮箱的密码。',
'email_from': '寄信人',
'email_from_desc': '邮箱寄信人名。',
'email_ssl': '邮箱SSL',
'email_ssl_desc': '是否启用安全邮箱传输。',
'project_creation_restriction': '项目创建约束',
'project_creation_restriction_desc': '此标志用于控制用户是否有权限创建项目。 默认允许任何人创建项目, 当设置为"adminonly"只允许管理员创建项目。',
'verify_remote_cert': '验证远程服务证书',
'verify_remote_cert_desc': '确定镜像复制是否检查远程Harbor服务证书。 当使用非受信或自签名证书时应该关闭该检查。',
'max_job_workers': '最大任务调度数',
'max_job_workers_desc': '任务调度服务最大调度数。',
'please_save_changes': '请在离开此页之前保存。',
'undo': '撤销',
'invalid_port_number': '无效的端口号。',
'max_job_workers_is_required': '最大任务数为必填项。',
'timeout_is_required': '超时时间为必填项。',
'invalid_timeout': '无效的超时时间。',
'ldap_scope_is_required': '范围值为必填项。',
'invalid_ldap_scope': '无效的范围值。',
'update_configuration_title': '修改设置',
'successful_update_configuration': '修改设置成功。',
'failed_to_update_configuration': '修改设置失败。'
};

View File

@ -0,0 +1,43 @@
/*
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.
*/
(function() {
'use strict';
angular.module('harbor.services.system.info')
.service('ConfigurationService', ConfigurationService);
ConfigurationService.$inject = ['$http', '$q', '$timeout'];
function ConfigurationService($http, $q, $timeout) {
this.get = get;
this.update = update;
this.pingLDAP = pingLDAP;
function get() {
return $http.get('/api/configurations');
}
function update(updates) {
return $http.put('/api/configurations', updates);
}
function pingLDAP(ldapConf) {
return $http
.post('/api/ldap/ping', ldapConf);
}
}
})();

View File

@ -1,5 +1,6 @@
package main
/*
import (
"testing"
)
@ -7,3 +8,4 @@ import (
func TestMain(t *testing.T) {
}
*/

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