package vmware/registry into offline package

This commit is contained in:
Tan Jiang 2017-03-23 12:36:36 +08:00
commit 980101eab5
63 changed files with 1712 additions and 1185 deletions

5
.gitignore vendored
View File

@ -10,7 +10,10 @@ src/common/dao/dao.test
*.pyc
jobservice/test
src/ui/static/dist/
src/ui/static/*.html
src/ui/static/*.bundle.js
src/ui/static/*.bundle.js.map
src/ui/static/harbor-log.*.png
src/ui_ng/coverage/
src/ui_ng/dist/

View File

@ -76,6 +76,8 @@ before_script:
script:
- sudo mkdir -p /harbor_storage/ca_download
- sudo mv ./tests/ca.crt /harbor_storage/ca_download
- sudo mkdir -p /harbor
- sudo mv ./VERSION /harbor/VERSION
- sudo service mysql stop
- sudo ./tests/testprepare.sh
- docker-compose -f ./make/docker-compose.test.yml up -d
@ -98,6 +100,7 @@ script:
- sudo make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=danieljt/harbor-clarity-base:0.8.4 NOTARYFLAG=true
- docker ps
- ./tests/notarytest.sh
- go run tests/startuptest.go https://localhost/
- go run tests/userlogintest.go -name ${HARBOR_ADMIN} -passwd ${HARBOR_ADMIN_PASSWD}

888
Makefile
View File

@ -1,445 +1,443 @@
# Makefile for Harbor project
#
# Targets:
#
# all: prepare env, compile binarys, build images and install images
# prepare: prepare env
# 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_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 baseimage
#
# install: include compile binarys, build images, prepare specific \
# version composefile and startup Harbor instance
#
# start: startup Harbor instance
#
# down: shutdown Harbor instance
#
# package_online:
# prepare online install package
# for example: make package_online -e DEVFLAG=false\
# REGISTRYSERVER=reg-bj.eng.vmware.com \
# REGISTRYPROJECTNAME=harborrelease
#
# package_offline:
# prepare offline install package
#
# pushimage: push Harbor images to specific registry server
# for example: make pushimage -e DEVFLAG=false REGISTRYUSER=admin \
# REGISTRYPASSWORD=***** \
# REGISTRYSERVER=reg-bj.eng.vmware.com/ \
# REGISTRYPROJECTNAME=harborrelease
# note**: need add "/" on end of REGISTRYSERVER. If not setting \
# this value will push images directly to dockerhub.
# make pushimage -e DEVFLAG=false REGISTRYUSER=vmware \
# REGISTRYPASSWORD=***** \
# REGISTRYPROJECTNAME=vmware
#
# clean: remove binary, Harbor images, specific version docker-compose \
# file, specific version tag and online/offline install package
# cleanbinary: remove adminserver, ui and jobservice binary
# cleanimage: remove Harbor images
# cleandockercomposefile:
# remove specific version docker-compose
# cleanversiontag:
# cleanpackageremove specific version tag
# cleanpackage: remove online/offline install package
#
# other example:
# clean specific version binarys and images:
# make clean -e VERSIONTAG=[TAG]
# note**: If commit new code to github, the git commit TAG will \
# change. Better use this commond clean previous images and \
# files with specific TAG.
# By default DEVFLAG=true, if you want to release new version of Harbor, \
# should setting the flag to false.
# make XXXX -e DEVFLAG=false
SHELL := /bin/bash
BUILDPATH=$(CURDIR)
MAKEPATH=$(BUILDPATH)/make
MAKEDEVPATH=$(MAKEPATH)/dev
SRCPATH=./src
TOOLSPATH=$(BUILDPATH)/tools
UIPATH=$(BUILDPATH)/src/ui
UINGPATH=$(BUILDPATH)/src/ui_ng
GOBASEPATH=/go/src/github.com/vmware
CHECKENVCMD=checkenv.sh
BASEIMAGE=photon
COMPILETAG=compile_normal
REGISTRYSERVER=
REGISTRYPROJECTNAME=vmware
DEVFLAG=true
NOTARYFLAG=false
REGISTRYVERSION=photon-2.6.0
NGINXVERSION=1.11.5
PHOTONVERSION=1.0
NOTARYVERSION=server-0.5.0
NOTARYSIGNERVERSION=signer-0.5.0
MARIADBVERSION=10.1.10
HTTPPROXY=
#clarity parameters
CLARITYIMAGE=danieljt/harbor-clarity-base[:tag]
CLARITYSEEDPATH=/clarity-seed
CLARITYBUILDSCRIPT=/entrypoint.sh
# docker parameters
DOCKERCMD=$(shell which docker)
DOCKERBUILD=$(DOCKERCMD) build
DOCKERRMIMAGE=$(DOCKERCMD) rmi
DOCKERPULL=$(DOCKERCMD) pull
DOCKERIMASES=$(DOCKERCMD) images
DOCKERSAVE=$(DOCKERCMD) save
DOCKERCOMPOSECMD=$(shell which docker-compose)
DOCKERTAG=$(DOCKERCMD) tag
# go parameters
GOCMD=$(shell which go)
GOBUILD=$(GOCMD) build
GOCLEAN=$(GOCMD) clean
GOINSTALL=$(GOCMD) install
GOTEST=$(GOCMD) test
GODEP=$(GOTEST) -i
GOFMT=gofmt -w
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
JOBSERVICESOURCECODE=$(SRCPATH)/jobservice
JOBSERVICEBINARYPATH=$(MAKEDEVPATH)/jobservice
JOBSERVICEBINARYNAME=harbor_jobservice
# prepare parameters
PREPAREPATH=$(TOOLSPATH)
PREPARECMD=prepare
# configfile
CONFIGPATH=$(MAKEPATH)
CONFIGFILE=harbor.cfg
# makefile
MAKEFILEPATH_PHOTON=$(MAKEPATH)/photon
# common dockerfile
DOCKERFILEPATH_COMMON=$(MAKEPATH)/common
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
DOCKERIMAGENAME_DB=vmware/harbor-db
# docker-compose files
DOCKERCOMPOSEFILEPATH=$(MAKEPATH)
DOCKERCOMPOSETPLFILENAME=docker-compose.tpl
DOCKERCOMPOSEFILENAME=docker-compose.yml
DOCKERCOMPOSENOTARYFILENAME=docker-compose.notary.yml
# version prepare
VERSIONFILEPATH=$(SRCPATH)/ui/views/sections
VERSIONFILENAME=header-content.htm
GITCMD=$(shell which git)
GITTAG=$(GITCMD) describe --tags
ifeq ($(DEVFLAG), true)
VERSIONTAG=dev
else
VERSIONTAG=$(shell $(GITTAG))
endif
SEDCMD=$(shell which sed)
# package
TARCMD=$(shell which tar)
ZIPCMD=$(shell which gzip)
DOCKERIMGFILE=harbor
HARBORPKG=harbor
# pushimage
PUSHSCRIPTPATH=$(MAKEPATH)
PUSHSCRIPTNAME=pushimage.sh
REGISTRYUSER=user
REGISTRYPASSWORD=default
version:
@if [ "$(DEVFLAG)" = "false" ] ; then \
$(SEDCMD) -i 's/version=\"{{.Version}}\"/version=\"$(VERSIONTAG)\"/' -i $(VERSIONFILEPATH)/$(VERSIONFILENAME) ; \
fi
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)
@echo "Done."
compile_jobservice:
@echo "compiling binary for jobservice..."
@$(GOBUILD) -o $(JOBSERVICEBINARYPATH)/$(JOBSERVICEBINARYNAME) $(JOBSERVICESOURCECODE)
@echo "Done."
compile_clarity:
@echo "compiling binary for clarity ui..."
@if [ "$(HTTPPROXY)" != "" ] ; then \
$(DOCKERCMD) run --rm -v $(UIPATH)/static:$(CLARITYSEEDPATH)/dist -v $(UINGPATH)/src:$(CLARITYSEEDPATH)/src $(CLARITYIMAGE) $(SHELL) $(CLARITYBUILDSCRIPT) -p $(HTTPPROXY); \
else \
$(DOCKERCMD) run --rm -v $(UIPATH)/static:$(CLARITYSEEDPATH)/dist -v $(UINGPATH)/src:$(CLARITYSEEDPATH)/src $(CLARITYIMAGE) $(SHELL) $(CLARITYBUILDSCRIPT); \
fi
@echo "Done."
compile_normal: compile_clarity compile_adminserver compile_ui compile_jobservice
compile_golangimage: compile_clarity
@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)
@$(DOCKERCMD) run --rm -v $(BUILDPATH):$(GOBUILDPATH) -w $(GOBUILDPATH_UI) $(GOBUILDIMAGE) $(GOIMAGEBUILD) -v -o $(GOBUILDMAKEPATH_UI)/$(UIBINARYNAME)
@echo "Done."
@echo "compiling binary for jobservice (golang image)..."
@$(DOCKERCMD) run --rm -v $(BUILDPATH):$(GOBUILDPATH) -w $(GOBUILDPATH_JOBSERVICE) $(GOBUILDIMAGE) $(GOIMAGEBUILD) -v -o $(GOBUILDMAKEPATH_JOBSERVICE)/$(JOBSERVICEBINARYNAME)
@echo "Done."
compile:check_environment $(COMPILETAG)
prepare:
@echo "preparing..."
@if [ "$(NOTARYFLAG)" = "true" ] ; then \
$(MAKEPATH)/$(PREPARECMD) --conf $(CONFIGPATH)/$(CONFIGFILE) --with-notary; \
else \
$(MAKEPATH)/$(PREPARECMD) --conf $(CONFIGPATH)/$(CONFIGFILE) ; \
fi
build_common: version
@echo "buildging db container for photon..."
@cd $(DOCKERFILEPATH_DB) && $(DOCKERBUILD) -f $(DOCKERFILENAME_DB) -t $(DOCKERIMAGENAME_DB):$(VERSIONTAG) .
@echo "Done."
build_photon: build_common
make -f $(MAKEFILEPATH_PHOTON)/Makefile build -e DEVFLAG=$(DEVFLAG)
build: build_$(BASEIMAGE)
modify_composefile:
@echo "preparing docker-compose file..."
@cp $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSETPLFILENAME) $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSEFILENAME)
@$(SEDCMD) -i 's/__version__/$(VERSIONTAG)/g' $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSEFILENAME)
install: compile build prepare modify_composefile start
package_online: modify_composefile
@echo "packing online package ..."
@cp -r make $(HARBORPKG)
@if [ -n "$(REGISTRYSERVER)" ] ; then \
$(SEDCMD) -i 's/image\: vmware/image\: $(REGISTRYSERVER)\/$(REGISTRYPROJECTNAME)/' \
$(HARBORPKG)/docker-compose.yml ; \
fi
@cp LICENSE $(HARBORPKG)/LICENSE
@cp NOTICE $(HARBORPKG)/NOTICE
@if [ "$(NOTARYFLAG)" = "true" ] ; then \
$(TARCMD) -zcvf harbor-online-installer-$(VERSIONTAG).tgz \
$(HARBORPKG)/common/templates $(HARBORPKG)/prepare \
$(HARBORPKG)/LICENSE $(HARBORPKG)/NOTICE \
$(HARBORPKG)/install.sh $(HARBORPKG)/$(DOCKERCOMPOSEFILENAME) \
$(HARBORPKG)/harbor.cfg $(HARBORPKG)/$(DOCKERCOMPOSENOTARYFILENAME); \
else \
$(TARCMD) -zcvf harbor-online-installer-$(VERSIONTAG).tgz \
$(HARBORPKG)/common/templates $(HARBORPKG)/prepare \
$(HARBORPKG)/LICENSE $(HARBORPKG)/NOTICE \
$(HARBORPKG)/install.sh $(HARBORPKG)/$(DOCKERCOMPOSEFILENAME) \
$(HARBORPKG)/harbor.cfg ; \
fi
@rm -rf $(HARBORPKG)
@echo "Done."
package_offline: compile build modify_composefile
@echo "packing offline package ..."
@cp -r make $(HARBORPKG)
@cp LICENSE $(HARBORPKG)/LICENSE
@cp NOTICE $(HARBORPKG)/NOTICE
@echo "pulling nginx and registry..."
@$(DOCKERPULL) registry:$(REGISTRYVERSION)
@$(DOCKERPULL) nginx:$(NGINXVERSION)
@if [ "$(NOTARYFLAG)" = "true" ] ; then \
echo "pulling notary and mariadb..."; \
$(DOCKERPULL) vmware/notary-photon:$(NOTARYVERSION); \
$(DOCKERPULL) vmware/notary-photon:$(NOTARYSIGNERVERSION); \
$(DOCKERPULL) mariadb:$(MARIADBVERSION); \
fi
@echo "saving harbor docker image"
@if [ "$(NOTARYFLAG)" = "true" ] ; then \
$(DOCKERSAVE) -o $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tgz \
$(DOCKERIMAGENAME_ADMINSERVER):$(VERSIONTAG) \
$(DOCKERIMAGENAME_UI):$(VERSIONTAG) \
$(DOCKERIMAGENAME_LOG):$(VERSIONTAG) \
$(DOCKERIMAGENAME_DB):$(VERSIONTAG) \
$(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG) \
nginx:$(NGINXVERSION) vmware/registry:$(REGISTRYVERSION) photon:$(PHOTONVERSION) \
vmware/notary-photon:$(NOTARYVERSION) vmware/notary-photon:$(NOTARYSIGNERVERSION) mariadb:$(MARIADBVERSION); \
else \
$(DOCKERSAVE) -o $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tgz \
$(DOCKERIMAGENAME_ADMINSERVER):$(VERSIONTAG) \
$(DOCKERIMAGENAME_UI):$(VERSIONTAG) \
$(DOCKERIMAGENAME_LOG):$(VERSIONTAG) \
$(DOCKERIMAGENAME_DB):$(VERSIONTAG) \
$(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG) \
nginx:$(NGINXVERSION) registry:$(REGISTRYVERSION) photon:$(PHOTONVERSION) ; \
fi
@if [ "$(NOTARYFLAG)" = "true" ] ; then \
$(TARCMD) -zcvf harbor-offline-installer-$(VERSIONTAG).tgz \
$(HARBORPKG)/common/templates $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tgz \
$(HARBORPKG)/prepare $(HARBORPKG)/NOTICE \
$(HARBORPKG)/LICENSE $(HARBORPKG)/install.sh \
$(HARBORPKG)/harbor.cfg $(HARBORPKG)/$(DOCKERCOMPOSEFILENAME) \
$(HARBORPKG)/$(DOCKERCOMPOSENOTARYFILENAME) ; \
else \
$(TARCMD) -zcvf harbor-offline-installer-$(VERSIONTAG).tgz \
$(HARBORPKG)/common/templates $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tgz \
$(HARBORPKG)/prepare $(HARBORPKG)/NOTICE \
$(HARBORPKG)/LICENSE $(HARBORPKG)/install.sh \
$(HARBORPKG)/harbor.cfg $(HARBORPKG)/$(DOCKERCOMPOSEFILENAME) ; \
fi
@rm -rf $(HARBORPKG)
@echo "Done."
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)
@$(DOCKERRMIMAGE) $(REGISTRYSERVER)$(DOCKERIMAGENAME_UI):$(VERSIONTAG)
@$(DOCKERTAG) $(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG) $(REGISTRYSERVER)$(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG)
@$(PUSHSCRIPTPATH)/$(PUSHSCRIPTNAME) $(REGISTRYSERVER)$(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG) \
$(REGISTRYUSER) $(REGISTRYPASSWORD) $(REGISTRYSERVER)
@$(DOCKERRMIMAGE) $(REGISTRYSERVER)$(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG)
@$(DOCKERTAG) $(DOCKERIMAGENAME_LOG):$(VERSIONTAG) $(REGISTRYSERVER)$(DOCKERIMAGENAME_LOG):$(VERSIONTAG)
@$(PUSHSCRIPTPATH)/$(PUSHSCRIPTNAME) $(REGISTRYSERVER)$(DOCKERIMAGENAME_LOG):$(VERSIONTAG) \
$(REGISTRYUSER) $(REGISTRYPASSWORD) $(REGISTRYSERVER)
@$(DOCKERRMIMAGE) $(REGISTRYSERVER)$(DOCKERIMAGENAME_LOG):$(VERSIONTAG)
@$(DOCKERTAG) $(DOCKERIMAGENAME_DB):$(VERSIONTAG) $(REGISTRYSERVER)$(DOCKERIMAGENAME_DB):$(VERSIONTAG)
@$(PUSHSCRIPTPATH)/$(PUSHSCRIPTNAME) $(REGISTRYSERVER)$(DOCKERIMAGENAME_DB):$(VERSIONTAG) \
$(REGISTRYUSER) $(REGISTRYPASSWORD) $(REGISTRYSERVER)
@$(DOCKERRMIMAGE) $(REGISTRYSERVER)$(DOCKERIMAGENAME_DB):$(VERSIONTAG)
start:
@echo "loading harbor images..."
@if [ "$(NOTARYFLAG)" = "true" ] ; then \
$(DOCKERCOMPOSECMD) -f $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSEFILENAME) -f $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSENOTARYFILENAME) up -d ; \
else \
$(DOCKERCOMPOSECMD) -f $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSEFILENAME) up -d ; \
fi
@echo "Start complete. You can visit harbor now."
down:
@echo "Please make sure to set -e NOTARYFLAG=true if you are using Notary in Harbor, otherwise the Notary containers cannot be stop automaticlly."
@while [ -z "$$CONTINUE" ]; do \
read -r -p "Type anything but Y or y to exit. [Y/N]: " CONTINUE; \
done ; \
[ $$CONTINUE = "y" ] || [ $$CONTINUE = "Y" ] || (echo "Exiting."; exit 1;)
@echo "stoping harbor instance..."
@if [ "$(NOTARYFLAG)" = "true" ] ; then \
$(DOCKERCOMPOSECMD) -f $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSEFILENAME) -f $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSENOTARYFILENAME) down ; \
else \
$(DOCKERCOMPOSECMD) -f $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSEFILENAME) down ; \
fi
@echo "Done."
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)
- $(DOCKERRMIMAGE) -f $(DOCKERIMAGENAME_LOG):$(VERSIONTAG)
# - $(DOCKERRMIMAGE) -f registry:$(REGISTRYVERSION)
# - $(DOCKERRMIMAGE) -f nginx:1.11.5
cleandockercomposefile:
@echo "cleaning $(DOCKERCOMPOSEFILEPATH)/docker-compose.yml"
@if [ -f $(DOCKERCOMPOSEFILEPATH)/docker-compose.yml ] ; then rm $(DOCKERCOMPOSEFILEPATH)/docker-compose.yml ; fi
cleanversiontag:
@echo "cleaning version TAG"
@$(SEDCMD) -i 's/version=\"$(VERSIONTAG)\"/version=\"{{.Version}}\"/' -i $(VERSIONFILEPATH)/$(VERSIONFILENAME)
cleanpackage:
@echo "cleaning harbor install package"
@if [ -d $(BUILDPATH)/harbor ] ; then rm -rf $(BUILDPATH)/harbor ; fi
@if [ -f $(BUILDPATH)/harbor-online-installer-$(VERSIONTAG).tgz ] ; \
then rm $(BUILDPATH)/harbor-online-installer-$(VERSIONTAG).tgz ; fi
@if [ -f $(BUILDPATH)/harbor-offline-installer-$(VERSIONTAG).tgz ] ; \
then rm $(BUILDPATH)/harbor-offline-installer-$(VERSIONTAG).tgz ; fi
.PHONY: cleanall
cleanall: cleanbinary cleanimage cleandockercomposefile cleanversiontag cleanpackage
clean:
@echo " make cleanall: remove binary, Harbor images, specific version docker-compose"
@echo " file, specific version tag, online and offline install package"
@echo " make cleanbinary: remove ui and jobservice binary"
@echo " make cleanimage: remove Harbor images"
@echo " make cleandockercomposefile: remove specific version docker-compose"
@echo " make cleanversiontag: cleanpackageremove specific version tag"
@echo " make cleanpackage: remove online and offline install package"
all: install
# Makefile for Harbor project
#
# Targets:
#
# all: prepare env, compile binarys, build images and install images
# prepare: prepare env
# 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_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 baseimage
#
# install: include compile binarys, build images, prepare specific \
# version composefile and startup Harbor instance
#
# start: startup Harbor instance
#
# down: shutdown Harbor instance
#
# package_online:
# prepare online install package
# for example: make package_online -e DEVFLAG=false\
# REGISTRYSERVER=reg-bj.eng.vmware.com \
# REGISTRYPROJECTNAME=harborrelease
#
# package_offline:
# prepare offline install package
#
# pushimage: push Harbor images to specific registry server
# for example: make pushimage -e DEVFLAG=false REGISTRYUSER=admin \
# REGISTRYPASSWORD=***** \
# REGISTRYSERVER=reg-bj.eng.vmware.com/ \
# REGISTRYPROJECTNAME=harborrelease
# note**: need add "/" on end of REGISTRYSERVER. If not setting \
# this value will push images directly to dockerhub.
# make pushimage -e DEVFLAG=false REGISTRYUSER=vmware \
# REGISTRYPASSWORD=***** \
# REGISTRYPROJECTNAME=vmware
#
# clean: remove binary, Harbor images, specific version docker-compose \
# file, specific version tag and online/offline install package
# cleanbinary: remove adminserver, ui and jobservice binary
# cleanimage: remove Harbor images
# cleandockercomposefile:
# remove specific version docker-compose
# cleanversiontag:
# cleanpackageremove specific version tag
# cleanpackage: remove online/offline install package
#
# other example:
# clean specific version binarys and images:
# make clean -e VERSIONTAG=[TAG]
# note**: If commit new code to github, the git commit TAG will \
# change. Better use this commond clean previous images and \
# files with specific TAG.
# By default DEVFLAG=true, if you want to release new version of Harbor, \
# should setting the flag to false.
# make XXXX -e DEVFLAG=false
SHELL := /bin/bash
BUILDPATH=$(CURDIR)
MAKEPATH=$(BUILDPATH)/make
MAKEDEVPATH=$(MAKEPATH)/dev
SRCPATH=./src
TOOLSPATH=$(BUILDPATH)/tools
UIPATH=$(BUILDPATH)/src/ui
UINGPATH=$(BUILDPATH)/src/ui_ng
GOBASEPATH=/go/src/github.com/vmware
CHECKENVCMD=checkenv.sh
BASEIMAGE=photon
COMPILETAG=compile_normal
REGISTRYSERVER=
REGISTRYPROJECTNAME=vmware
DEVFLAG=true
NOTARYFLAG=false
REGISTRYVERSION=photon-2.6.0
NGINXVERSION=1.11.5
PHOTONVERSION=1.0
NOTARYVERSION=server-0.5.0
NOTARYSIGNERVERSION=signer-0.5.0
MARIADBVERSION=mariadb-10.1.10
HTTPPROXY=
#clarity parameters
CLARITYIMAGE=danieljt/harbor-clarity-base[:tag]
CLARITYSEEDPATH=/clarity-seed
CLARITYBUILDSCRIPT=/entrypoint.sh
# docker parameters
DOCKERCMD=$(shell which docker)
DOCKERBUILD=$(DOCKERCMD) build
DOCKERRMIMAGE=$(DOCKERCMD) rmi
DOCKERPULL=$(DOCKERCMD) pull
DOCKERIMASES=$(DOCKERCMD) images
DOCKERSAVE=$(DOCKERCMD) save
DOCKERCOMPOSECMD=$(shell which docker-compose)
DOCKERTAG=$(DOCKERCMD) tag
# go parameters
GOCMD=$(shell which go)
GOBUILD=$(GOCMD) build
GOCLEAN=$(GOCMD) clean
GOINSTALL=$(GOCMD) install
GOTEST=$(GOCMD) test
GODEP=$(GOTEST) -i
GOFMT=gofmt -w
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
JOBSERVICESOURCECODE=$(SRCPATH)/jobservice
JOBSERVICEBINARYPATH=$(MAKEDEVPATH)/jobservice
JOBSERVICEBINARYNAME=harbor_jobservice
# prepare parameters
PREPAREPATH=$(TOOLSPATH)
PREPARECMD=prepare
# configfile
CONFIGPATH=$(MAKEPATH)
CONFIGFILE=harbor.cfg
# makefile
MAKEFILEPATH_PHOTON=$(MAKEPATH)/photon
# common dockerfile
DOCKERFILEPATH_COMMON=$(MAKEPATH)/common
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
DOCKERIMAGENAME_DB=vmware/harbor-db
# docker-compose files
DOCKERCOMPOSEFILEPATH=$(MAKEPATH)
DOCKERCOMPOSETPLFILENAME=docker-compose.tpl
DOCKERCOMPOSEFILENAME=docker-compose.yml
DOCKERCOMPOSENOTARYFILENAME=docker-compose.notary.yml
# version prepare
VERSIONFILEPATH=$(CURDIR)
VERSIONFILENAME=VERSION
GITCMD=$(shell which git)
GITTAG=$(GITCMD) describe --tags
ifeq ($(DEVFLAG), true)
VERSIONTAG=dev
else
VERSIONTAG=$(shell $(GITTAG))
endif
SEDCMD=$(shell which sed)
# package
TARCMD=$(shell which tar)
ZIPCMD=$(shell which gzip)
DOCKERIMGFILE=harbor
HARBORPKG=harbor
# pushimage
PUSHSCRIPTPATH=$(MAKEPATH)
PUSHSCRIPTNAME=pushimage.sh
REGISTRYUSER=user
REGISTRYPASSWORD=default
version:
@printf $(VERSIONTAG) > $(VERSIONFILEPATH)/$(VERSIONFILENAME);
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)
@echo "Done."
compile_jobservice:
@echo "compiling binary for jobservice..."
@$(GOBUILD) -o $(JOBSERVICEBINARYPATH)/$(JOBSERVICEBINARYNAME) $(JOBSERVICESOURCECODE)
@echo "Done."
compile_clarity:
@echo "compiling binary for clarity ui..."
@if [ "$(HTTPPROXY)" != "" ] ; then \
$(DOCKERCMD) run --rm -v $(UIPATH)/static:$(CLARITYSEEDPATH)/dist -v $(UINGPATH)/src:$(CLARITYSEEDPATH)/src $(CLARITYIMAGE) $(SHELL) $(CLARITYBUILDSCRIPT) -p $(HTTPPROXY); \
else \
$(DOCKERCMD) run --rm -v $(UIPATH)/static:$(CLARITYSEEDPATH)/dist -v $(UINGPATH)/src:$(CLARITYSEEDPATH)/src $(CLARITYIMAGE) $(SHELL) $(CLARITYBUILDSCRIPT); \
fi
@echo "Done."
compile_normal: compile_clarity compile_adminserver compile_ui compile_jobservice
compile_golangimage: compile_clarity
@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)
@$(DOCKERCMD) run --rm -v $(BUILDPATH):$(GOBUILDPATH) -w $(GOBUILDPATH_UI) $(GOBUILDIMAGE) $(GOIMAGEBUILD) -v -o $(GOBUILDMAKEPATH_UI)/$(UIBINARYNAME)
@echo "Done."
@echo "compiling binary for jobservice (golang image)..."
@$(DOCKERCMD) run --rm -v $(BUILDPATH):$(GOBUILDPATH) -w $(GOBUILDPATH_JOBSERVICE) $(GOBUILDIMAGE) $(GOIMAGEBUILD) -v -o $(GOBUILDMAKEPATH_JOBSERVICE)/$(JOBSERVICEBINARYNAME)
@echo "Done."
compile:check_environment $(COMPILETAG)
prepare:
@echo "preparing..."
@if [ "$(NOTARYFLAG)" = "true" ] ; then \
$(MAKEPATH)/$(PREPARECMD) --conf $(CONFIGPATH)/$(CONFIGFILE) --with-notary; \
else \
$(MAKEPATH)/$(PREPARECMD) --conf $(CONFIGPATH)/$(CONFIGFILE) ; \
fi
build_common: version
@echo "buildging db container for photon..."
@cd $(DOCKERFILEPATH_DB) && $(DOCKERBUILD) -f $(DOCKERFILENAME_DB) -t $(DOCKERIMAGENAME_DB):$(VERSIONTAG) .
@echo "Done."
build_photon: build_common
make -f $(MAKEFILEPATH_PHOTON)/Makefile build -e DEVFLAG=$(DEVFLAG)
build: build_$(BASEIMAGE)
modify_composefile:
@echo "preparing docker-compose file..."
@cp $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSETPLFILENAME) $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSEFILENAME)
@$(SEDCMD) -i 's/__version__/$(VERSIONTAG)/g' $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSEFILENAME)
install: compile build prepare modify_composefile start
package_online: modify_composefile
@echo "packing online package ..."
@cp -r make $(HARBORPKG)
@if [ -n "$(REGISTRYSERVER)" ] ; then \
$(SEDCMD) -i 's/image\: vmware/image\: $(REGISTRYSERVER)\/$(REGISTRYPROJECTNAME)/' \
$(HARBORPKG)/docker-compose.yml ; \
fi
@cp LICENSE $(HARBORPKG)/LICENSE
@cp NOTICE $(HARBORPKG)/NOTICE
@if [ "$(NOTARYFLAG)" = "true" ] ; then \
$(TARCMD) -zcvf harbor-online-installer-$(VERSIONTAG).tgz \
$(HARBORPKG)/common/templates $(HARBORPKG)/prepare \
$(HARBORPKG)/LICENSE $(HARBORPKG)/NOTICE \
$(HARBORPKG)/install.sh $(HARBORPKG)/$(DOCKERCOMPOSEFILENAME) \
$(HARBORPKG)/harbor.cfg $(HARBORPKG)/$(DOCKERCOMPOSENOTARYFILENAME); \
else \
$(TARCMD) -zcvf harbor-online-installer-$(VERSIONTAG).tgz \
$(HARBORPKG)/common/templates $(HARBORPKG)/prepare \
$(HARBORPKG)/LICENSE $(HARBORPKG)/NOTICE \
$(HARBORPKG)/install.sh $(HARBORPKG)/$(DOCKERCOMPOSEFILENAME) \
$(HARBORPKG)/harbor.cfg ; \
fi
@rm -rf $(HARBORPKG)
@echo "Done."
package_offline: compile build modify_composefile
@echo "packing offline package ..."
@cp -r make $(HARBORPKG)
@cp LICENSE $(HARBORPKG)/LICENSE
@cp NOTICE $(HARBORPKG)/NOTICE
@echo "pulling nginx and registry..."
@$(DOCKERPULL) registry:$(REGISTRYVERSION)
@$(DOCKERPULL) nginx:$(NGINXVERSION)
@if [ "$(NOTARYFLAG)" = "true" ] ; then \
echo "pulling notary and harbor-notary-db..."; \
$(DOCKERPULL) vmware/notary-photon:$(NOTARYVERSION); \
$(DOCKERPULL) vmware/notary-photon:$(NOTARYSIGNERVERSION); \
$(DOCKERPULL) vmware/harbor-notary-db:$(MARIADBVERSION); \
fi
@echo "saving harbor docker image"
@if [ "$(NOTARYFLAG)" = "true" ] ; then \
$(DOCKERSAVE) -o $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tgz \
$(DOCKERIMAGENAME_ADMINSERVER):$(VERSIONTAG) \
$(DOCKERIMAGENAME_UI):$(VERSIONTAG) \
$(DOCKERIMAGENAME_LOG):$(VERSIONTAG) \
$(DOCKERIMAGENAME_DB):$(VERSIONTAG) \
$(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG) \
nginx:$(NGINXVERSION) vmware/registry:$(REGISTRYVERSION) photon:$(PHOTONVERSION) \
vmware/notary-photon:$(NOTARYVERSION) vmware/notary-photon:$(NOTARYSIGNERVERSION) vmware/harbor-notary-db:$(MARIADBVERSION); \
else \
$(DOCKERSAVE) -o $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tgz \
$(DOCKERIMAGENAME_ADMINSERVER):$(VERSIONTAG) \
$(DOCKERIMAGENAME_UI):$(VERSIONTAG) \
$(DOCKERIMAGENAME_LOG):$(VERSIONTAG) \
$(DOCKERIMAGENAME_DB):$(VERSIONTAG) \
$(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG) \
nginx:$(NGINXVERSION) vmware/registry:$(REGISTRYVERSION) photon:$(PHOTONVERSION) ; \
fi
@if [ "$(NOTARYFLAG)" = "true" ] ; then \
$(TARCMD) -zcvf harbor-offline-installer-$(VERSIONTAG).tgz \
$(HARBORPKG)/common/templates $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tgz \
$(HARBORPKG)/prepare $(HARBORPKG)/NOTICE \
$(HARBORPKG)/LICENSE $(HARBORPKG)/install.sh \
$(HARBORPKG)/harbor.cfg $(HARBORPKG)/$(DOCKERCOMPOSEFILENAME) \
$(HARBORPKG)/$(DOCKERCOMPOSENOTARYFILENAME) ; \
else \
$(TARCMD) -zcvf harbor-offline-installer-$(VERSIONTAG).tgz \
$(HARBORPKG)/common/templates $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tgz \
$(HARBORPKG)/prepare $(HARBORPKG)/NOTICE \
$(HARBORPKG)/LICENSE $(HARBORPKG)/install.sh \
$(HARBORPKG)/harbor.cfg $(HARBORPKG)/$(DOCKERCOMPOSEFILENAME) ; \
fi
@rm -rf $(HARBORPKG)
@echo "Done."
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)
@$(DOCKERRMIMAGE) $(REGISTRYSERVER)$(DOCKERIMAGENAME_UI):$(VERSIONTAG)
@$(DOCKERTAG) $(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG) $(REGISTRYSERVER)$(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG)
@$(PUSHSCRIPTPATH)/$(PUSHSCRIPTNAME) $(REGISTRYSERVER)$(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG) \
$(REGISTRYUSER) $(REGISTRYPASSWORD) $(REGISTRYSERVER)
@$(DOCKERRMIMAGE) $(REGISTRYSERVER)$(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG)
@$(DOCKERTAG) $(DOCKERIMAGENAME_LOG):$(VERSIONTAG) $(REGISTRYSERVER)$(DOCKERIMAGENAME_LOG):$(VERSIONTAG)
@$(PUSHSCRIPTPATH)/$(PUSHSCRIPTNAME) $(REGISTRYSERVER)$(DOCKERIMAGENAME_LOG):$(VERSIONTAG) \
$(REGISTRYUSER) $(REGISTRYPASSWORD) $(REGISTRYSERVER)
@$(DOCKERRMIMAGE) $(REGISTRYSERVER)$(DOCKERIMAGENAME_LOG):$(VERSIONTAG)
@$(DOCKERTAG) $(DOCKERIMAGENAME_DB):$(VERSIONTAG) $(REGISTRYSERVER)$(DOCKERIMAGENAME_DB):$(VERSIONTAG)
@$(PUSHSCRIPTPATH)/$(PUSHSCRIPTNAME) $(REGISTRYSERVER)$(DOCKERIMAGENAME_DB):$(VERSIONTAG) \
$(REGISTRYUSER) $(REGISTRYPASSWORD) $(REGISTRYSERVER)
@$(DOCKERRMIMAGE) $(REGISTRYSERVER)$(DOCKERIMAGENAME_DB):$(VERSIONTAG)
start:
@echo "loading harbor images..."
@if [ "$(NOTARYFLAG)" = "true" ] ; then \
$(DOCKERCOMPOSECMD) -f $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSEFILENAME) -f $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSENOTARYFILENAME) up -d ; \
else \
$(DOCKERCOMPOSECMD) -f $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSEFILENAME) up -d ; \
fi
@echo "Start complete. You can visit harbor now."
down:
@echo "Please make sure to set -e NOTARYFLAG=true if you are using Notary in Harbor, otherwise the Notary containers cannot be stop automaticlly."
@while [ -z "$$CONTINUE" ]; do \
read -r -p "Type anything but Y or y to exit. [Y/N]: " CONTINUE; \
done ; \
[ $$CONTINUE = "y" ] || [ $$CONTINUE = "Y" ] || (echo "Exiting."; exit 1;)
@echo "stoping harbor instance..."
@if [ "$(NOTARYFLAG)" = "true" ] ; then \
$(DOCKERCOMPOSECMD) -f $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSEFILENAME) -f $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSENOTARYFILENAME) down ; \
else \
$(DOCKERCOMPOSECMD) -f $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSEFILENAME) down ; \
fi
@echo "Done."
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)
- $(DOCKERRMIMAGE) -f $(DOCKERIMAGENAME_LOG):$(VERSIONTAG)
# - $(DOCKERRMIMAGE) -f registry:$(REGISTRYVERSION)
# - $(DOCKERRMIMAGE) -f nginx:1.11.5
cleandockercomposefile:
@echo "cleaning $(DOCKERCOMPOSEFILEPATH)/docker-compose.yml"
@if [ -f $(DOCKERCOMPOSEFILEPATH)/docker-compose.yml ] ; then rm $(DOCKERCOMPOSEFILEPATH)/docker-compose.yml ; fi
cleanversiontag:
@echo "cleaning version TAG"
@rm -rf $(VERSIONFILEPATH)/$(VERSIONFILENAME)
cleanpackage:
@echo "cleaning harbor install package"
@if [ -d $(BUILDPATH)/harbor ] ; then rm -rf $(BUILDPATH)/harbor ; fi
@if [ -f $(BUILDPATH)/harbor-online-installer-$(VERSIONTAG).tgz ] ; \
then rm $(BUILDPATH)/harbor-online-installer-$(VERSIONTAG).tgz ; fi
@if [ -f $(BUILDPATH)/harbor-offline-installer-$(VERSIONTAG).tgz ] ; \
then rm $(BUILDPATH)/harbor-offline-installer-$(VERSIONTAG).tgz ; fi
.PHONY: cleanall
cleanall: cleanbinary cleanimage cleandockercomposefile cleanversiontag cleanpackage
clean:
@echo " make cleanall: remove binary, Harbor images, specific version docker-compose"
@echo " file, specific version tag, online and offline install package"
@echo " make cleanbinary: remove ui and jobservice binary"
@echo " make cleanimage: remove Harbor images"
@echo " make cleandockercomposefile: remove specific version docker-compose"
@echo " make cleanversiontag: cleanpackageremove specific version tag"
@echo " make cleanpackage: remove online and offline install package"
all: install

1
VERSION Normal file
View File

@ -0,0 +1 @@
dev

View File

@ -2028,6 +2028,9 @@ definitions:
has_ca_root:
type: boolean
description: Indicate whether there is a ca root cert file ready for download in the file system.
harbor_version:
type: string
description: The build version of Harbor.
SystemInfo:
type: object
properties:

View File

@ -50,6 +50,7 @@ services:
volumes:
- /data/config/:/etc/adminserver/
- /data/secretkey:/etc/adminserver/key
- /data/:/data/
depends_on:
- log
logging:

View File

@ -56,6 +56,7 @@ services:
volumes:
- /data/config/:/etc/adminserver/
- /data/secretkey:/etc/adminserver/key
- /data/:/data/
networks:
- harbor
depends_on:
@ -74,7 +75,6 @@ services:
volumes:
- ./common/config/ui/app.conf:/etc/ui/app.conf
- ./common/config/ui/private_key.pem:/etc/ui/private_key.pem
- /data:/harbor_storage
- /data/secretkey:/etc/ui/key
networks:
- harbor

View File

@ -7,6 +7,7 @@ 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 ./VERSION /harbor/VERSION
RUN chmod u+x /harbor/harbor_ui

View File

@ -26,7 +26,7 @@ import (
"testing"
"github.com/vmware/harbor/src/adminserver/systemcfg"
comcfg "github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/utils/test"
)
@ -43,7 +43,7 @@ func TestConfigAPI(t *testing.T) {
secret := "secret"
envs := map[string]string{
"AUTH_MODE": comcfg.DBAuth,
"AUTH_MODE": common.DBAuth,
"JSON_CFG_STORE_PATH": configPath,
"KEY_PATH": secretKeyPath,
"UI_SECRET": secret,
@ -97,7 +97,7 @@ func TestConfigAPI(t *testing.T) {
return
}
scope := int(m[comcfg.LDAPScope].(float64))
scope := int(m[common.LDAPScope].(float64))
if scope != 3 {
t.Errorf("unexpected ldap scope: %d != %d", scope, 3)
return
@ -105,7 +105,7 @@ func TestConfigAPI(t *testing.T) {
// modify configurations
c := map[string]interface{}{
comcfg.AUTHMode: comcfg.LDAPAuth,
common.AUTHMode: common.LDAPAuth,
}
b, err := json.Marshal(c)
@ -155,9 +155,9 @@ func TestConfigAPI(t *testing.T) {
return
}
mode := m[comcfg.AUTHMode].(string)
if mode != comcfg.LDAPAuth {
t.Errorf("unexpected auth mode: %s != %s", mode, comcfg.LDAPAuth)
mode := m[common.AUTHMode].(string)
if mode != common.LDAPAuth {
t.Errorf("unexpected auth mode: %s != %s", mode, common.LDAPAuth)
return
}
@ -203,9 +203,9 @@ func TestConfigAPI(t *testing.T) {
return
}
mode = m[comcfg.AUTHMode].(string)
if mode != comcfg.DBAuth {
t.Errorf("unexpected auth mode: %s != %s", mode, comcfg.LDAPAuth)
mode = m[common.AUTHMode].(string)
if mode != common.DBAuth {
t.Errorf("unexpected auth mode: %s != %s", mode, common.LDAPAuth)
return
}
}

View File

@ -0,0 +1,51 @@
/*
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 auth
import (
"net/http"
)
// Authorizer authorizes request
type Authorizer interface {
Authorize(*http.Request) error
}
// NewSecretAuthorizer returns an instance of secretAuthorizer
func NewSecretAuthorizer(cookieName, secret string) Authorizer {
return &secretAuthorizer{
cookieName: cookieName,
secret: secret,
}
}
type secretAuthorizer struct {
cookieName string
secret string
}
func (s *secretAuthorizer) Authorize(req *http.Request) error {
if req == nil {
return nil
}
req.AddCookie(&http.Cookie{
Name: s.cookieName,
Value: s.secret,
})
return nil
}

View File

@ -0,0 +1,44 @@
/*
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 auth
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestAuthorize(t *testing.T) {
cookieName := "secret"
secret := "secret"
authorizer := NewSecretAuthorizer(cookieName, secret)
req, err := http.NewRequest("", "", nil)
if !assert.Nil(t, err, "unexpected error") {
return
}
err = authorizer.Authorize(req)
if !assert.Nil(t, err, "unexpected error") {
return
}
cookie, err := req.Cookie(cookieName)
if !assert.Nil(t, err, "unexpected error") {
return
}
assert.Equal(t, secret, cookie.Value, "unexpected cookie")
}

View File

@ -0,0 +1,184 @@
/*
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 client
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
"github.com/vmware/harbor/src/adminserver/client/auth"
"github.com/vmware/harbor/src/adminserver/systeminfo/imagestorage"
"github.com/vmware/harbor/src/common/utils"
)
// Client defines methods that an Adminserver client should implement
type Client interface {
// Ping tests the connection with server
Ping() error
// GetCfgs returns system configurations
GetCfgs() (map[string]interface{}, error)
// UpdateCfgs updates system configurations
UpdateCfgs(map[string]interface{}) error
// ResetCfgs resets system configuratoins form environment variables
ResetCfgs() error
// Capacity returns the capacity of image storage
Capacity() (*imagestorage.Capacity, error)
}
// NewClient return an instance of Adminserver client
func NewClient(baseURL string, authorizer auth.Authorizer) Client {
baseURL = strings.TrimRight(baseURL, "/")
if !strings.Contains(baseURL, "://") {
baseURL = "http://" + baseURL
}
return &client{
baseURL: baseURL,
client: &http.Client{},
authorizer: authorizer,
}
}
type client struct {
baseURL string
client *http.Client
authorizer auth.Authorizer
}
// do creates request and authorizes it if authorizer is not nil
func (c *client) do(method, relativePath string, body io.Reader) (*http.Response, error) {
url := c.baseURL + relativePath
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
if c.authorizer != nil {
if err := c.authorizer.Authorize(req); err != nil {
return nil, err
}
}
return c.client.Do(req)
}
func (c *client) Ping() error {
addr := strings.Split(c.baseURL, "://")[1]
if !strings.Contains(addr, ":") {
addr = addr + ":80"
}
return utils.TestTCPConn(addr, 60, 2)
}
// GetCfgs ...
func (c *client) GetCfgs() (map[string]interface{}, error) {
resp, err := c.do(http.MethodGet, "/api/configurations", nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to get configurations: %d %s",
resp.StatusCode, b)
}
cfgs := map[string]interface{}{}
if err = json.Unmarshal(b, &cfgs); err != nil {
return nil, err
}
return cfgs, nil
}
// UpdateCfgs ...
func (c *client) UpdateCfgs(cfgs map[string]interface{}) error {
data, err := json.Marshal(cfgs)
if err != nil {
return err
}
resp, err := c.do(http.MethodPut, "/api/configurations", bytes.NewReader(data))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return fmt.Errorf("failed to update configurations: %d %s",
resp.StatusCode, b)
}
return nil
}
// ResetCfgs ...
func (c *client) ResetCfgs() error {
resp, err := c.do(http.MethodPost, "/api/configurations/reset", nil)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return fmt.Errorf("failed to reset configurations: %d %s",
resp.StatusCode, b)
}
return nil
}
// Capacity ...
func (c *client) Capacity() (*imagestorage.Capacity, error) {
resp, err := c.do(http.MethodGet, "/api/systeminfo/capacity", nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to get capacity: %d %s",
resp.StatusCode, b)
}
capacity := &imagestorage.Capacity{}
if err = json.Unmarshal(b, capacity); err != nil {
return nil, err
}
return capacity, nil
}

View File

@ -0,0 +1,82 @@
/*
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 client
import (
"fmt"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/utils/test"
)
var c Client
func TestMain(m *testing.M) {
server, err := test.NewAdminserver(nil)
if err != nil {
fmt.Printf("failed to create adminserver: %v", err)
os.Exit(1)
}
c = NewClient(server.URL, nil)
os.Exit(m.Run())
}
func TestPing(t *testing.T) {
err := c.Ping()
assert.Nil(t, err, "unexpected error")
}
func TestGetCfgs(t *testing.T) {
cfgs, err := c.GetCfgs()
if !assert.Nil(t, err, "unexpected error") {
return
}
assert.Equal(t, common.DBAuth, cfgs[common.AUTHMode], "unexpected configuration")
}
func TestUpdateCfgs(t *testing.T) {
cfgs := map[string]interface{}{
common.AUTHMode: common.LDAPAuth,
}
err := c.UpdateCfgs(cfgs)
if !assert.Nil(t, err, "unexpected error") {
return
}
}
func TestResetCfgs(t *testing.T) {
err := c.ResetCfgs()
if !assert.Nil(t, err, "unexpected error") {
return
}
}
func TestCapacity(t *testing.T) {
capacity, err := c.Capacity()
if !assert.Nil(t, err, "unexpected error") {
return
}
assert.Equal(t, uint64(100), capacity.Total)
assert.Equal(t, uint64(90), capacity.Free)
}

View File

@ -23,6 +23,7 @@ import (
"github.com/vmware/harbor/src/adminserver/systemcfg/store"
"github.com/vmware/harbor/src/adminserver/systemcfg/store/json"
"github.com/vmware/harbor/src/common"
comcfg "github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/utils"
"github.com/vmware/harbor/src/common/utils/log"
@ -40,82 +41,82 @@ var (
// attrs need to be encrypted or decrypted
attrs = []string{
comcfg.EmailPassword,
comcfg.LDAPSearchPwd,
comcfg.MySQLPassword,
comcfg.AdminInitialPassword,
common.EmailPassword,
common.LDAPSearchPwd,
common.MySQLPassword,
common.AdminInitialPassword,
}
// all configurations need read from environment variables
allEnvs = map[string]interface{}{
comcfg.ExtEndpoint: "EXT_ENDPOINT",
comcfg.AUTHMode: "AUTH_MODE",
comcfg.SelfRegistration: &parser{
common.ExtEndpoint: "EXT_ENDPOINT",
common.AUTHMode: "AUTH_MODE",
common.SelfRegistration: &parser{
env: "SELF_REGISTRATION",
parse: parseStringToBool,
},
comcfg.DatabaseType: "DATABASE_TYPE",
comcfg.MySQLHost: "MYSQL_HOST",
comcfg.MySQLPort: &parser{
common.DatabaseType: "DATABASE_TYPE",
common.MySQLHost: "MYSQL_HOST",
common.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{
common.MySQLUsername: "MYSQL_USR",
common.MySQLPassword: "MYSQL_PWD",
common.MySQLDatabase: "MYSQL_DATABASE",
common.SQLiteFile: "SQLITE_FILE",
common.LDAPURL: "LDAP_URL",
common.LDAPSearchDN: "LDAP_SEARCH_DN",
common.LDAPSearchPwd: "LDAP_SEARCH_PWD",
common.LDAPBaseDN: "LDAP_BASE_DN",
common.LDAPFilter: "LDAP_FILTER",
common.LDAPUID: "LDAP_UID",
common.LDAPScope: &parser{
env: "LDAP_SCOPE",
parse: parseStringToInt,
},
comcfg.LDAPTimeout: &parser{
common.LDAPTimeout: &parser{
env: "LDAP_TIMEOUT",
parse: parseStringToInt,
},
comcfg.EmailHost: "EMAIL_HOST",
comcfg.EmailPort: &parser{
common.EmailHost: "EMAIL_HOST",
common.EmailPort: &parser{
env: "EMAIL_PORT",
parse: parseStringToInt,
},
comcfg.EmailUsername: "EMAIL_USR",
comcfg.EmailPassword: "EMAIL_PWD",
comcfg.EmailSSL: &parser{
common.EmailUsername: "EMAIL_USR",
common.EmailPassword: "EMAIL_PWD",
common.EmailSSL: &parser{
env: "EMAIL_SSL",
parse: parseStringToBool,
},
comcfg.EmailFrom: "EMAIL_FROM",
comcfg.EmailIdentity: "EMAIL_IDENTITY",
comcfg.RegistryURL: "REGISTRY_URL",
comcfg.TokenExpiration: &parser{
common.EmailFrom: "EMAIL_FROM",
common.EmailIdentity: "EMAIL_IDENTITY",
common.RegistryURL: "REGISTRY_URL",
common.TokenExpiration: &parser{
env: "TOKEN_EXPIRATION",
parse: parseStringToInt,
},
comcfg.UseCompressedJS: &parser{
common.UseCompressedJS: &parser{
env: "USE_COMPRESSED_JS",
parse: parseStringToBool,
},
comcfg.CfgExpiration: &parser{
common.CfgExpiration: &parser{
env: "CFG_EXPIRATION",
parse: parseStringToInt,
},
comcfg.MaxJobWorkers: &parser{
common.MaxJobWorkers: &parser{
env: "MAX_JOB_WORKERS",
parse: parseStringToInt,
},
comcfg.VerifyRemoteCert: &parser{
common.VerifyRemoteCert: &parser{
env: "VERIFY_REMOTE_CERT",
parse: parseStringToBool,
},
comcfg.ProjectCreationRestriction: "PROJECT_CREATION_RESTRICTION",
comcfg.AdminInitialPassword: "HARBOR_ADMIN_PASSWORD",
comcfg.AdmiralEndpoint: "ADMIRAL_URL",
comcfg.WithNotary: &parser{
common.ProjectCreationRestriction: "PROJECT_CREATION_RESTRICTION",
common.AdminInitialPassword: "HARBOR_ADMIN_PASSWORD",
common.AdmiralEndpoint: "ADMIRAL_URL",
common.WithNotary: &parser{
env: "WITH_NOTARY",
parse: parseStringToBool,
},
@ -124,23 +125,23 @@ var (
// configurations need read from environment variables
// every time the system startup
repeatLoadEnvs = map[string]interface{}{
comcfg.ExtEndpoint: "EXT_ENDPOINT",
comcfg.MySQLPassword: "MYSQL_PWD",
comcfg.MaxJobWorkers: &parser{
common.ExtEndpoint: "EXT_ENDPOINT",
common.MySQLPassword: "MYSQL_PWD",
common.MaxJobWorkers: &parser{
env: "MAX_JOB_WORKERS",
parse: parseStringToInt,
},
// TODO remove this config?
comcfg.UseCompressedJS: &parser{
common.UseCompressedJS: &parser{
env: "USE_COMPRESSED_JS",
parse: parseStringToBool,
},
comcfg.CfgExpiration: &parser{
common.CfgExpiration: &parser{
env: "CFG_EXPIRATION",
parse: parseStringToInt,
},
comcfg.AdmiralEndpoint: "ADMIRAL_URL",
comcfg.WithNotary: &parser{
common.AdmiralEndpoint: "ADMIRAL_URL",
common.WithNotary: &parser{
env: "WITH_NOTARY",
parse: parseStringToBool,
},

View File

@ -19,7 +19,7 @@ import (
"os"
"testing"
comcfg "github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/utils/test"
)
@ -54,7 +54,7 @@ func TestSystemcfg(t *testing.T) {
}
m := map[string]string{
"AUTH_MODE": comcfg.DBAuth,
"AUTH_MODE": common.DBAuth,
"LDAP_SCOPE": "1",
"LDAP_TIMEOUT": "30",
"MYSQL_PORT": "3306",
@ -93,13 +93,13 @@ func TestSystemcfg(t *testing.T) {
return
}
if cfg[comcfg.AUTHMode] != comcfg.DBAuth {
if cfg[common.AUTHMode] != common.DBAuth {
t.Errorf("unexpected auth mode: %s != %s",
cfg[comcfg.AUTHMode], comcfg.DBAuth)
cfg[common.AUTHMode], common.DBAuth)
return
}
cfg[comcfg.AUTHMode] = comcfg.LDAPAuth
cfg[common.AUTHMode] = common.LDAPAuth
if err = UpdateSystemCfg(cfg); err != nil {
t.Errorf("failed to update system configurations: %v", err)
@ -112,9 +112,9 @@ func TestSystemcfg(t *testing.T) {
return
}
if cfg[comcfg.AUTHMode] != comcfg.LDAPAuth {
if cfg[common.AUTHMode] != common.LDAPAuth {
t.Errorf("unexpected auth mode: %s != %s",
cfg[comcfg.AUTHMode], comcfg.DBAuth)
cfg[common.AUTHMode], common.DBAuth)
return
}
@ -129,9 +129,9 @@ func TestSystemcfg(t *testing.T) {
return
}
if cfg[comcfg.AUTHMode] != comcfg.DBAuth {
if cfg[common.AUTHMode] != common.DBAuth {
t.Errorf("unexpected auth mode: %s != %s",
cfg[comcfg.AUTHMode], comcfg.DBAuth)
cfg[common.AUTHMode], common.DBAuth)
return
}
}

View File

@ -17,83 +17,26 @@
package config
import (
"bytes"
"encoding/json"
"fmt"
"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"
)
// 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"
AdmiralEndpoint = "admiral_url"
WithNotary = "with_notary"
"github.com/vmware/harbor/src/adminserver/client"
"github.com/vmware/harbor/src/common"
)
// Manager manages configurations
type Manager struct {
Loader *Loader
Parser *Parser
client client.Client
Cache bool
cache cache.Cache
key string
}
// NewManager returns an instance of Manager
// url: the url from which loader loads configurations
func NewManager(url, secret string, enableCache bool) *Manager {
func NewManager(client client.Client, enableCache bool) *Manager {
m := &Manager{
Loader: NewLoader(url, secret),
Parser: &Parser{},
client: client,
}
if enableCache {
@ -105,19 +48,9 @@ func NewManager(url, secret string, enableCache bool) *Manager {
return m
}
// Init loader
func (m *Manager) Init() error {
return m.Loader.Init()
}
// 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)
c, err := m.client.GetCfgs()
if err != nil {
return nil, err
}
@ -127,7 +60,15 @@ func (m *Manager) Load() (map[string]interface{}, error) {
if err != nil {
return nil, err
}
if err = m.cache.Put(m.key, c,
// copy the configuration map so that later modification to the
// map does not effect the cached value
cachedCfgs := map[string]interface{}{}
for k, v := range c {
cachedCfgs[k] = v
}
if err = m.cache.Put(m.key, cachedCfgs,
time.Duration(expi)*time.Second); err != nil {
return nil, err
}
@ -138,7 +79,7 @@ func (m *Manager) Load() (map[string]interface{}, error) {
// Reset configurations
func (m *Manager) Reset() error {
return m.Loader.Reset()
return m.client.ResetCfgs()
}
func getCfgExpiration(m map[string]interface{}) (int, error) {
@ -146,7 +87,7 @@ func getCfgExpiration(m map[string]interface{}) (int, error) {
return 0, fmt.Errorf("can not get cfg expiration as configurations are null")
}
expi, ok := m[CfgExpiration]
expi, ok := m[common.CfgExpiration]
if !ok {
return 0, fmt.Errorf("cfg expiration is not set")
}
@ -167,133 +108,6 @@ func (m *Manager) Get() (map[string]interface{}, error) {
}
// 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 configurations 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
}
req.AddCookie(&http.Cookie{
Name: "secret",
Value: l.secret,
})
resp, err := l.client.Do(req)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected http status code: %d", resp.StatusCode)
}
log.Debug("configurations uploaded")
return nil
}
// Reset sends configurations resetting command to
// remote server
func (l *Loader) Reset() error {
req, err := http.NewRequest("POST", l.url+"/api/configurations/reset", nil)
if err != nil {
return err
}
req.AddCookie(&http.Cookie{
Name: "secret",
Value: l.secret,
})
resp, err := l.client.Do(req)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected http status code: %d", resp.StatusCode)
}
log.Debug("configurations resetted")
return nil
}
// Parser parses configurations
type Parser struct {
}
// 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
}
return c, nil
func (m *Manager) Upload(cfgs map[string]interface{}) error {
return m.client.UpdateCfgs(cfgs)
}

65
src/common/const.go Normal file
View File

@ -0,0 +1,65 @@
/*
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 common
// 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"
AdmiralEndpoint = "admiral_url"
WithNotary = "with_notary"
)

View File

@ -21,7 +21,7 @@ import (
"os"
"testing"
"github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
@ -30,24 +30,24 @@ import (
)
var adminServerLdapTestConfig = map[string]interface{}{
config.ExtEndpoint: "host01.com",
config.AUTHMode: "ldap_auth",
config.DatabaseType: "mysql",
config.MySQLHost: "127.0.0.1",
config.MySQLPort: 3306,
config.MySQLUsername: "root",
config.MySQLPassword: "root123",
config.MySQLDatabase: "registry",
config.SQLiteFile: "/tmp/registry.db",
common.ExtEndpoint: "host01.com",
common.AUTHMode: "ldap_auth",
common.DatabaseType: "mysql",
common.MySQLHost: "127.0.0.1",
common.MySQLPort: 3306,
common.MySQLUsername: "root",
common.MySQLPassword: "root123",
common.MySQLDatabase: "registry",
common.SQLiteFile: "/tmp/registry.db",
//config.SelfRegistration: true,
config.LDAPURL: "ldap://127.0.0.1",
config.LDAPSearchDN: "cn=admin,dc=example,dc=com",
config.LDAPSearchPwd: "admin",
config.LDAPBaseDN: "dc=example,dc=com",
config.LDAPUID: "uid",
config.LDAPFilter: "",
config.LDAPScope: 3,
config.LDAPTimeout: 30,
common.LDAPURL: "ldap://127.0.0.1",
common.LDAPSearchDN: "cn=admin,dc=example,dc=com",
common.LDAPSearchPwd: "admin",
common.LDAPBaseDN: "dc=example,dc=com",
common.LDAPUID: "uid",
common.LDAPFilter: "",
common.LDAPScope: 3,
common.LDAPTimeout: 30,
// config.TokenServiceURL: "",
// config.RegistryURL: "",
// config.EmailHost: "",
@ -61,10 +61,10 @@ var adminServerLdapTestConfig = map[string]interface{}{
// config.VerifyRemoteCert: false,
// config.MaxJobWorkers: 3,
// config.TokenExpiration: 30,
config.CfgExpiration: 5,
common.CfgExpiration: 5,
// config.JobLogDir: "/var/log/jobs",
// config.UseCompressedJS: true,
config.AdminInitialPassword: "password",
common.AdminInitialPassword: "password",
}
func TestMain(t *testing.T) {

View File

@ -20,46 +20,47 @@ import (
"net/http"
"net/http/httptest"
"github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/adminserver/systeminfo/imagestorage"
"github.com/vmware/harbor/src/common"
)
var adminServerDefaultConfig = map[string]interface{}{
config.ExtEndpoint: "https://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.UseCompressedJS: true,
config.AdminInitialPassword: "password",
config.AdmiralEndpoint: "http://www.vmware.com",
config.WithNotary: false,
common.ExtEndpoint: "https://host01.com",
common.AUTHMode: common.DBAuth,
common.DatabaseType: "mysql",
common.MySQLHost: "127.0.0.1",
common.MySQLPort: 3306,
common.MySQLUsername: "user01",
common.MySQLPassword: "password",
common.MySQLDatabase: "registry",
common.SQLiteFile: "/tmp/registry.db",
common.SelfRegistration: true,
common.LDAPURL: "ldap://127.0.0.1",
common.LDAPSearchDN: "uid=searchuser,ou=people,dc=mydomain,dc=com",
common.LDAPSearchPwd: "password",
common.LDAPBaseDN: "ou=people,dc=mydomain,dc=com",
common.LDAPUID: "uid",
common.LDAPFilter: "",
common.LDAPScope: 3,
common.LDAPTimeout: 30,
common.TokenServiceURL: "http://token_service",
common.RegistryURL: "http://registry",
common.EmailHost: "127.0.0.1",
common.EmailPort: 25,
common.EmailUsername: "user01",
common.EmailPassword: "password",
common.EmailFrom: "from",
common.EmailSSL: true,
common.EmailIdentity: "",
common.ProjectCreationRestriction: common.ProCrtRestrAdmOnly,
common.VerifyRemoteCert: false,
common.MaxJobWorkers: 3,
common.TokenExpiration: 30,
common.CfgExpiration: 5,
common.UseCompressedJS: true,
common.AdminInitialPassword: "password",
common.AdmiralEndpoint: "http://www.vmware.com",
common.WithNotary: false,
}
// NewAdminserver returns a mock admin server
@ -101,5 +102,32 @@ func NewAdminserver(config map[string]interface{}) (*httptest.Server, error) {
}),
})
capacityHandler, err := NewCapacityHandle()
if err != nil {
return nil, err
}
m = append(m, &RequestHandlerMapping{
Method: "GET",
Pattern: "/api/systeminfo/capacity",
Handler: capacityHandler,
})
return NewServer(m...), nil
}
// NewCapacityHandle ...
func NewCapacityHandle() (func(http.ResponseWriter, *http.Request), error) {
capacity := imagestorage.Capacity{
Total: 100,
Free: 90,
}
b, err := json.Marshal(capacity)
if err != nil {
return nil, err
}
resp := &Response{
StatusCode: http.StatusOK,
Body: b,
}
return Handler(resp), nil
}

View File

@ -16,21 +16,28 @@
package config
import (
"fmt"
"os"
"github.com/vmware/harbor/src/adminserver/client"
"github.com/vmware/harbor/src/adminserver/client/auth"
"github.com/vmware/harbor/src/common"
comcfg "github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
)
const (
defaultKeyPath string = "/etc/jobservice/key"
defaultLogDir string = "/var/log/jobs"
defaultKeyPath string = "/etc/jobservice/key"
defaultLogDir string = "/var/log/jobs"
secretCookieName string = "secret"
)
var (
mg *comcfg.Manager
keyProvider comcfg.KeyProvider
// AdminserverClient is a client for adminserver
AdminserverClient client.Client
mg *comcfg.Manager
keyProvider comcfg.KeyProvider
)
// Init configurations
@ -42,12 +49,15 @@ func Init() error {
if len(adminServerURL) == 0 {
adminServerURL = "http://adminserver"
}
mg = comcfg.NewManager(adminServerURL, JobserviceSecret(), true)
if err := mg.Init(); err != nil {
return err
log.Infof("initializing client for adminserver %s ...", adminServerURL)
authorizer := auth.NewSecretAuthorizer(secretCookieName, UISecret())
AdminserverClient = client.NewClient(adminServerURL, authorizer)
if err := AdminserverClient.Ping(); err != nil {
return fmt.Errorf("failed to ping adminserver: %v", err)
}
mg = comcfg.NewManager(AdminserverClient, true)
if _, err := mg.Load(); err != nil {
return err
}
@ -71,7 +81,7 @@ func VerifyRemoteCert() (bool, error) {
if err != nil {
return true, err
}
return cfg[comcfg.VerifyRemoteCert].(bool), nil
return cfg[common.VerifyRemoteCert].(bool), nil
}
// Database ...
@ -81,16 +91,16 @@ func Database() (*models.Database, error) {
return nil, err
}
database := &models.Database{}
database.Type = cfg[comcfg.DatabaseType].(string)
database.Type = cfg[common.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)
mysql.Host = cfg[common.MySQLHost].(string)
mysql.Port = int(cfg[common.MySQLPort].(float64))
mysql.Username = cfg[common.MySQLUsername].(string)
mysql.Password = cfg[common.MySQLPassword].(string)
mysql.Database = cfg[common.MySQLDatabase].(string)
database.MySQL = mysql
sqlite := &models.SQLite{}
sqlite.File = cfg[comcfg.SQLiteFile].(string)
sqlite.File = cfg[common.SQLiteFile].(string)
database.SQLite = sqlite
return database, nil
@ -102,7 +112,7 @@ func MaxJobWorkers() (int, error) {
if err != nil {
return 0, err
}
return int(cfg[comcfg.MaxJobWorkers].(float64)), nil
return int(cfg[common.MaxJobWorkers].(float64)), nil
}
// LocalUIURL returns the local ui url, job service will use this URL to call API hosted on ui process
@ -116,7 +126,7 @@ func LocalRegURL() (string, error) {
if err != nil {
return "", err
}
return cfg[comcfg.RegistryURL].(string), nil
return cfg[common.RegistryURL].(string), nil
}
// LogDir returns the absolute path to which the log file will be written
@ -151,7 +161,7 @@ func ExtEndpoint() (string, error) {
if err != nil {
return "", err
}
return cfg[comcfg.ExtEndpoint].(string), nil
return cfg[common.ExtEndpoint].(string), nil
}
// InternalTokenServiceEndpoint ...

View File

@ -20,8 +20,8 @@ import (
"net/http"
"strconv"
"github.com/vmware/harbor/src/common"
"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"
@ -30,65 +30,65 @@ import (
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,
common.ExtEndpoint,
common.AUTHMode,
common.DatabaseType,
common.MySQLHost,
common.MySQLPort,
common.MySQLUsername,
common.MySQLPassword,
common.MySQLDatabase,
common.SQLiteFile,
common.SelfRegistration,
common.LDAPURL,
common.LDAPSearchDN,
common.LDAPSearchPwd,
common.LDAPBaseDN,
common.LDAPUID,
common.LDAPFilter,
common.LDAPScope,
common.LDAPTimeout,
common.TokenServiceURL,
common.RegistryURL,
common.EmailHost,
common.EmailPort,
common.EmailUsername,
common.EmailPassword,
common.EmailFrom,
common.EmailSSL,
common.EmailIdentity,
common.ProjectCreationRestriction,
common.VerifyRemoteCert,
common.MaxJobWorkers,
common.TokenExpiration,
common.CfgExpiration,
common.JobLogDir,
common.UseCompressedJS,
common.AdminInitialPassword,
}
numKeys = []string{
comcfg.EmailPort,
comcfg.LDAPScope,
comcfg.LDAPTimeout,
comcfg.MySQLPort,
comcfg.MaxJobWorkers,
comcfg.TokenExpiration,
comcfg.CfgExpiration,
common.EmailPort,
common.LDAPScope,
common.LDAPTimeout,
common.MySQLPort,
common.MaxJobWorkers,
common.TokenExpiration,
common.CfgExpiration,
}
boolKeys = []string{
comcfg.EmailSSL,
comcfg.SelfRegistration,
comcfg.VerifyRemoteCert,
comcfg.UseCompressedJS,
common.EmailSSL,
common.SelfRegistration,
common.VerifyRemoteCert,
common.UseCompressedJS,
}
passwordKeys = []string{
comcfg.AdminInitialPassword,
comcfg.EmailPassword,
comcfg.LDAPSearchPwd,
comcfg.MySQLPassword,
common.AdminInitialPassword,
common.EmailPassword,
common.LDAPSearchPwd,
common.MySQLPassword,
}
)
@ -157,7 +157,7 @@ func (c *ConfigAPI) Put() {
c.CustomAbort(http.StatusBadRequest, err.Error())
}
if value, ok := cfg[comcfg.AUTHMode]; ok {
if value, ok := cfg[common.AUTHMode]; ok {
mode, err := config.AuthMode()
if err != nil {
log.Errorf("failed to get auth mode: %v", err)
@ -174,7 +174,7 @@ func (c *ConfigAPI) Put() {
if !flag {
c.CustomAbort(http.StatusBadRequest,
fmt.Sprintf("%s can not be modified as new users have been inserted into database",
comcfg.AUTHMode))
common.AUTHMode))
}
}
}
@ -213,14 +213,14 @@ func validateCfg(c map[string]string) (bool, error) {
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)
if value, ok := c[common.AUTHMode]; ok {
if value != common.DBAuth && value != common.LDAPAuth {
return isSysErr, fmt.Errorf("invalid %s, shoud be %s or %s", common.AUTHMode, common.DBAuth, common.LDAPAuth)
}
mode = value
}
if mode == comcfg.LDAPAuth {
if mode == common.LDAPAuth {
ldap, err := config.LDAP()
if err != nil {
isSysErr = true
@ -228,46 +228,46 @@ func validateCfg(c map[string]string) (bool, error) {
}
if len(ldap.URL) == 0 {
if _, ok := c[comcfg.LDAPURL]; !ok {
return isSysErr, fmt.Errorf("%s is missing", comcfg.LDAPURL)
if _, ok := c[common.LDAPURL]; !ok {
return isSysErr, fmt.Errorf("%s is missing", common.LDAPURL)
}
}
if len(ldap.BaseDN) == 0 {
if _, ok := c[comcfg.LDAPBaseDN]; !ok {
return isSysErr, fmt.Errorf("%s is missing", comcfg.LDAPBaseDN)
if _, ok := c[common.LDAPBaseDN]; !ok {
return isSysErr, fmt.Errorf("%s is missing", common.LDAPBaseDN)
}
}
if len(ldap.UID) == 0 {
if _, ok := c[comcfg.LDAPUID]; !ok {
return isSysErr, fmt.Errorf("%s is missing", comcfg.LDAPUID)
if _, ok := c[common.LDAPUID]; !ok {
return isSysErr, fmt.Errorf("%s is missing", common.LDAPUID)
}
}
if ldap.Scope == 0 {
if _, ok := c[comcfg.LDAPScope]; !ok {
return isSysErr, fmt.Errorf("%s is missing", comcfg.LDAPScope)
if _, ok := c[common.LDAPScope]; !ok {
return isSysErr, fmt.Errorf("%s is missing", common.LDAPScope)
}
}
}
if ldapURL, ok := c[comcfg.LDAPURL]; ok && len(ldapURL) == 0 {
return isSysErr, fmt.Errorf("%s is empty", comcfg.LDAPURL)
if ldapURL, ok := c[common.LDAPURL]; ok && len(ldapURL) == 0 {
return isSysErr, fmt.Errorf("%s is empty", common.LDAPURL)
}
if baseDN, ok := c[comcfg.LDAPBaseDN]; ok && len(baseDN) == 0 {
return isSysErr, fmt.Errorf("%s is empty", comcfg.LDAPBaseDN)
if baseDN, ok := c[common.LDAPBaseDN]; ok && len(baseDN) == 0 {
return isSysErr, fmt.Errorf("%s is empty", common.LDAPBaseDN)
}
if uID, ok := c[comcfg.LDAPUID]; ok && len(uID) == 0 {
return isSysErr, fmt.Errorf("%s is empty", comcfg.LDAPUID)
if uID, ok := c[common.LDAPUID]; ok && len(uID) == 0 {
return isSysErr, fmt.Errorf("%s is empty", common.LDAPUID)
}
if scope, ok := c[comcfg.LDAPScope]; ok &&
scope != comcfg.LDAPScopeBase &&
scope != comcfg.LDAPScopeOnelevel &&
scope != comcfg.LDAPScopeSubtree {
if scope, ok := c[common.LDAPScope]; ok &&
scope != common.LDAPScopeBase &&
scope != common.LDAPScopeOnelevel &&
scope != common.LDAPScopeSubtree {
return isSysErr, fmt.Errorf("invalid %s, should be %s, %s or %s",
comcfg.LDAPScope,
comcfg.LDAPScopeBase,
comcfg.LDAPScopeOnelevel,
comcfg.LDAPScopeSubtree)
common.LDAPScope,
common.LDAPScopeBase,
common.LDAPScopeOnelevel,
common.LDAPScopeSubtree)
}
for _, k := range boolKeys {
@ -293,19 +293,19 @@ func validateCfg(c map[string]string) (bool, error) {
return isSysErr, fmt.Errorf("invalid %s: %s", k, v)
}
if (k == comcfg.EmailPort ||
k == comcfg.MySQLPort) && n > 65535 {
if (k == common.EmailPort ||
k == common.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 {
if crt, ok := c[common.ProjectCreationRestriction]; ok &&
crt != common.ProCrtRestrEveryone &&
crt != common.ProCrtRestrAdmOnly {
return isSysErr, fmt.Errorf("invalid %s, should be %s or %s",
comcfg.ProjectCreationRestriction,
comcfg.ProCrtRestrAdmOnly,
comcfg.ProCrtRestrEveryone)
common.ProjectCreationRestriction,
common.ProCrtRestrAdmOnly,
common.ProCrtRestrEveryone)
}
return isSysErr, nil
@ -363,7 +363,7 @@ func convertForGet(cfg map[string]interface{}) (map[string]*value, error) {
if err != nil {
return nil, err
}
result[comcfg.AUTHMode].Editable = flag
result[common.AUTHMode].Editable = flag
return result, nil
}

View File

@ -20,7 +20,8 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/ui/config"
)
func TestGetConfig(t *testing.T) {
@ -46,8 +47,13 @@ func TestGetConfig(t *testing.T) {
return
}
mode := cfg[config.AUTHMode].Value.(string)
assert.Equal(config.DBAuth, mode, fmt.Sprintf("the auth mode should be %s", config.DBAuth))
mode := cfg[common.AUTHMode].Value.(string)
assert.Equal(common.DBAuth, mode, fmt.Sprintf("the auth mode should be %s", common.DBAuth))
ccc, err := config.GetSystemCfg()
if err != nil {
t.Logf("failed to get system configurations: %v", err)
}
t.Logf("%v", ccc)
}
func TestPutConfig(t *testing.T) {
@ -56,7 +62,7 @@ func TestPutConfig(t *testing.T) {
apiTest := newHarborAPI()
cfg := map[string]string{
config.VerifyRemoteCert: "0",
common.VerifyRemoteCert: "0",
}
code, err := apiTest.PutConfig(*admin, cfg)
@ -67,6 +73,11 @@ func TestPutConfig(t *testing.T) {
if !assert.Equal(200, code, "the status code of modifying configurations with admin user should be 200") {
return
}
ccc, err := config.GetSystemCfg()
if err != nil {
t.Logf("failed to get system configurations: %v", err)
}
t.Logf("%v", ccc)
}
func TestResetConfig(t *testing.T) {
@ -94,11 +105,17 @@ func TestResetConfig(t *testing.T) {
return
}
value, ok := cfgs[config.VerifyRemoteCert]
value, ok := cfgs[common.VerifyRemoteCert]
if !ok {
t.Errorf("%s not found", config.VerifyRemoteCert)
t.Errorf("%s not found", common.VerifyRemoteCert)
return
}
assert.Equal(value.Value.(bool), true, "unexpected value")
ccc, err := config.GetSystemCfg()
if err != nil {
t.Logf("failed to get system configurations: %v", err)
}
t.Logf("%v", ccc)
}

View File

@ -1,14 +1,13 @@
package api
import (
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"syscall"
"github.com/vmware/harbor/src/common"
"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"
@ -21,8 +20,8 @@ type SystemInfoAPI struct {
isAdmin bool
}
const harborStoragePath = "/harbor_storage"
const defaultRootCert = "/harbor_storage/ca_download/ca.crt"
const harborVersionFile = "/harbor/VERSION"
//SystemInfo models for system info.
type SystemInfo struct {
@ -45,6 +44,7 @@ type GeneralInfo struct {
ProjectCreationRestrict string `json:"project_creation_restriction"`
SelfRegistration bool `json:"self_registration"`
HasCARoot bool `json:"has_ca_root"`
HarborVersion string `json:"harbor_version"`
}
// validate for validating user if an admin.
@ -66,18 +66,16 @@ func (sia *SystemInfoAPI) GetVolumeInfo() {
sia.RenderError(http.StatusForbidden, "User does not have admin role.")
return
}
var stat syscall.Statfs_t
err := syscall.Statfs(filepath.Join("/", harborStoragePath), &stat)
if err != nil {
log.Errorf("Error occurred in syscall.Statfs: %v", err)
sia.CustomAbort(http.StatusInternalServerError, "Internal error.")
return
}
capacity, err := config.AdminserverClient.Capacity()
if err != nil {
log.Errorf("failed to get capacity: %v", err)
sia.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
systemInfo := SystemInfo{
HarborStorage: Storage{
Total: stat.Blocks * uint64(stat.Bsize),
Free: stat.Bavail * uint64(stat.Bsize),
Total: capacity.Total,
Free: capacity.Free,
},
}
@ -112,22 +110,34 @@ func (sia *SystemInfoAPI) GetGeneralInfo() {
sia.CustomAbort(http.StatusInternalServerError, "Unexpected error")
}
var registryURL string
if l := strings.Split(cfg[comcfg.ExtEndpoint].(string), "://"); len(l) > 1 {
if l := strings.Split(cfg[common.ExtEndpoint].(string), "://"); len(l) > 1 {
registryURL = l[1]
} else {
registryURL = l[0]
}
_, caStatErr := os.Stat(defaultRootCert)
harborVersion := sia.getVersion()
info := GeneralInfo{
AdmiralEndpoint: cfg[comcfg.AdmiralEndpoint].(string),
AdmiralEndpoint: cfg[common.AdmiralEndpoint].(string),
WithAdmiral: config.WithAdmiral(),
WithNotary: config.WithNotary(),
AuthMode: cfg[comcfg.AUTHMode].(string),
ProjectCreationRestrict: cfg[comcfg.ProjectCreationRestriction].(string),
SelfRegistration: cfg[comcfg.SelfRegistration].(bool),
AuthMode: cfg[common.AUTHMode].(string),
ProjectCreationRestrict: cfg[common.ProjectCreationRestriction].(string),
SelfRegistration: cfg[common.SelfRegistration].(bool),
RegistryURL: registryURL,
HasCARoot: caStatErr == nil,
HarborVersion: harborVersion,
}
sia.Data["json"] = info
sia.ServeJSON()
}
// GetVersion gets harbor version.
func (sia *SystemInfoAPI) getVersion() string {
version, err := ioutil.ReadFile(harborVersionFile)
if err != nil {
log.Errorf("Error occured getting harbor version: %v", err)
return ""
}
return string(version[:])
}

View File

@ -3,8 +3,9 @@ package api
import (
"encoding/json"
"fmt"
"github.com/stretchr/testify/assert"
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetVolumeInfo(t *testing.T) {
@ -49,6 +50,7 @@ func TestGetGeneralInfo(t *testing.T) {
assert.Nil(err, fmt.Sprintf("Unexpected Error: %v", err))
assert.Equal(false, g.WithNotary, "with notary should be false")
assert.Equal(true, g.HasCARoot, "has ca root should be true")
assert.NotEmpty(g.HarborVersion, "harbor version should not be empty")
}
func TestGetCert(t *testing.T) {

View File

@ -6,7 +6,7 @@ import (
"os"
"testing"
"github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
@ -15,24 +15,24 @@ import (
)
var adminServerLdapTestConfig = map[string]interface{}{
config.ExtEndpoint: "host01.com",
config.AUTHMode: "ldap_auth",
config.DatabaseType: "mysql",
config.MySQLHost: "127.0.0.1",
config.MySQLPort: 3306,
config.MySQLUsername: "root",
config.MySQLPassword: "root123",
config.MySQLDatabase: "registry",
config.SQLiteFile: "/tmp/registry.db",
common.ExtEndpoint: "host01.com",
common.AUTHMode: "ldap_auth",
common.DatabaseType: "mysql",
common.MySQLHost: "127.0.0.1",
common.MySQLPort: 3306,
common.MySQLUsername: "root",
common.MySQLPassword: "root123",
common.MySQLDatabase: "registry",
common.SQLiteFile: "/tmp/registry.db",
//config.SelfRegistration: true,
config.LDAPURL: "ldap://127.0.0.1",
config.LDAPSearchDN: "cn=admin,dc=example,dc=com",
config.LDAPSearchPwd: "admin",
config.LDAPBaseDN: "dc=example,dc=com",
config.LDAPUID: "uid",
config.LDAPFilter: "",
config.LDAPScope: 3,
config.LDAPTimeout: 30,
common.LDAPURL: "ldap://127.0.0.1",
common.LDAPSearchDN: "cn=admin,dc=example,dc=com",
common.LDAPSearchPwd: "admin",
common.LDAPBaseDN: "dc=example,dc=com",
common.LDAPUID: "uid",
common.LDAPFilter: "",
common.LDAPScope: 3,
common.LDAPTimeout: 30,
// config.TokenServiceURL: "",
// config.RegistryURL: "",
// config.EmailHost: "",
@ -46,10 +46,10 @@ var adminServerLdapTestConfig = map[string]interface{}{
// config.VerifyRemoteCert: false,
// config.MaxJobWorkers: 3,
// config.TokenExpiration: 30,
config.CfgExpiration: 5,
common.CfgExpiration: 5,
// config.JobLogDir: "/var/log/jobs",
// config.UseCompressedJS: true,
config.AdminInitialPassword: "password",
common.AdminInitialPassword: "password",
}
func TestMain(t *testing.T) {

View File

@ -16,20 +16,28 @@
package config
import (
"encoding/json"
"fmt"
"os"
"strings"
"github.com/vmware/harbor/src/adminserver/client"
"github.com/vmware/harbor/src/adminserver/client/auth"
"github.com/vmware/harbor/src/common"
comcfg "github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
)
const defaultKeyPath string = "/etc/ui/key"
const (
defaultKeyPath string = "/etc/ui/key"
secretCookieName string = "secret"
)
var (
mg *comcfg.Manager
keyProvider comcfg.KeyProvider
// AdminserverClient is a client for adminserver
AdminserverClient client.Client
mg *comcfg.Manager
keyProvider comcfg.KeyProvider
)
// Init configurations
@ -41,14 +49,17 @@ func Init() error {
if len(adminServerURL) == 0 {
adminServerURL = "http://adminserver"
}
log.Debugf("admin server URL: %s", adminServerURL)
mg = comcfg.NewManager(adminServerURL, UISecret(), true)
if err := mg.Init(); err != nil {
return err
log.Infof("initializing client for adminserver %s ...", adminServerURL)
authorizer := auth.NewSecretAuthorizer(secretCookieName, UISecret())
AdminserverClient = client.NewClient(adminServerURL, authorizer)
if err := AdminserverClient.Ping(); err != nil {
return fmt.Errorf("failed to ping adminserver: %v", err)
}
if _, err := mg.Load(); err != nil {
mg = comcfg.NewManager(AdminserverClient, true)
if err := Load(); err != nil {
return err
}
@ -78,26 +89,12 @@ func Reset() error {
// 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)
return mg.Upload(cfg)
}
// 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
return mg.Load()
}
// AuthMode ...
@ -106,7 +103,7 @@ func AuthMode() (string, error) {
if err != nil {
return "", err
}
return cfg[comcfg.AUTHMode].(string), nil
return cfg[common.AUTHMode].(string), nil
}
// LDAP returns the setting of ldap server
@ -117,14 +114,14 @@ func LDAP() (*models.LDAP, error) {
}
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))
ldap.URL = cfg[common.LDAPURL].(string)
ldap.SearchDN = cfg[common.LDAPSearchDN].(string)
ldap.SearchPassword = cfg[common.LDAPSearchPwd].(string)
ldap.BaseDN = cfg[common.LDAPBaseDN].(string)
ldap.UID = cfg[common.LDAPUID].(string)
ldap.Filter = cfg[common.LDAPFilter].(string)
ldap.Scope = int(cfg[common.LDAPScope].(float64))
ldap.Timeout = int(cfg[common.LDAPTimeout].(float64))
return ldap, nil
}
@ -135,7 +132,7 @@ func TokenExpiration() (int, error) {
if err != nil {
return 0, err
}
return int(cfg[comcfg.TokenExpiration].(float64)), nil
return int(cfg[common.TokenExpiration].(float64)), nil
}
// ExtEndpoint returns the external URL of Harbor: protocol://host:port
@ -144,7 +141,7 @@ func ExtEndpoint() (string, error) {
if err != nil {
return "", err
}
return cfg[comcfg.ExtEndpoint].(string), nil
return cfg[common.ExtEndpoint].(string), nil
}
// ExtURL returns the external URL: host:port
@ -171,7 +168,7 @@ func SelfRegistration() (bool, error) {
if err != nil {
return false, err
}
return cfg[comcfg.SelfRegistration].(bool), nil
return cfg[common.SelfRegistration].(bool), nil
}
// RegistryURL ...
@ -180,7 +177,7 @@ func RegistryURL() (string, error) {
if err != nil {
return "", err
}
return cfg[comcfg.RegistryURL].(string), nil
return cfg[common.RegistryURL].(string), nil
}
// InternalJobServiceURL returns jobservice URL for internal communication between Harbor containers
@ -205,7 +202,7 @@ func InitialAdminPassword() (string, error) {
if err != nil {
return "", err
}
return cfg[comcfg.AdminInitialPassword].(string), nil
return cfg[common.AdminInitialPassword].(string), nil
}
// OnlyAdminCreateProject returns the flag to restrict that only sys admin can create project
@ -214,7 +211,7 @@ func OnlyAdminCreateProject() (bool, error) {
if err != nil {
return true, err
}
return cfg[comcfg.ProjectCreationRestriction].(string) == comcfg.ProCrtRestrAdmOnly, nil
return cfg[common.ProjectCreationRestriction].(string) == common.ProCrtRestrAdmOnly, nil
}
// VerifyRemoteCert returns bool value.
@ -223,7 +220,7 @@ func VerifyRemoteCert() (bool, error) {
if err != nil {
return true, err
}
return cfg[comcfg.VerifyRemoteCert].(bool), nil
return cfg[common.VerifyRemoteCert].(bool), nil
}
// Email returns email server settings
@ -234,13 +231,13 @@ func Email() (*models.Email, error) {
}
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)
email.Host = cfg[common.EmailHost].(string)
email.Port = int(cfg[common.EmailPort].(float64))
email.Username = cfg[common.EmailUsername].(string)
email.Password = cfg[common.EmailPassword].(string)
email.SSL = cfg[common.EmailSSL].(bool)
email.From = cfg[common.EmailFrom].(string)
email.Identity = cfg[common.EmailIdentity].(string)
return email, nil
}
@ -252,16 +249,16 @@ func Database() (*models.Database, error) {
return nil, err
}
database := &models.Database{}
database.Type = cfg[comcfg.DatabaseType].(string)
database.Type = cfg[common.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)
mysql.Host = cfg[common.MySQLHost].(string)
mysql.Port = int(cfg[common.MySQLPort].(float64))
mysql.Username = cfg[common.MySQLUsername].(string)
mysql.Password = cfg[common.MySQLPassword].(string)
mysql.Database = cfg[common.MySQLDatabase].(string)
database.MySQL = mysql
sqlite := &models.SQLite{}
sqlite.File = cfg[comcfg.SQLiteFile].(string)
sqlite.File = cfg[common.SQLiteFile].(string)
database.SQLite = sqlite
return database, nil
@ -286,7 +283,7 @@ func WithNotary() bool {
log.Errorf("Failed to get configuration, will return WithNotary == false")
return false
}
return cfg[comcfg.WithNotary].(bool)
return cfg[common.WithNotary].(bool)
}
// AdmiralEndpoint returns the URL of admiral, if Harbor is not deployed with admiral it should return an empty string.
@ -297,10 +294,10 @@ func AdmiralEndpoint() string {
return ""
}
if e, ok := cfg[comcfg.AdmiralEndpoint].(string); !ok || e == "NA" {
cfg[comcfg.AdmiralEndpoint] = ""
if e, ok := cfg[common.AdmiralEndpoint].(string); !ok || e == "NA" {
cfg[common.AdmiralEndpoint] = ""
}
return cfg[comcfg.AdmiralEndpoint].(string)
return cfg[common.AdmiralEndpoint].(string)
}
// WithAdmiral returns a bool to indicate if Harbor's deployed with admiral.

View File

@ -9,7 +9,7 @@ import (
"github.com/astaxie/beego"
"github.com/beego/i18n"
"github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils"
@ -113,7 +113,7 @@ func (cc *CommonController) SendEmail() {
message := new(bytes.Buffer)
harborURL := config.ExtEndpoint
harborURL := common.ExtEndpoint
if harborURL == "" {
harborURL = "localhost"
}

View File

@ -62,7 +62,7 @@ func initRouters() {
//API:
beego.Router("/api/search", &api.SearchAPI{})
beego.Router("/api/projects/:pid([0-9]+)/members/?:mid", &api.ProjectMemberAPI{})
beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List;post:Post")
beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List;post:Post;head:Head")
beego.Router("/api/projects/:id", &api.ProjectAPI{})
beego.Router("/api/projects/:id/publicity", &api.ProjectAPI{}, "put:ToggleProjectPublic")
beego.Router("/api/statistics", &api.StatisticAPI{})

View File

@ -7,11 +7,11 @@
<div class="form-group form-group-override">
<label for="oldPassword" class="required form-group-label-override">{{'CHANGE_PWD.CURRENT_PWD' | translate}}</label>
<label for="oldPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]="oldPassInput.invalid && (oldPassInput.dirty || oldPassInput.touched)">
<input type="password" id="oldPassword" placeholder='{{"PLACEHOLDER.CURRENT_PWD" | translate}}'
<input type="password" id="oldPassword"
required
name="oldPassword"
[(ngModel)]="oldPwd"
#oldPassInput="ngModel" size="30">
#oldPassInput="ngModel" size="42">
<span class="tooltip-content">
{{'TOOLTIP.CURRENT_PWD' | translate}}
</span>
@ -19,27 +19,32 @@
</div>
<div class="form-group form-group-override">
<label for="newPassword" class="required form-group-label-override">{{'CHANGE_PWD.NEW_PWD' | translate}}</label>
<label for="newPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]="newPassInput.invalid && (newPassInput.dirty || newPassInput.touched)">
<input type="password" id="newPassword" placeholder='{{"PLACEHOLDER.NEW_PWD" | translate}}'
<label for="newPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]='!getValidationState("newPassword")'>
<input type="password" id="newPassword"
required
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{7,}$"
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$"
name="newPassword"
[(ngModel)]="newPwd"
#newPassInput="ngModel" size="30">
#newPassInput="ngModel" size="42"
(input)='handleValidation("newPassword", false)'
(focusout)='handleValidation("newPassword", true)'>
<span class="tooltip-content">
{{'TOOLTIP.PASSWORD' | translate}}
</span>
</label>
<label class="sub-label-for-input">{{'CHANGE_PWD.PASS_TIPS' | translate}}</label>
</div>
<div class="form-group form-group-override">
<label for="reNewPassword" class="required form-group-label-override">{{'CHANGE_PWD.CONFIRM_PWD' | translate}}</label>
<label for="reNewPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]="(reNewPassInput.invalid && (reNewPassInput.dirty || reNewPassInput.touched)) || (!newPassInput.invalid && reNewPassInput.value != newPassInput.value)">
<input type="password" id="reNewPassword" placeholder='{{"PLACEHOLDER.CONFIRM_PWD" | translate}}'
<label for="reNewPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]='!getValidationState("reNewPassword")'>
<input type="password" id="reNewPassword"
required
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{7,}$"
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$"
name="reNewPassword"
[(ngModel)]="reNewPwd"
#reNewPassInput="ngModel" size="30">
#reNewPassInput="ngModel" size="42"
(input)='handleValidation("reNewPassword", false)'
(focusout)='handleValidation("reNewPassword", true)'>
<span class="tooltip-content">
{{'TOOLTIP.CONFIRM_PWD' | translate}}
</span>

View File

@ -23,6 +23,10 @@ export class PasswordSettingComponent implements AfterViewChecked {
private formValueChanged: boolean = false;
private onCalling: boolean = false;
private validationStateMap: any = {
"newPassword": true,
"reNewPassword": true
};
pwdFormRef: NgForm;
@ViewChild("changepwdForm") pwdForm: NgForm;
@ -52,6 +56,29 @@ export class PasswordSettingComponent implements AfterViewChecked {
return this.onCalling;
}
private getValidationState(key: string): boolean {
return this.validationStateMap[key];
}
private handleValidation(key: string, flag: boolean): void {
if (flag) {
//Checking
let cont = this.pwdForm.controls[key];
if (cont) {
this.validationStateMap[key] = cont.valid;
if(key === "reNewPassword" && cont.valid){
let compareCont = this.pwdForm.controls["newPassword"];
if(compareCont){
this.validationStateMap[key]= cont.value === compareCont.value;
}
}
}
} else {
//Reset
this.validationStateMap[key] = true;
}
}
ngAfterViewChecked() {
if (this.pwdFormRef != this.pwdForm) {
this.pwdFormRef = this.pwdForm;

View File

@ -7,7 +7,6 @@ import { CookieService } from 'angular2-cookie/core';
import { CookieKeyOfAdmiral, HarborQueryParamKey } from './shared/shared.const';
import { maintainUrlQueryParmas } from './shared/shared.utils';
export const systemInfoEndpoint = "/api/systeminfo";
/**
* Declare service to handle the bootstrap options
@ -50,7 +49,6 @@ export class AppConfigService {
//Catch the error
console.error("Failed to load bootstrap options with error: ", error);
});
}
public getConfig(): AppConfig {

View File

@ -1,7 +1,5 @@
import { modalEvents } from './modal-events.const'
//Define a object to store the modal event
export class ModalEvent {
modalName: modalEvents;
modalName: string;
modalFlag: boolean; //true for open, false for close
}

View File

@ -7,7 +7,7 @@
</div>
<div class="header-nav">
<a href="{{admiralLink}}" class="nav-link" *ngIf="isIntegrationMode"><span class="nav-text">Management</span></a>
<a href="javascript:void(0)" routerLink="/harbor" class="active nav-link" *ngIf="isIntegrationMode"><span class="nav-text">Registry</span></a>
<a href="javascript:void(0)" (click)="registryAction()" routerLink="/harbor" class="active nav-link" *ngIf="isIntegrationMode"><span class="nav-text">Registry</span></a>
</div>
<global-search></global-search>
<div class="header-actions">

View File

@ -9,9 +9,11 @@ import { SessionUser } from '../../shared/session-user';
import { SessionService } from '../../shared/session.service';
import { CookieService } from 'angular2-cookie/core';
import { supportedLangs, enLang, languageNames, CommonRoutes } from '../../shared/shared.const';
import { supportedLangs, enLang, languageNames, CommonRoutes, AlertType } from '../../shared/shared.const';
import { errorHandler } from '../../shared/shared.utils';
import { AppConfigService } from '../../app-config.service';
import { SearchTriggerService } from '../global-search/search-trigger.service';
import { MessageService } from '../../global-message/message.service';
@Component({
selector: 'navigator',
@ -31,7 +33,9 @@ export class NavigatorComponent implements OnInit {
private router: Router,
private translate: TranslateService,
private cookie: CookieService,
private appConfigService: AppConfigService) { }
private appConfigService: AppConfigService,
private msgService: MessageService,
private searchTrigger: SearchTriggerService) { }
ngOnInit(): void {
this.selectedLang = this.translate.currentLang;
@ -98,8 +102,10 @@ export class NavigatorComponent implements OnInit {
this.router.navigate([CommonRoutes.EMBEDDED_SIGN_IN]);
})
.catch(error => {
console.error("Log out with error: ", error);
this.msgService.announceMessage(error.status | 500, errorHandler(error), AlertType.WARNING);
});
//Confirm search result panel is close
this.searchTrigger.closeSearch(false);
}
//Switch languages
@ -124,5 +130,12 @@ export class NavigatorComponent implements OnInit {
//Naviagte to signin page
this.router.navigate([CommonRoutes.HARBOR_ROOT]);
}
//Confirm search result panel is close
this.searchTrigger.closeSearch(false);
}
registryAction(): void {
this.searchTrigger.closeSearch(false);
}
}

View File

@ -5,4 +5,12 @@
.form-group-label-override {
font-size: 14px;
font-weight: 400;
}
.sub-label-for-input {
position: absolute;
top: 26px;
font-size: 10px;
font-weight: 400;
line-height: 12px;
}

View File

@ -1,23 +1,26 @@
<clr-modal [(clrModalOpen)]="createProjectOpened">
<clr-modal [(clrModalOpen)]="createProjectOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{'PROJECT.NEW_PROJECT' | translate}}</h3>
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
<div class="modal-body">
<form #projectForm="ngForm">
<section class="form-block">
<div class="form-group">
<label for="create_project_name" class="col-md-4">{{'PROJECT.NAME' | translate}}</label>
<label for="create_project_name" class="col-md-4 form-group-label-override">{{'PROJECT.NAME' | translate}}</label>
<label for="create_project_name" aria-haspopup="true" role="tooltip" [class.invalid]="projectName.invalid && (projectName.dirty || projectName.touched)" [class.valid]="projectName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="create_project_name" [(ngModel)]="project.name" name="name" size="20" required minlength="2" #projectName="ngModel">
<input type="text" id="create_project_name" [(ngModel)]="project.name" name="name" size="20" required minlength="2" #projectName="ngModel" targetExists="PROJECT_NAME">
<span class="tooltip-content" *ngIf="projectName.errors && projectName.errors.required && (projectName.dirty || projectName.touched)">
{{'PROJECT.NAME_IS_REQUIRED' | translate}}
</span>
<span class="tooltip-content" *ngIf="projectName.errors && projectName.errors.minlength && (projectName.dirty || projectName.touched)">
{{'PROJECT.NAME_MINIMUM_LENGTH' | translate}}
</span>
<span class="tooltip-content" *ngIf="projectName.errors && projectName.errors.targetExists && (projectName.dirty || projectName.touched)">
{{'PROJECT.NAME_ALREADY_EXISTS' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label class="col-md-4">{{'PROJECT.PUBLIC_OR_PRIVATE' | translate}}</label>
<label class="col-md-4 form-group-label-override">{{'PROJECT.PUBLIC_OR_PRIVATE' | translate}}</label>
<div class="checkbox-inline">
<input type="checkbox" id="create_project_public" [(ngModel)]="project.public" name="public">
<label for="create_project_public"></label>

View File

@ -1,5 +1,4 @@
import { Component, EventEmitter, Output, ViewChild, AfterViewChecked } from '@angular/core';
import { Component, EventEmitter, Output, ViewChild, AfterViewChecked, HostBinding } from '@angular/core';
import { Response } from '@angular/http';
import { NgForm } from '@angular/forms';
@ -14,6 +13,7 @@ import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.com
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'create-project',
templateUrl: 'create-project.component.html',
@ -27,12 +27,15 @@ export class CreateProjectComponent implements AfterViewChecked {
currentForm: NgForm;
project: Project = new Project();
initVal: Project = new Project();
createProjectOpened: boolean;
hasChanged: boolean;
staticBackdrop: boolean = true;
closable: boolean = false;
@Output() create = new EventEmitter<boolean>();
@ViewChild(InlineAlertComponent)
private inlineAlert: InlineAlertComponent;
@ -75,7 +78,9 @@ export class CreateProjectComponent implements AfterViewChecked {
this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'});
} else {
this.createProjectOpened = false;
this.projectForm.reset();
}
}
ngAfterViewChecked(): void {
@ -83,17 +88,14 @@ export class CreateProjectComponent implements AfterViewChecked {
if(this.projectForm) {
this.projectForm.valueChanges.subscribe(data=>{
for(let i in data) {
let item = data[i];
if(typeof item === 'string' && (<string>item).trim().length !== 0) {
this.hasChanged = true;
break;
} else if (typeof item === 'boolean' && (<boolean>item)) {
let origin = this.initVal[i];
let current = data[i];
if(current && current !== origin) {
this.hasChanged = true;
break;
} else {
this.hasChanged = false;
this.inlineAlert.close();
break;
}
}
});
@ -109,7 +111,7 @@ export class CreateProjectComponent implements AfterViewChecked {
confirmCancel(event: boolean): void {
this.createProjectOpened = false;
this.inlineAlert.close();
this.projectForm.reset();
}
}

View File

@ -1,4 +1,4 @@
<clr-datagrid (clrDgRefresh)="refresh($event)" >
<clr-datagrid (clrDgRefresh)="refresh($event)">
<clr-dg-column>{{'PROJECT.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.PUBLIC_OR_PRIVATE' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.REPO_COUNT'| translate}}</clr-dg-column>
@ -19,5 +19,5 @@
<clr-dg-footer>
{{totalRecordCount || (projects ? projects.length : 0)}} {{'PROJECT.ITEMS' | translate}}
<clr-dg-pagination [clrDgPageSize]="pageOffset" [clrDgTotalItems]="totalPage"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</clr-dg-footer>
</clr-datagrid>

View File

@ -1,27 +1,23 @@
<clr-modal [(clrModalOpen)]="addMemberOpened">
<clr-modal [(clrModalOpen)]="addMemberOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{'MEMBER.NEW_MEMBER' | translate}}</h3>
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
<div class="modal-body">
<form #memberForm="ngForm">
<section class="form-block">
<clr-alert [clrAlertType]="'alert-danger'" [(clrAlertClosed)]="!errorMessageOpened" (clrAlertClosedChange)="onErrorMessageClose()">
<div class="alert-item">
<span class="alert-text">
{{errorMessage}}
</span>
</div>
</clr-alert>
<div class="form-group">
<label for="member_name" class="col-md-4">{{'MEMBER.NAME' | translate}}</label>
<label for="member_name" class="col-md-4 form-group-label-override">{{'MEMBER.NAME' | translate}}</label>
<label for="member_name" aria-haspopup="true" role="tooltip" [class.invalid]="memberName.invalid && (memberName.dirty || memberName.touched)" [class.valid]="memberName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="member_name" [(ngModel)]="member.username" name="name" size="20" #memberName="ngModel" required>
<input type="text" id="member_name" [(ngModel)]="member.username" name="name" size="20" #memberName="ngModel" required targetExists="MEMBER_NAME" [projectId]="projectId">
<span class="tooltip-content" *ngIf="memberName.errors && memberName.errors.required && (memberName.dirty || memberName.touched)">
Username is required.
{{ 'MEMBER.USERNAME_IS_REQUIRED' | translate }}
</span>
<span class="tooltip-content" *ngIf="memberName.errors && memberName.errors.targetExists && (memberName.dirty || memberName.touched)">
{{ 'MEMBER.USERNAME_ALREADY_EXISTS' | translate }}
</span>
</label>
</div>
<div class="form-group">
<label class="col-md-4">{{'MEMBER.ROLE' | translate}}</label>
<label class="col-md-4 form-group-label-override">{{'MEMBER.ROLE' | translate}}</label>
<div class="radio">
<input type="radio" name="roleRadios" id="checkrads_project_admin" value="1" [(ngModel)]="member.role_id">
<label for="checkrads_project_admin">{{'MEMBER.PROJECT_ADMIN' | translate}}</label>

View File

@ -20,10 +20,15 @@ import { Member } from '../member';
export class AddMemberComponent implements AfterViewChecked {
member: Member = new Member();
initVal: Member = new Member();
addMemberOpened: boolean;
memberForm: NgForm;
staticBackdrop: boolean = true;
closable: boolean = false;
@ViewChild('memberForm')
currentForm: NgForm;
@ -40,6 +45,7 @@ export class AddMemberComponent implements AfterViewChecked {
private translateService: TranslateService) {}
onSubmit(): void {
if(!this.member.username || this.member.username.length === 0) { return; }
console.log('Adding member:' + JSON.stringify(this.member));
this.memberService
.addMember(this.projectId, this.member.username, +this.member.role_id)
@ -76,6 +82,7 @@ export class AddMemberComponent implements AfterViewChecked {
this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'});
} else {
this.addMemberOpened = false;
this.memberForm.reset();
}
}
@ -83,21 +90,15 @@ export class AddMemberComponent implements AfterViewChecked {
this.memberForm = this.currentForm;
if(this.memberForm) {
this.memberForm.valueChanges.subscribe(data=>{
for(let i in data) {
let item = data[i];
if(typeof item === 'string' && (<string>item).trim().length !== 0) {
this.hasChanged = true;
break;
} else if (typeof item === 'boolean' && (<boolean>item)) {
this.hasChanged = true;
break;
} else if (typeof item === 'number' && (<number>item) !== 0) {
for(let i in data) {
let origin = this.initVal[i];
let current = data[i];
if(current && current !== origin) {
this.hasChanged = true;
break;
} else {
this.hasChanged = false;
this.inlineAlert.close();
break;
}
}
});
@ -107,6 +108,7 @@ export class AddMemberComponent implements AfterViewChecked {
confirmCancel(confirmed: boolean) {
this.addMemberOpened = false;
this.inlineAlert.close();
this.memberForm.reset();
}
openAddMemberModal(): void {

View File

@ -5,15 +5,15 @@
<li class="nav-item">
<a class="nav-link" routerLink="repository" routerLinkActive="active">{{'PROJECT_DETAIL.REPOSITORIES' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSessionValid && isSystemAdmin">
<a class="nav-link" routerLink="replication" routerLinkActive="active">{{'PROJECT_DETAIL.REPLICATION' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSessionValid">
<a class="nav-link" routerLink="member" routerLinkActive="active">{{'PROJECT_DETAIL.USERS' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSessionValid">
<a class="nav-link" routerLink="log" routerLinkActive="active">{{'PROJECT_DETAIL.LOGS' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSessionValid && isSystemAdmin">
<a class="nav-link" routerLink="replication" routerLinkActive="active">{{'PROJECT_DETAIL.REPLICATION' | translate}}</a>
</li>
</ul>
</nav>
<router-outlet></router-outlet>

View File

@ -51,7 +51,7 @@ export class ProjectComponent implements OnInit, OnDestroy {
isPublic: number;
page: number = 1;
pageSize: number = 3;
pageSize: number = 15;
totalPage: number;
totalRecordCount: number;

View File

@ -18,6 +18,8 @@ import { ProjectService } from './project.service';
import { MemberService } from './member/member.service';
import { ProjectRoutingResolver } from './project-routing-resolver.service';
import { TargetExistsValidatorDirective } from '../shared/target-exists-directive';
@NgModule({
imports: [
SharedModule,
@ -32,7 +34,8 @@ import { ProjectRoutingResolver } from './project-routing-resolver.service';
ListProjectComponent,
ProjectDetailComponent,
MemberComponent,
AddMemberComponent
AddMemberComponent,
TargetExistsValidatorDirective
],
exports: [ProjectComponent, ListProjectComponent],
providers: [ProjectRoutingResolver, ProjectService, MemberService]

View File

@ -62,4 +62,12 @@ export class ProjectService {
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
checkProjectExists(projectName: string): Observable<any> {
return this.http
.head(`/api/projects/?project_name=${projectName}`)
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
}

View File

@ -1,18 +1,11 @@
<clr-modal [(clrModalOpen)]="createEditDestinationOpened">
<clr-modal [(clrModalOpen)]="createEditDestinationOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{modalTitle}}</h3>
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
<div class="modal-body">
<form #targetForm="ngForm">
<section class="form-block">
<clr-alert [clrAlertType]="'alert-danger'" [(clrAlertClosed)]="!errorMessageOpened" (clrAlertClosedChange)="onErrorMessageClose()">
<div class="alert-item">
<span class="alert-text">
{{errorMessage}}
</span>
</div>
</clr-alert>
<div class="form-group">
<label for="destination_name" class="col-md-4">{{ 'DESTINATION.NAME' | translate }}<span style="color: red">*</span></label>
<label for="destination_name" class="col-md-4 form-group-label-override">{{ 'DESTINATION.NAME' | translate }}<span style="color: red">*</span></label>
<label class="col-md-8" for="destination_name" aria-haspopup="true" role="tooltip" [class.invalid]="targetName.errors && (targetName.dirty || targetName.touched)" [class.valid]="targetName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="destination_name" [disabled]="testOngoing" [(ngModel)]="target.name" name="targetName" size="20" #targetName="ngModel" value="" required>
<span class="tooltip-content" *ngIf="targetName.errors && targetName.errors.required && (targetName.dirty || targetName.touched)">
@ -21,7 +14,7 @@
</label>
</div>
<div class="form-group">
<label for="destination_url" class="col-md-4">{{ 'DESTINATION.URL' | translate }}<span style="color: red">*</span></label>
<label for="destination_url" class="col-md-4 form-group-label-override">{{ 'DESTINATION.URL' | translate }}<span style="color: red">*</span></label>
<label class="col-md-8" for="destination_url" aria-haspopup="true" role="tooltip" [class.invalid]="targetEndpoint.errors && (targetEndpoint.dirty || targetEndpoint.touched)" [class.valid]="targetEndpoint.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="destination_url" [disabled]="testOngoing" [(ngModel)]="target.endpoint" size="20" name="endpointUrl" #targetEndpoint="ngModel" required>
<span class="tooltip-content" *ngIf="targetEndpoint.errors && targetEndpoint.errors.required && (targetEndpoint.dirty || targetEndpoint.touched)">
@ -30,17 +23,17 @@
</label>
</div>
<div class="form-group">
<label for="destination_username" class="col-md-4">{{ 'DESTINATION.USERNAME' | translate }}</label>
<label for="destination_username" class="col-md-4 form-group-label-override">{{ 'DESTINATION.USERNAME' | translate }}</label>
<input type="text" class="col-md-8" id="destination_username" [disabled]="testOngoing" [(ngModel)]="target.username" size="20" name="username" #username="ngModel">
</div>
<div class="form-group">
<label for="destination_password" class="col-md-4">{{ 'DESTINATION.PASSWORD' | translate }}</label>
<label for="destination_password" class="col-md-4 form-group-label-override">{{ 'DESTINATION.PASSWORD' | translate }}</label>
<input type="password" class="col-md-8" id="destination_password" [disabled]="testOngoing" [(ngModel)]="target.password" size="20" name="password" #password="ngModel">
</div>
<div class="form-group">
<label for="spin" class="col-md-4"></label>
<span class="col-md-8 spinner spinner-inline" [hidden]="!testOngoing"></span>
<span [style.color]="!pingStatus ? 'red': ''">{{ pingTestMessage }}</span>
<span [style.color]="!pingStatus ? 'red': ''" class="form-group-label-override">{{ pingTestMessage }}</span>
</div>
</section>
</form>

View File

@ -15,7 +15,7 @@ import { TranslateService } from '@ngx-translate/core';
selector: 'create-edit-destination',
templateUrl: './create-edit-destination.component.html'
})
export class CreateEditDestinationComponent {
export class CreateEditDestinationComponent implements AfterViewChecked {
modalTitle: string;
createEditDestinationOpened: boolean;
@ -27,9 +27,13 @@ export class CreateEditDestinationComponent {
actionType: ActionType;
target: Target = new Target();
initVal: Target = new Target();
targetForm: NgForm;
staticBackdrop: boolean = true;
closable: boolean = false;
@ViewChild('targetForm')
currentForm: NgForm;
@ -47,7 +51,6 @@ export class CreateEditDestinationComponent {
openCreateEditTarget(targetId?: number) {
this.target = new Target();
this.createEditDestinationOpened = true;
this.hasChanged = false;
@ -62,7 +65,13 @@ export class CreateEditDestinationComponent {
this.replicationService
.getTarget(targetId)
.subscribe(
target=>this.target=target,
target=>{
this.target = target;
this.initVal.name = this.target.name;
this.initVal.endpoint = this.target.endpoint;
this.initVal.username = this.target.username;
this.initVal.password = this.target.password;
},
error=>this.messageService
.announceMessage(error.status, 'DESTINATION.FAILED_TO_GET_TARGET', AlertType.DANGER)
);
@ -171,22 +180,26 @@ export class CreateEditDestinationComponent {
this.inlineAlert.close();
}
mappedName: {} = {
'targetName': 'name',
'endpointUrl': 'endpoint',
'username': 'username',
'password': 'password'
};
ngAfterViewChecked(): void {
this.targetForm = this.currentForm;
if(this.targetForm) {
this.targetForm.valueChanges.subscribe(data=>{
for(let i in data) {
let item = data[i];
if(typeof item === 'string' && (<string>item).trim().length !== 0) {
this.hasChanged = true;
break;
} else if (typeof item === 'boolean' && (<boolean>item)) {
let current = data[i];
let origin = this.initVal[this.mappedName[i]];
if(current && current !== origin) {
this.hasChanged = true;
break;
} else {
this.hasChanged = false;
this.inlineAlert.close();
break;
}
}
});

View File

@ -16,8 +16,8 @@
<clr-dg-cell>{{t.tag}}</clr-dg-cell>
<clr-dg-cell>{{t.pullCommand}}</clr-dg-cell>
<clr-dg-cell>
<clr-icon shape="check" *ngIf="t.verified" style="color: #1D5100;"></clr-icon>
<clr-icon shape="close" *ngIf="!t.verified" style="color: #C92100;"></clr-icon>
<clr-icon shape="check" *ngIf="t.signed" style="color: #1D5100;"></clr-icon>
<clr-icon shape="close" *ngIf="!t.signed" style="color: #C92100;"></clr-icon>
</clr-dg-cell>
<clr-dg-cell>{{t.author}}</clr-dg-cell>
<clr-dg-cell>{{t.created | date: 'yyyy/MM/dd'}}</clr-dg-cell>

View File

@ -26,6 +26,7 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
repoName: string;
tags: TagView[];
registryUrl: string;
private subscription: Subscription;
@ -66,6 +67,7 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
this.projectId = this.route.snapshot.params['id'];
this.repoName = this.route.snapshot.params['repo'];
this.tags = [];
this.registryUrl = this.appConfigService.getConfig().registry_url;
this.retrieve();
}
@ -87,10 +89,10 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
let data = JSON.parse(t.manifest.history[0].v1Compatibility);
tag.architecture = data['architecture'];
tag.author = data['author'];
tag.verified = t.signed;
tag.signed = t.signed;
tag.created = data['created'];
tag.dockerVersion = data['docker_version'];
tag.pullCommand = 'docker pull ' + t.manifest.name + ':' + t.tag;
tag.pullCommand = 'docker pull ' + this.registryUrl + '/' + t.manifest.name + ':' + t.tag;
tag.os = data['os'];
this.tags.push(tag);
});
@ -100,18 +102,20 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
deleteTag(tag: TagView) {
if (tag) {
let titleKey: string, summaryKey: string;
if (tag.verified) {
let titleKey: string, summaryKey: string, content: string;
if (tag.signed) {
titleKey = 'REPOSITORY.DELETION_TITLE_TAG_DENIED';
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG_DENIED';
content = 'notary -s https://' + this.registryUrl + ' -d ~/.docker/trust remove -p ' + this.registryUrl + '/' + this.repoName + ':' + tag.tag;
} else {
titleKey = 'REPOSITORY.DELETION_TITLE_TAG';
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG';
content = tag.tag;
}
let message = new ConfirmationMessage(
titleKey,
summaryKey,
tag.tag,
content,
tag,
ConfirmationTargets.TAG);
this.deletionDialogService.openComfirmDialog(message);

View File

@ -1,7 +1,7 @@
export class TagView {
tag: string;
pullCommand: string;
verified: boolean;
signed: boolean;
author: string;
created: Date;
dockerVersion: string;

View File

@ -1,75 +1,68 @@
<clr-modal [(clrModalOpen)]="createEditPolicyOpened">
<clr-modal [(clrModalOpen)]="createEditPolicyOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{modalTitle}}</h3>
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
<div class="modal-body">
<form #policyForm="ngForm">
<section class="form-block">
<clr-alert [clrAlertType]="'alert-danger'" [(clrAlertClosed)]="!errorMessageOpened" (clrAlertClosedChange)="onErrorMessageClose()">
<div class="alert-item">
<span class="alert-text">
{{errorMessage}}
</span>
</div>
</clr-alert>
<div class="form-group">
<label for="policy_name" class="col-md-4">{{'REPLICATION.NAME' | translate}}<span style="color: red">*</span></label>
<label for="policy_name" class="col-md-4 form-group-label-override">{{'REPLICATION.NAME' | translate}}<span style="color: red">*</span></label>
<label for="policy_name" class="col-md-8" aria-haspopup="true" role="tooltip" [class.invalid]="name.errors && (name.dirty || name.touched)" [class.valid]="name.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="policy_name" [(ngModel)]="createEditPolicy.name" name="name" #name="ngModel" required>
<input type="text" id="policy_name" [(ngModel)]="createEditPolicy.name" name="name" #name="ngModel" required [disabled]="readonly">
<span class="tooltip-content" *ngIf="name.errors && name.errors.required && (name.dirty || name.touched)">
{{'REPLICATION.NAME_IS_REQUIRED'}}
{{'REPLICATION.NAME_IS_REQUIRED' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="policy_description" class="col-md-4">{{'REPLICATION.DESCRIPTION' | translate}}</label>
<input type="text" class="col-md-8" id="policy_description" [(ngModel)]="createEditPolicy.description" name="description" size="20" #description="ngModel">
<label for="policy_description" class="col-md-4 form-group-label-override">{{'REPLICATION.DESCRIPTION' | translate}}</label>
<input type="text" class="col-md-8" id="policy_description" [(ngModel)]="createEditPolicy.description" name="description" size="20" #description="ngModel" [disabled]="readonly">
</div>
<div class="form-group">
<label class="col-md-4">{{'REPLICATION.ENABLE' | translate}}</label>
<div class="checkbox-inline">
<input type="checkbox" id="policy_enable" [(ngModel)]="createEditPolicy.enable" name="enable" #enable="ngModel">
<input type="checkbox" id="policy_enable" [(ngModel)]="createEditPolicy.enable" name="enable" #enable="ngModel" [disabled]="untoggleable">
<label for="policy_enable"></label>
</div>
</div>
<div class="form-group">
<label for="destination_name" class="col-md-4">{{'REPLICATION.DESTINATION_NAME' | translate}}<span style="color: red">*</span></label>
<label for="destination_name" class="col-md-4 form-group-label-override">{{'REPLICATION.DESTINATION_NAME' | translate}}<span style="color: red">*</span></label>
<div class="select" *ngIf="!isCreateDestination">
<select id="destination_name" [(ngModel)]="createEditPolicy.targetId" name="targetId" (change)="selectTarget()" [disabled]="testOngoing">
<select id="destination_name" [(ngModel)]="createEditPolicy.targetId" name="targetId" (change)="selectTarget()" [disabled]="testOngoing || readonly">
<option *ngFor="let t of targets" [value]="t.id" [selected]="t.id == createEditPolicy.targetId">{{t.name}}</option>
</select>
</div>
<label class="col-md-8" *ngIf="isCreateDestination" for="destination_name" aria-haspopup="true" role="tooltip" [class.invalid]="targetName.errors && (targetName.dirty || targetName.touched)" [class.valid]="targetName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="destination_name" [(ngModel)]="createEditPolicy.targetName" name="targetName" size="20" #targetName="ngModel" value="" required>
<input type="text" id="destination_name" [(ngModel)]="createEditPolicy.targetName" name="targetName" size="8" #targetName="ngModel" value="" required>
<span class="tooltip-content" *ngIf="targetName.errors && targetName.errors.required && (targetName.dirty || targetName.touched)">
{{'REPLICATION.DESTINATION_NAME_IS_REQUIRED' | translate}}
</span>
</label>
<div class="checkbox-inline">
<input type="checkbox" id="check_new" (click)="newDestination(checkedAddNew.checked)" #checkedAddNew [checked]="isCreateDestination" [disabled]="testOngoing">
<div class="checkbox-inline" *ngIf="showNewDestination">
<input type="checkbox" id="check_new" (click)="newDestination(checkedAddNew.checked)" #checkedAddNew [checked]="isCreateDestination" [disabled]="testOngoing || readonly">
<label for="check_new">{{'REPLICATION.NEW_DESTINATION' | translate}}</label>
</div>
</div>
<div class="form-group">
<label for="destination_url" class="col-md-4">{{'REPLICATION.DESTINATION_URL' | translate}}<span style="color: red">*</span></label>
<label for="destination_url" class="col-md-4 form-group-label-override">{{'REPLICATION.DESTINATION_URL' | translate}}<span style="color: red">*</span></label>
<label for="destination_url" class="col-md-8" aria-haspopup="true" role="tooltip" [class.invalid]="endpointUrl.errors && (endpointUrl.dirty || endpointUrl.touched)" [class.valid]="endpointUrl.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="destination_url" [disabled]="testOngoing" [(ngModel)]="createEditPolicy.endpointUrl" size="20" name="endpointUrl" required #endpointUrl="ngModel">
<input type="text" id="destination_url" [disabled]="testOngoing || readonly" [(ngModel)]="createEditPolicy.endpointUrl" size="20" name="endpointUrl" required #endpointUrl="ngModel">
<span class="tooltip-content" *ngIf="endpointUrl.errors && endpointUrl.errors.required && (endpointUrl.dirty || endpointUrl.touched)">
{{'REPLICATION.DESTINATION_URL_IS_REQUIRED' | translate}}
</span>
</label>
</div>
<div class="form-group">
<label for="destination_username" class="col-md-4">{{'REPLICATION.DESTINATION_USERNAME' | translate}}</label>
<input type="text" class="col-md-8" id="destination_username" [disabled]="testOngoing" [(ngModel)]="createEditPolicy.username" size="20" name="username" #username="ngModel">
<label for="destination_username" class="col-md-4 form-group-label-override">{{'REPLICATION.DESTINATION_USERNAME' | translate}}</label>
<input type="text" class="col-md-8" id="destination_username" [disabled]="testOngoing || readonly" [(ngModel)]="createEditPolicy.username" size="20" name="username" #username="ngModel">
</div>
<div class="form-group">
<label for="destination_password" class="col-md-4">{{'REPLICATION.DESTINATION_PASSWORD' | translate}}</label>
<input type="password" class="col-md-8" id="destination_password" [disabled]="testOngoing" [(ngModel)]="createEditPolicy.password" size="20" name="password" #password="ngModel">
<label for="destination_password" class="col-md-4 form-group-label-override">{{'REPLICATION.DESTINATION_PASSWORD' | translate}}</label>
<input type="password" class="col-md-8" id="destination_password" [disabled]="testOngoing || readonly" [(ngModel)]="createEditPolicy.password" size="20" name="password" #password="ngModel">
</div>
<div class="form-group">
<label for="spin" class="col-md-4"></label>
<span class="col-md-8 spinner spinner-inline" [hidden]="!testOngoing"></span>
<span [style.color]="!pingStatus ? 'red': ''">{{ pingTestMessage }}</span>
<span [style.color]="!pingStatus ? 'red': ''" class="form-group-label-override">{{ pingTestMessage }}</span>
</div>
</section>
</form>

View File

@ -24,6 +24,7 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
modalTitle: string;
createEditPolicyOpened: boolean;
createEditPolicy: CreateEditPolicy = new CreateEditPolicy();
initVal: CreateEditPolicy = new CreateEditPolicy();
actionType: ActionType;
@ -40,6 +41,9 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
policyForm: NgForm;
staticBackdrop: boolean = true;
closable: boolean = false;
@ViewChild('policyForm')
currentForm: NgForm;
@ -48,6 +52,18 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
@ViewChild(InlineAlertComponent)
inlineAlert: InlineAlertComponent;
get readonly(): boolean {
return this.actionType === ActionType.EDIT && this.createEditPolicy.enable;
}
get untoggleable(): boolean {
return this.actionType === ActionType.EDIT && this.initVal.enable;
}
get showNewDestination(): boolean {
return this.actionType === ActionType.ADD_NEW || !this.createEditPolicy.enable;
}
constructor(
private replicationService: ReplicationService,
private messageService: MessageService,
@ -67,6 +83,11 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
this.createEditPolicy.endpointUrl = initialTarget.endpoint;
this.createEditPolicy.username = initialTarget.username;
this.createEditPolicy.password = initialTarget.password;
this.initVal.targetId = this.createEditPolicy.targetId;
this.initVal.endpointUrl = this.createEditPolicy.endpointUrl;
this.initVal.username = this.createEditPolicy.username;
this.initVal.password = this.createEditPolicy.password;
}
},
error=>this.messageService.announceMessage(error.status, 'Error occurred while get targets.', AlertType.DANGER)
@ -78,6 +99,7 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
openCreateEditPolicy(policyId?: number): void {
this.createEditPolicyOpened = true;
this.createEditPolicy = new CreateEditPolicy();
this.isCreateDestination = false;
this.hasChanged = false;
@ -97,7 +119,11 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
this.createEditPolicy.name = policy.name;
this.createEditPolicy.description = policy.description;
this.createEditPolicy.enable = policy.enabled === 1? true : false;
this.prepareTargets(policy.target_id);
this.prepareTargets(policy.target_id);
this.initVal.name = this.createEditPolicy.name;
this.initVal.description = this.createEditPolicy.description;
this.initVal.enable = this.createEditPolicy.enable;
}
)
} else {
@ -218,12 +244,14 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'});
} else {
this.createEditPolicyOpened = false;
this.policyForm.reset();
}
}
confirmCancel(confirmed: boolean) {
this.createEditPolicyOpened = false;
this.inlineAlert.close();
this.policyForm.reset();
}
ngAfterViewChecked(): void {
@ -231,24 +259,20 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
if(this.policyForm) {
this.policyForm.valueChanges.subscribe(data=>{
for(let i in data) {
let item = data[i];
if(typeof item === 'string' && (<string>item).trim().length !== 0) {
this.hasChanged = true;
break;
} else if (typeof item === 'boolean' && (<boolean>item)) {
let origin = this.initVal[i];
let current = data[i];
if(current && current !== origin) {
this.hasChanged = true;
break;
} else {
this.hasChanged = false;
this.inlineAlert.close();
break;
}
}
});
}
}
testConnection() {
this.pingStatus = true;
this.translateService.get('REPLICATION.TESTING_CONNECTION').subscribe(res=>this.pingTestMessage=res);

View File

@ -8,10 +8,17 @@
<clr-dg-row *ngFor="let p of policies;let i = index;" [clrDgItem]="p" (click)="selectPolicy(p)" [style.backgroundColor]="(!projectless && selectedId === p.id) ? '#eee' : ''">
<clr-dg-action-overflow>
<button class="action-item" (click)="editPolicy(p)">{{'REPLICATION.EDIT_POLICY' | translate}}</button>
<button class="action-item" (click)="togglePolicy(p)">{{ (p.enabled === 0 ? 'REPLICATION.ENABLE' : 'REPLICATION.DISABLE') | translate}}</button>
<button class="action-item" (click)="togglePolicy(p)">{{ (p.enabled === 0 ? 'REPLICATION.TOGGLE_ENABLE_TITLE' : 'REPLICATION.TOGGLE_DISABLE_TITLE') | translate}}</button>
<button class="action-item" (click)="deletePolicy(p)">{{'REPLICATION.DELETE_POLICY' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell>{{p.name}}</clr-dg-cell>
<clr-dg-cell>
<template [ngIf]="projectless">
<a href="javascript:void(0)" [routerLink]="['/harbor', 'projects', p.project_id, 'replication']">{{p.name}}</a>
</template>
<template [ngIf]="!projectless">
{{p.name}}
</template>
</clr-dg-cell>
<clr-dg-cell *ngIf="projectless">{{p.project_name}}</clr-dg-cell>
<clr-dg-cell>{{p.description}}</clr-dg-cell>
<clr-dg-cell>{{p.target_name}}</clr-dg-cell>

View File

@ -28,31 +28,53 @@ export class ListPolicyComponent implements OnDestroy {
@Output() editOne = new EventEmitter<number>();
@Output() toggleOne = new EventEmitter<Policy>();
toggleSubscription: Subscription;
subscription: Subscription;
constructor(
private replicationService: ReplicationService,
private toggleConfirmDialogService: ConfirmationDialogService,
private deletionDialogService: ConfirmationDialogService,
private messageService: MessageService) {
this.subscription = this.subscription = this.deletionDialogService
this.toggleSubscription = this.toggleConfirmDialogService
.confirmationConfirm$
.subscribe(
message=> {
if(message &&
message.source === ConfirmationTargets.TOGGLE_CONFIRM &&
message.state === ConfirmationState.CONFIRMED) {
let policy: Policy = message.data;
policy.enabled = policy.enabled === 0 ? 1 : 0;
console.log('Enable policy ID:' + policy.id + ' with activation status ' + policy.enabled);
this.replicationService
.enablePolicy(policy.id, policy.enabled)
.subscribe(
res => console.log('Successful toggled policy status'),
error => this.messageService.announceMessage(error.status, "Failed to toggle policy status.", AlertType.DANGER)
);
}
}
);
this.subscription = this.deletionDialogService
.confirmationConfirm$
.subscribe(
message => {
if (message &&
message.source === ConfirmationTargets.POLICY &&
message.state === ConfirmationState.CONFIRMED) {
this.replicationService
.deletePolicy(message.data)
.subscribe(
response => {
console.log('Successful delete policy with ID:' + message.data);
this.reload.emit(true);
},
error => this.messageService.announceMessage(error.status, 'Failed to delete policy with ID:' + message.data, AlertType.DANGER)
);
message => {
if (message &&
message.source === ConfirmationTargets.POLICY &&
message.state === ConfirmationState.CONFIRMED) {
this.replicationService
.deletePolicy(message.data)
.subscribe(
response => {
console.log('Successful delete policy with ID:' + message.data);
this.reload.emit(true);
},
error => this.messageService.announceMessage(error.status, 'Failed to delete policy with ID:' + message.data, AlertType.DANGER)
);
}
});
}
);
}
@ -60,6 +82,9 @@ export class ListPolicyComponent implements OnDestroy {
if (this.subscription) {
this.subscription.unsubscribe();
}
if(this.toggleSubscription) {
this.toggleSubscription.unsubscribe();
}
}
selectPolicy(policy: Policy): void {
@ -74,13 +99,14 @@ export class ListPolicyComponent implements OnDestroy {
}
togglePolicy(policy: Policy) {
policy.enabled = policy.enabled === 0 ? 1 : 0;
console.log('Enable policy ID:' + policy.id + ' with activation status ' + policy.enabled);
this.replicationService.enablePolicy(policy.id, policy.enabled)
.subscribe(
res => console.log('Successful toggled policy status'),
error => this.messageService.announceMessage(error.status, "Failed to toggle policy status.", AlertType.DANGER)
);
let toggleConfirmMessage: ConfirmationMessage = new ConfirmationMessage(
policy.enabled === 1 ? 'REPLICATION.TOGGLE_DISABLE_TITLE' : 'REPLICATION.TOGGLE_ENABLE_TITLE',
policy.enabled === 1 ? 'REPLICATION.CONFIRM_TOGGLE_DISABLE_POLICY': 'REPLICATION.CONFIRM_TOGGLE_ENABLE_POLICY',
policy.name,
policy,
ConfirmationTargets.TOGGLE_CONFIRM
);
this.toggleConfirmDialogService.openComfirmDialog(toggleConfirmMessage);
}
deletePolicy(policy: Policy) {

View File

@ -2,4 +2,9 @@
margin: 0px !important;
padding: 0px !important;
margin-top: -5px !important;
}
.spinner-pos {
margin-right: 0px !important;
top: 2px;
}

View File

@ -4,82 +4,66 @@
<div class="form-group form-group-override">
<label for="username" class="required form-group-label-override">{{'PROFILE.USER_NAME' | translate}}</label>
<label for="username" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("username")'>
<input type="text" placeholder='{{"PLACEHOLDER.USER_NAME" | translate}}' required pattern='[^"~#$%]+' maxLengthExt="20" #usernameInput="ngModel" name="username" [(ngModel)]="newUser.username" id="username" size="28"
<input type="text" required pattern='[^"~#$%]+' maxLengthExt="20" #usernameInput="ngModel" name="username" [(ngModel)]="newUser.username" id="username" size="40"
(input)='handleValidation("username", false)'
(focusout)='handleValidation("username", true)'>
<span class="tooltip-content">
{{usernameTooltip | translate}}
</span>
</label><span class="spinner spinner-inline" [hidden]='isChecking("username")'></span>
</label><span class="spinner spinner-inline spinner-pos" [hidden]='isChecking("username")'></span>
</div>
<div class="form-group form-group-override">
<label for="email" class="required form-group-label-override">{{'PROFILE.EMAIL' | translate}}</label>
<label for="email" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("email")'>
<input name="email" type="text" #eamilInput="ngModel" [(ngModel)]="newUser.email"
placeholder='{{"PLACEHOLDER.MAIL" | translate}}'
required
pattern='^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$' id="email" size="28"
pattern='^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$' id="email" size="40"
(input)='handleValidation("email", false)'
(focusout)='handleValidation("email", true)'>
<span class="tooltip-content">
{{emailTooltip | translate}}
</span>
</label>
<label *ngIf="isSelfRegistration" role="tooltip" aria-haspopup="true" class="tooltip tooltip-bottom-left">
<clr-icon shape="info" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">
{{'TOOLTIP.SIGN_UP_MAIL' | translate}}
</span>
</label><span class="spinner spinner-inline" [hidden]='isChecking("email")'></span>
<span class="spinner spinner-inline spinner-pos" [hidden]='isChecking("email")'></span>
<label class="sub-label-for-input" *ngIf="isSelfRegistration">{{'TOOLTIP.SIGN_UP_MAIL' | translate}}</label>
</div>
<div class="form-group form-group-override">
<label for="realname" class="required form-group-label-override">{{'PROFILE.FULL_NAME' | translate}}</label>
<label for="realname" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("realname")'>
<input type="text" placeholder='{{"PLACEHOLDER.FULL_NAME" | translate}}' name="realname" #fullNameInput="ngModel" [(ngModel)]="newUser.realname" required maxLengthExt="20" id="realname" size="28"
<input type="text" name="realname" #fullNameInput="ngModel" [(ngModel)]="newUser.realname" required maxLengthExt="20" id="realname" size="40"
(input)='handleValidation("realname", false)'
(focusout)='handleValidation("realname", true)'>
<span class="tooltip-content">
{{'TOOLTIP.FULL_NAME' | translate}}
</span>
</label>
<label *ngIf="isSelfRegistration" role="tooltip" aria-haspopup="true" class="tooltip tooltip-bottom-left">
<clr-icon shape="info" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">
{{'TOOLTIP.SIGN_UP_REAL_NAME' | translate}}
</span>
</label>
</div>
<div class="form-group form-group-override">
<label for="newPassword" class="required form-group-label-override">{{'PROFILE.PASSWORD' | translate}}</label>
<label for="newPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("newPassword")'>
<input type="password" id="newPassword" placeholder='{{"PLACEHOLDER.NEW_PWD" | translate}}'
required
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{7,}$"
<input type="password" id="newPassword"
required
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$"
name="newPassword"
[(ngModel)]="newUser.password"
#newPassInput="ngModel" size="28"
#newPassInput="ngModel" size="40"
(input)='handleValidation("newPassword", false)'
(focusout)='handleValidation("newPassword", true)'>
<span class="tooltip-content">
{{'TOOLTIP.PASSWORD' | translate}}
</span>
</label>
<label *ngIf="isSelfRegistration" role="tooltip" aria-haspopup="true" class="tooltip tooltip-bottom-left">
<clr-icon shape="info" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">
{{'TOOLTIP.PASSWORD' | translate}}
</span>
</label>
<label class="sub-label-for-input" *ngIf="isSelfRegistration">{{'CHANGE_PWD.PASS_TIPS' | translate}}</label>
</div>
<div class="form-group form-group-override">
<label for="confirmPassword" class="required form-group-label-override">{{'CHANGE_PWD.CONFIRM_PWD' | translate}}</label>
<label for="confirmPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("confirmPassword")'>
<input type="password" id="confirmPassword" placeholder='{{"PLACEHOLDER.CONFIRM_PWD" | translate}}'
<input type="password" id="confirmPassword"
required
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{7,}$"
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$"
name="confirmPassword"
[(ngModel)]="confirmedPwd"
#confirmPassInput="ngModel" size="28"
#confirmPassInput="ngModel" size="40"
(input)='handleValidation("confirmPassword", false)'
(focusout)='handleValidation("confirmPassword", true)'>
<span class="tooltip-content">
@ -90,7 +74,7 @@
<div class="form-group form-group-override">
<label for="comment" class="form-group-label-override">{{'PROFILE.COMMENT' | translate}}</label>
<label for="comment" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("comment")'>
<input type="text" #commentInput="ngModel" name="comment" [(ngModel)]="newUser.comment" maxLengthExt="20" id="comment" size="28"
<input type="text" #commentInput="ngModel" name="comment" [(ngModel)]="newUser.comment" maxLengthExt="20" id="comment" size="40"
(input)='handleValidation("comment", false)'
(focusout)='handleValidation("comment", true)'>
<span class="tooltip-content">
@ -100,5 +84,4 @@
</div>
</section>
</form>
</div>
<div style="height: 15px;"></div>
</div>

View File

@ -6,13 +6,11 @@ import {
CanActivateChild,
NavigationExtras
} from '@angular/router';
import { SessionService } from '../../shared/session.service';
import { CommonRoutes, AdmiralQueryParamKey } from '../../shared/shared.const';
import { AppConfigService } from '../../app-config.service';
import { maintainUrlQueryParmas } from '../../shared/shared.utils';
@Injectable()
export class AuthCheckGuard implements CanActivate, CanActivateChild {
constructor(
@ -50,7 +48,6 @@ export class AuthCheckGuard implements CanActivate, CanActivateChild {
this.router.navigateByUrl(keyRemovedUrl);
return resolve(false);
}
}

View File

@ -19,6 +19,7 @@ export const enum ConfirmationTargets {
PROJECT_MEMBER,
USER,
POLICY,
TOGGLE_CONFIRM,
TARGET,
REPOSITORY,
TAG,

View File

@ -0,0 +1,66 @@
import { Directive, OnChanges, Input, SimpleChanges } from '@angular/core';
import { NG_ASYNC_VALIDATORS, Validator, Validators, ValidatorFn, AbstractControl } from '@angular/forms';
import { ProjectService} from '../project/project.service';
import { MemberService } from '../project/member/member.service';
import { Member } from '../project/member/member';
@Directive({
selector: '[targetExists]',
providers: [
ProjectService, MemberService,
{ provide: NG_ASYNC_VALIDATORS, useExisting: TargetExistsValidatorDirective, multi: true},
]
})
export class TargetExistsValidatorDirective implements Validator, OnChanges {
@Input() targetExists: string;
@Input() projectId: number;
private valFn = Validators.nullValidator;
constructor(
private projectService: ProjectService,
private memberService: MemberService) {}
ngOnChanges(changes: SimpleChanges): void {
const change = changes['targetExists'];
if (change) {
const target: string = change.currentValue;
this.valFn = this.targetExistsValidator(target);
} else {
this.valFn = Validators.nullValidator;
}
}
validate(control: AbstractControl): {[key: string]: any} {
return this.valFn(control);
}
targetExistsValidator(target: string): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} => {
console.log('Target:' + target + ', validate value:' + control.value);
switch(target) {
case 'PROJECT_NAME':
return new Promise(resolve=>{
this.projectService
.checkProjectExists(control.value)
.subscribe(res=>resolve({'targetExists': true}),error=>resolve(null));
});
case 'MEMBER_NAME':
return new Promise(resolve=>{
this.memberService
.listMembers(this.projectId, control.value)
.subscribe((members: Member[])=>{
return members.filter(m=>{
if(m.username === control.value) {
return true;
}
return null;
}).length > 0 ?
resolve({'targetExists': true}) : resolve(null);
},error=>resolve(null));
});
}
}
}
}

View File

@ -56,7 +56,7 @@
"TITLE": "User Profile",
"USER_NAME": "Username",
"EMAIL": "Email",
"FULL_NAME": "Full name",
"FULL_NAME": "First and last name",
"COMMENT": "Comments",
"PASSWORD": "Password",
"SAVE_SUCCESS": "User profile saved successfully"
@ -66,7 +66,8 @@
"CURRENT_PWD": "Current Password",
"NEW_PWD": "New Password",
"CONFIRM_PWD": "Confirm Password",
"SAVE_SUCCESS": "User password changed successfully"
"SAVE_SUCCESS": "User password changed successfully",
"PASS_TIPS": "At least 8 chars with 1 uppercase, 1 lowercase and 1 number"
},
"ACCOUNT_SETTINGS": {
"PROFILE": "User Profile",
@ -152,6 +153,7 @@
"DELETE": "Delete",
"ITEMS": "item(s)",
"ACTIONS": "Actions",
"USERNAME_IS_REQUIRED": "Username is required",
"USERNAME_DOES_NOT_EXISTS": "Username does not exist.",
"USERNAME_ALREADY_EXISTS": "Username already exists.",
"UNKNOWN_ERROR": "Unknown error occurred while adding member.",
@ -229,7 +231,11 @@
"CREATION_TIME": "Start Time",
"END_TIME": "End Time",
"LOGS": "Logs",
"ITEMS": "item(s)"
"ITEMS": "item(s)",
"TOGGLE_ENABLE_TITLE": "Enable Policy",
"CONFIRM_TOGGLE_ENABLE_POLICY": "After enabling the replication policy, all repositories under the project will be replicated to the destination registry. Please confirm to continue.",
"TOGGLE_DISABLE_TITLE": "Disable Policy",
"CONFIRM_TOGGLE_DISABLE_POLICY": "After disabling the policy, all unfinished replication jobs of this policy will be stopped and canceled. Please confirm to continue."
},
"DESTINATION": {
"NEW_ENDPOINT": "New Endpoint",
@ -268,7 +274,7 @@
"DELETION_TITLE_TAG": "Confirm Tag Deletion",
"DELETION_SUMMARY_TAG": "Do you want to delete tag {{param}}?",
"DELETION_TITLE_TAG_DENIED": "Signed Tag can't be deleted",
"DELETION_SUMMARY_TAG_DENIED": "The tag must be removed from the Notary before it can be deleted.",
"DELETION_SUMMARY_TAG_DENIED": "The tag must be removed from the Notary before it can be deleted. {{param}}",
"FILTER_FOR_REPOSITORIES": "Filter for repositories",
"TAG": "Tag",
"SIGNED": "Signed",

View File

@ -66,7 +66,8 @@
"CURRENT_PWD": "当前密码",
"NEW_PWD": "新密码",
"CONFIRM_PWD": "确认密码",
"SAVE_SUCCESS": "更改用户密码成功"
"SAVE_SUCCESS": "更改用户密码成功",
"PASS_TIPS": "至少8个字符且需包含至少一个大写字符、小写字符或者数字"
},
"ACCOUNT_SETTINGS": {
"PROFILE": "用户设置",
@ -152,8 +153,9 @@
"DELETE": "删除",
"ITEMS": "条记录",
"ACTIONS": "操作",
"USERNAME_DOES_NOT_EXISTS": "用户名不存在",
"USERNAME_ALREADY_EXISTS": "用户名已存在",
"USERNAME_IS_REQUIRED": "用户名为必填项。",
"USERNAME_DOES_NOT_EXISTS": "用户名不存在。",
"USERNAME_ALREADY_EXISTS": "用户名已存在。",
"UNKNOWN_ERROR": "添加成员时发生未知错误。",
"FILTER_PLACEHOLDER": "过滤成员",
"DELETION_TITLE": "删除项目成员确认",
@ -229,7 +231,11 @@
"CREATION_TIME": "创建时间",
"END_TIME": "结束时间",
"LOGS": "日志",
"ITEMS": "条记录"
"ITEMS": "条记录",
"TOGGLE_ENABLE_TITLE": "启用策略",
"CONFIRM_TOGGLE_ENABLE_POLICY": "启用策略后,该项目下的所有镜像仓库将复制到目标实例。请确认继续。",
"TOGGLE_DISABLE_TITLE": "停用策略",
"CONFIRM_TOGGLE_DISABLE_POLICY": "停用策略后,所有未完成的复制任务将被终止和取消。请确认继续。"
},
"DESTINATION": {
"NEW_ENDPOINT": "新建目标",
@ -268,7 +274,7 @@
"DELETION_TITLE_TAG": "删除镜像标签确认",
"DELETION_SUMMARY_TAG": "确认删除镜像标签 {{param}}?",
"DELETION_TITLE_TAG_DENIED": "已签名的镜像不能被删除",
"DELETION_SUMMARY_TAG_DENIED": "要删除此镜像标签必须首先从Notary中删除。",
"DELETION_SUMMARY_TAG_DENIED": "要删除此镜像标签必须首先从Notary中删除。{{param}}",
"FILTER_FOR_REPOSITORIES": "过滤镜像仓库",
"TAG": "标签",
"SIGNED": "已签名",

View File

@ -1,4 +1,9 @@
/* You can add global styles to this file, and also import other style files */
.datagrid-content-wrapper {
overflow: hidden;
}
.form-group-label-override {
font-size: 14px;
font-weight: 400;
}

View File

@ -31,5 +31,6 @@ services:
volumes:
- /data/config/:/etc/adminserver/
- /data/secretkey:/etc/adminserver/key
- /data/:/data/
ports:
- 8888:80

19
tests/notarytest.sh Executable file
View File

@ -0,0 +1,19 @@
#!/bin/sh
set -e
TIMEOUT=10
while [ $TIMEOUT -gt 0 ]; do
STATUS=$(curl -s -o /dev/null -w '%{http_code}' https://127.0.0.1/notary/v2/ -kv)
if [ $STATUS -eq 401 ]; then
echo "Notary is running success."
break
fi
TIMEOUT=$(($TIMEOUT - 1))
sleep 5
done
if [ $TIMEOUT -eq 0 ]; then
echo "Notary is running fail."
exit 1
fi