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 *.pyc
jobservice/test 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/coverage/
src/ui_ng/dist/ src/ui_ng/dist/

View File

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

888
Makefile
View File

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

1
VERSION Normal file
View File

@ -0,0 +1 @@
dev

View File

@ -2028,6 +2028,9 @@ definitions:
has_ca_root: has_ca_root:
type: boolean type: boolean
description: Indicate whether there is a ca root cert file ready for download in the file system. 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: SystemInfo:
type: object type: object
properties: properties:

View File

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

View File

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

View File

@ -7,6 +7,7 @@ COPY ./make/dev/ui/harbor_ui /harbor/
COPY ./src/ui/views /harbor/views COPY ./src/ui/views /harbor/views
COPY ./src/ui/static /harbor/static COPY ./src/ui/static /harbor/static
COPY ./src/favicon.ico /harbor/favicon.ico COPY ./src/favicon.ico /harbor/favicon.ico
COPY ./VERSION /harbor/VERSION
RUN chmod u+x /harbor/harbor_ui RUN chmod u+x /harbor/harbor_ui

View File

@ -26,7 +26,7 @@ import (
"testing" "testing"
"github.com/vmware/harbor/src/adminserver/systemcfg" "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" "github.com/vmware/harbor/src/common/utils/test"
) )
@ -43,7 +43,7 @@ func TestConfigAPI(t *testing.T) {
secret := "secret" secret := "secret"
envs := map[string]string{ envs := map[string]string{
"AUTH_MODE": comcfg.DBAuth, "AUTH_MODE": common.DBAuth,
"JSON_CFG_STORE_PATH": configPath, "JSON_CFG_STORE_PATH": configPath,
"KEY_PATH": secretKeyPath, "KEY_PATH": secretKeyPath,
"UI_SECRET": secret, "UI_SECRET": secret,
@ -97,7 +97,7 @@ func TestConfigAPI(t *testing.T) {
return return
} }
scope := int(m[comcfg.LDAPScope].(float64)) scope := int(m[common.LDAPScope].(float64))
if scope != 3 { if scope != 3 {
t.Errorf("unexpected ldap scope: %d != %d", scope, 3) t.Errorf("unexpected ldap scope: %d != %d", scope, 3)
return return
@ -105,7 +105,7 @@ func TestConfigAPI(t *testing.T) {
// modify configurations // modify configurations
c := map[string]interface{}{ c := map[string]interface{}{
comcfg.AUTHMode: comcfg.LDAPAuth, common.AUTHMode: common.LDAPAuth,
} }
b, err := json.Marshal(c) b, err := json.Marshal(c)
@ -155,9 +155,9 @@ func TestConfigAPI(t *testing.T) {
return return
} }
mode := m[comcfg.AUTHMode].(string) mode := m[common.AUTHMode].(string)
if mode != comcfg.LDAPAuth { if mode != common.LDAPAuth {
t.Errorf("unexpected auth mode: %s != %s", mode, comcfg.LDAPAuth) t.Errorf("unexpected auth mode: %s != %s", mode, common.LDAPAuth)
return return
} }
@ -203,9 +203,9 @@ func TestConfigAPI(t *testing.T) {
return return
} }
mode = m[comcfg.AUTHMode].(string) mode = m[common.AUTHMode].(string)
if mode != comcfg.DBAuth { if mode != common.DBAuth {
t.Errorf("unexpected auth mode: %s != %s", mode, comcfg.LDAPAuth) t.Errorf("unexpected auth mode: %s != %s", mode, common.LDAPAuth)
return 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"
"github.com/vmware/harbor/src/adminserver/systemcfg/store/json" "github.com/vmware/harbor/src/adminserver/systemcfg/store/json"
"github.com/vmware/harbor/src/common"
comcfg "github.com/vmware/harbor/src/common/config" comcfg "github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/utils" "github.com/vmware/harbor/src/common/utils"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
@ -40,82 +41,82 @@ var (
// attrs need to be encrypted or decrypted // attrs need to be encrypted or decrypted
attrs = []string{ attrs = []string{
comcfg.EmailPassword, common.EmailPassword,
comcfg.LDAPSearchPwd, common.LDAPSearchPwd,
comcfg.MySQLPassword, common.MySQLPassword,
comcfg.AdminInitialPassword, common.AdminInitialPassword,
} }
// all configurations need read from environment variables // all configurations need read from environment variables
allEnvs = map[string]interface{}{ allEnvs = map[string]interface{}{
comcfg.ExtEndpoint: "EXT_ENDPOINT", common.ExtEndpoint: "EXT_ENDPOINT",
comcfg.AUTHMode: "AUTH_MODE", common.AUTHMode: "AUTH_MODE",
comcfg.SelfRegistration: &parser{ common.SelfRegistration: &parser{
env: "SELF_REGISTRATION", env: "SELF_REGISTRATION",
parse: parseStringToBool, parse: parseStringToBool,
}, },
comcfg.DatabaseType: "DATABASE_TYPE", common.DatabaseType: "DATABASE_TYPE",
comcfg.MySQLHost: "MYSQL_HOST", common.MySQLHost: "MYSQL_HOST",
comcfg.MySQLPort: &parser{ common.MySQLPort: &parser{
env: "MYSQL_PORT", env: "MYSQL_PORT",
parse: parseStringToInt, parse: parseStringToInt,
}, },
comcfg.MySQLUsername: "MYSQL_USR", common.MySQLUsername: "MYSQL_USR",
comcfg.MySQLPassword: "MYSQL_PWD", common.MySQLPassword: "MYSQL_PWD",
comcfg.MySQLDatabase: "MYSQL_DATABASE", common.MySQLDatabase: "MYSQL_DATABASE",
comcfg.SQLiteFile: "SQLITE_FILE", common.SQLiteFile: "SQLITE_FILE",
comcfg.LDAPURL: "LDAP_URL", common.LDAPURL: "LDAP_URL",
comcfg.LDAPSearchDN: "LDAP_SEARCH_DN", common.LDAPSearchDN: "LDAP_SEARCH_DN",
comcfg.LDAPSearchPwd: "LDAP_SEARCH_PWD", common.LDAPSearchPwd: "LDAP_SEARCH_PWD",
comcfg.LDAPBaseDN: "LDAP_BASE_DN", common.LDAPBaseDN: "LDAP_BASE_DN",
comcfg.LDAPFilter: "LDAP_FILTER", common.LDAPFilter: "LDAP_FILTER",
comcfg.LDAPUID: "LDAP_UID", common.LDAPUID: "LDAP_UID",
comcfg.LDAPScope: &parser{ common.LDAPScope: &parser{
env: "LDAP_SCOPE", env: "LDAP_SCOPE",
parse: parseStringToInt, parse: parseStringToInt,
}, },
comcfg.LDAPTimeout: &parser{ common.LDAPTimeout: &parser{
env: "LDAP_TIMEOUT", env: "LDAP_TIMEOUT",
parse: parseStringToInt, parse: parseStringToInt,
}, },
comcfg.EmailHost: "EMAIL_HOST", common.EmailHost: "EMAIL_HOST",
comcfg.EmailPort: &parser{ common.EmailPort: &parser{
env: "EMAIL_PORT", env: "EMAIL_PORT",
parse: parseStringToInt, parse: parseStringToInt,
}, },
comcfg.EmailUsername: "EMAIL_USR", common.EmailUsername: "EMAIL_USR",
comcfg.EmailPassword: "EMAIL_PWD", common.EmailPassword: "EMAIL_PWD",
comcfg.EmailSSL: &parser{ common.EmailSSL: &parser{
env: "EMAIL_SSL", env: "EMAIL_SSL",
parse: parseStringToBool, parse: parseStringToBool,
}, },
comcfg.EmailFrom: "EMAIL_FROM", common.EmailFrom: "EMAIL_FROM",
comcfg.EmailIdentity: "EMAIL_IDENTITY", common.EmailIdentity: "EMAIL_IDENTITY",
comcfg.RegistryURL: "REGISTRY_URL", common.RegistryURL: "REGISTRY_URL",
comcfg.TokenExpiration: &parser{ common.TokenExpiration: &parser{
env: "TOKEN_EXPIRATION", env: "TOKEN_EXPIRATION",
parse: parseStringToInt, parse: parseStringToInt,
}, },
comcfg.UseCompressedJS: &parser{ common.UseCompressedJS: &parser{
env: "USE_COMPRESSED_JS", env: "USE_COMPRESSED_JS",
parse: parseStringToBool, parse: parseStringToBool,
}, },
comcfg.CfgExpiration: &parser{ common.CfgExpiration: &parser{
env: "CFG_EXPIRATION", env: "CFG_EXPIRATION",
parse: parseStringToInt, parse: parseStringToInt,
}, },
comcfg.MaxJobWorkers: &parser{ common.MaxJobWorkers: &parser{
env: "MAX_JOB_WORKERS", env: "MAX_JOB_WORKERS",
parse: parseStringToInt, parse: parseStringToInt,
}, },
comcfg.VerifyRemoteCert: &parser{ common.VerifyRemoteCert: &parser{
env: "VERIFY_REMOTE_CERT", env: "VERIFY_REMOTE_CERT",
parse: parseStringToBool, parse: parseStringToBool,
}, },
comcfg.ProjectCreationRestriction: "PROJECT_CREATION_RESTRICTION", common.ProjectCreationRestriction: "PROJECT_CREATION_RESTRICTION",
comcfg.AdminInitialPassword: "HARBOR_ADMIN_PASSWORD", common.AdminInitialPassword: "HARBOR_ADMIN_PASSWORD",
comcfg.AdmiralEndpoint: "ADMIRAL_URL", common.AdmiralEndpoint: "ADMIRAL_URL",
comcfg.WithNotary: &parser{ common.WithNotary: &parser{
env: "WITH_NOTARY", env: "WITH_NOTARY",
parse: parseStringToBool, parse: parseStringToBool,
}, },
@ -124,23 +125,23 @@ var (
// configurations need read from environment variables // configurations need read from environment variables
// every time the system startup // every time the system startup
repeatLoadEnvs = map[string]interface{}{ repeatLoadEnvs = map[string]interface{}{
comcfg.ExtEndpoint: "EXT_ENDPOINT", common.ExtEndpoint: "EXT_ENDPOINT",
comcfg.MySQLPassword: "MYSQL_PWD", common.MySQLPassword: "MYSQL_PWD",
comcfg.MaxJobWorkers: &parser{ common.MaxJobWorkers: &parser{
env: "MAX_JOB_WORKERS", env: "MAX_JOB_WORKERS",
parse: parseStringToInt, parse: parseStringToInt,
}, },
// TODO remove this config? // TODO remove this config?
comcfg.UseCompressedJS: &parser{ common.UseCompressedJS: &parser{
env: "USE_COMPRESSED_JS", env: "USE_COMPRESSED_JS",
parse: parseStringToBool, parse: parseStringToBool,
}, },
comcfg.CfgExpiration: &parser{ common.CfgExpiration: &parser{
env: "CFG_EXPIRATION", env: "CFG_EXPIRATION",
parse: parseStringToInt, parse: parseStringToInt,
}, },
comcfg.AdmiralEndpoint: "ADMIRAL_URL", common.AdmiralEndpoint: "ADMIRAL_URL",
comcfg.WithNotary: &parser{ common.WithNotary: &parser{
env: "WITH_NOTARY", env: "WITH_NOTARY",
parse: parseStringToBool, parse: parseStringToBool,
}, },

View File

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

View File

@ -17,83 +17,26 @@
package config package config
import ( import (
"bytes"
"encoding/json"
"fmt" "fmt"
"io/ioutil"
"net/http"
"strings"
"time" "time"
"github.com/astaxie/beego/cache" "github.com/astaxie/beego/cache"
"github.com/vmware/harbor/src/common/utils" "github.com/vmware/harbor/src/adminserver/client"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/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"
) )
// Manager manages configurations // Manager manages configurations
type Manager struct { type Manager struct {
Loader *Loader client client.Client
Parser *Parser
Cache bool Cache bool
cache cache.Cache cache cache.Cache
key string key string
} }
// NewManager returns an instance of Manager // NewManager returns an instance of Manager
// url: the url from which loader loads configurations func NewManager(client client.Client, enableCache bool) *Manager {
func NewManager(url, secret string, enableCache bool) *Manager {
m := &Manager{ m := &Manager{
Loader: NewLoader(url, secret), client: client,
Parser: &Parser{},
} }
if enableCache { if enableCache {
@ -105,19 +48,9 @@ func NewManager(url, secret string, enableCache bool) *Manager {
return m return m
} }
// Init loader
func (m *Manager) Init() error {
return m.Loader.Init()
}
// Load configurations, if cache is enabled, cache the configurations // Load configurations, if cache is enabled, cache the configurations
func (m *Manager) Load() (map[string]interface{}, error) { func (m *Manager) Load() (map[string]interface{}, error) {
b, err := m.Loader.Load() c, err := m.client.GetCfgs()
if err != nil {
return nil, err
}
c, err := m.Parser.Parse(b)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -127,7 +60,15 @@ func (m *Manager) Load() (map[string]interface{}, error) {
if err != nil { if err != nil {
return nil, err 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 { time.Duration(expi)*time.Second); err != nil {
return nil, err return nil, err
} }
@ -138,7 +79,7 @@ func (m *Manager) Load() (map[string]interface{}, error) {
// Reset configurations // Reset configurations
func (m *Manager) Reset() error { func (m *Manager) Reset() error {
return m.Loader.Reset() return m.client.ResetCfgs()
} }
func getCfgExpiration(m map[string]interface{}) (int, error) { 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") return 0, fmt.Errorf("can not get cfg expiration as configurations are null")
} }
expi, ok := m[CfgExpiration] expi, ok := m[common.CfgExpiration]
if !ok { if !ok {
return 0, fmt.Errorf("cfg expiration is not set") return 0, fmt.Errorf("cfg expiration is not set")
} }
@ -167,133 +108,6 @@ func (m *Manager) Get() (map[string]interface{}, error) {
} }
// Upload configurations // Upload configurations
func (m *Manager) Upload(b []byte) error { func (m *Manager) Upload(cfgs map[string]interface{}) error {
return m.Loader.Upload(b) return m.client.UpdateCfgs(cfgs)
}
// 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
} }

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

View File

@ -20,46 +20,47 @@ import (
"net/http" "net/http"
"net/http/httptest" "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{}{ var adminServerDefaultConfig = map[string]interface{}{
config.ExtEndpoint: "https://host01.com", common.ExtEndpoint: "https://host01.com",
config.AUTHMode: config.DBAuth, common.AUTHMode: common.DBAuth,
config.DatabaseType: "mysql", common.DatabaseType: "mysql",
config.MySQLHost: "127.0.0.1", common.MySQLHost: "127.0.0.1",
config.MySQLPort: 3306, common.MySQLPort: 3306,
config.MySQLUsername: "user01", common.MySQLUsername: "user01",
config.MySQLPassword: "password", common.MySQLPassword: "password",
config.MySQLDatabase: "registry", common.MySQLDatabase: "registry",
config.SQLiteFile: "/tmp/registry.db", common.SQLiteFile: "/tmp/registry.db",
config.SelfRegistration: true, common.SelfRegistration: true,
config.LDAPURL: "ldap://127.0.0.1", common.LDAPURL: "ldap://127.0.0.1",
config.LDAPSearchDN: "uid=searchuser,ou=people,dc=mydomain,dc=com", common.LDAPSearchDN: "uid=searchuser,ou=people,dc=mydomain,dc=com",
config.LDAPSearchPwd: "password", common.LDAPSearchPwd: "password",
config.LDAPBaseDN: "ou=people,dc=mydomain,dc=com", common.LDAPBaseDN: "ou=people,dc=mydomain,dc=com",
config.LDAPUID: "uid", common.LDAPUID: "uid",
config.LDAPFilter: "", common.LDAPFilter: "",
config.LDAPScope: 3, common.LDAPScope: 3,
config.LDAPTimeout: 30, common.LDAPTimeout: 30,
config.TokenServiceURL: "http://token_service", common.TokenServiceURL: "http://token_service",
config.RegistryURL: "http://registry", common.RegistryURL: "http://registry",
config.EmailHost: "127.0.0.1", common.EmailHost: "127.0.0.1",
config.EmailPort: 25, common.EmailPort: 25,
config.EmailUsername: "user01", common.EmailUsername: "user01",
config.EmailPassword: "password", common.EmailPassword: "password",
config.EmailFrom: "from", common.EmailFrom: "from",
config.EmailSSL: true, common.EmailSSL: true,
config.EmailIdentity: "", common.EmailIdentity: "",
config.ProjectCreationRestriction: config.ProCrtRestrAdmOnly, common.ProjectCreationRestriction: common.ProCrtRestrAdmOnly,
config.VerifyRemoteCert: false, common.VerifyRemoteCert: false,
config.MaxJobWorkers: 3, common.MaxJobWorkers: 3,
config.TokenExpiration: 30, common.TokenExpiration: 30,
config.CfgExpiration: 5, common.CfgExpiration: 5,
config.UseCompressedJS: true, common.UseCompressedJS: true,
config.AdminInitialPassword: "password", common.AdminInitialPassword: "password",
config.AdmiralEndpoint: "http://www.vmware.com", common.AdmiralEndpoint: "http://www.vmware.com",
config.WithNotary: false, common.WithNotary: false,
} }
// NewAdminserver returns a mock admin server // 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 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 package config
import ( import (
"fmt"
"os" "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" comcfg "github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
) )
const ( const (
defaultKeyPath string = "/etc/jobservice/key" defaultKeyPath string = "/etc/jobservice/key"
defaultLogDir string = "/var/log/jobs" defaultLogDir string = "/var/log/jobs"
secretCookieName string = "secret"
) )
var ( var (
mg *comcfg.Manager // AdminserverClient is a client for adminserver
keyProvider comcfg.KeyProvider AdminserverClient client.Client
mg *comcfg.Manager
keyProvider comcfg.KeyProvider
) )
// Init configurations // Init configurations
@ -42,12 +49,15 @@ func Init() error {
if len(adminServerURL) == 0 { if len(adminServerURL) == 0 {
adminServerURL = "http://adminserver" adminServerURL = "http://adminserver"
} }
mg = comcfg.NewManager(adminServerURL, JobserviceSecret(), true) log.Infof("initializing client for adminserver %s ...", adminServerURL)
authorizer := auth.NewSecretAuthorizer(secretCookieName, UISecret())
if err := mg.Init(); err != nil { AdminserverClient = client.NewClient(adminServerURL, authorizer)
return err 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 { if _, err := mg.Load(); err != nil {
return err return err
} }
@ -71,7 +81,7 @@ func VerifyRemoteCert() (bool, error) {
if err != nil { if err != nil {
return true, err return true, err
} }
return cfg[comcfg.VerifyRemoteCert].(bool), nil return cfg[common.VerifyRemoteCert].(bool), nil
} }
// Database ... // Database ...
@ -81,16 +91,16 @@ func Database() (*models.Database, error) {
return nil, err return nil, err
} }
database := &models.Database{} database := &models.Database{}
database.Type = cfg[comcfg.DatabaseType].(string) database.Type = cfg[common.DatabaseType].(string)
mysql := &models.MySQL{} mysql := &models.MySQL{}
mysql.Host = cfg[comcfg.MySQLHost].(string) mysql.Host = cfg[common.MySQLHost].(string)
mysql.Port = int(cfg[comcfg.MySQLPort].(float64)) mysql.Port = int(cfg[common.MySQLPort].(float64))
mysql.Username = cfg[comcfg.MySQLUsername].(string) mysql.Username = cfg[common.MySQLUsername].(string)
mysql.Password = cfg[comcfg.MySQLPassword].(string) mysql.Password = cfg[common.MySQLPassword].(string)
mysql.Database = cfg[comcfg.MySQLDatabase].(string) mysql.Database = cfg[common.MySQLDatabase].(string)
database.MySQL = mysql database.MySQL = mysql
sqlite := &models.SQLite{} sqlite := &models.SQLite{}
sqlite.File = cfg[comcfg.SQLiteFile].(string) sqlite.File = cfg[common.SQLiteFile].(string)
database.SQLite = sqlite database.SQLite = sqlite
return database, nil return database, nil
@ -102,7 +112,7 @@ func MaxJobWorkers() (int, error) {
if err != nil { if err != nil {
return 0, err 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 // 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 { if err != nil {
return "", err 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 // LogDir returns the absolute path to which the log file will be written
@ -151,7 +161,7 @@ func ExtEndpoint() (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
return cfg[comcfg.ExtEndpoint].(string), nil return cfg[common.ExtEndpoint].(string), nil
} }
// InternalTokenServiceEndpoint ... // InternalTokenServiceEndpoint ...

View File

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

View File

@ -20,7 +20,8 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "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) { func TestGetConfig(t *testing.T) {
@ -46,8 +47,13 @@ func TestGetConfig(t *testing.T) {
return return
} }
mode := cfg[config.AUTHMode].Value.(string) mode := cfg[common.AUTHMode].Value.(string)
assert.Equal(config.DBAuth, mode, fmt.Sprintf("the auth mode should be %s", config.DBAuth)) 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) { func TestPutConfig(t *testing.T) {
@ -56,7 +62,7 @@ func TestPutConfig(t *testing.T) {
apiTest := newHarborAPI() apiTest := newHarborAPI()
cfg := map[string]string{ cfg := map[string]string{
config.VerifyRemoteCert: "0", common.VerifyRemoteCert: "0",
} }
code, err := apiTest.PutConfig(*admin, cfg) 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") { if !assert.Equal(200, code, "the status code of modifying configurations with admin user should be 200") {
return 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) { func TestResetConfig(t *testing.T) {
@ -94,11 +105,17 @@ func TestResetConfig(t *testing.T) {
return return
} }
value, ok := cfgs[config.VerifyRemoteCert] value, ok := cfgs[common.VerifyRemoteCert]
if !ok { if !ok {
t.Errorf("%s not found", config.VerifyRemoteCert) t.Errorf("%s not found", common.VerifyRemoteCert)
return return
} }
assert.Equal(value.Value.(bool), true, "unexpected value") 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 package api
import ( import (
"io/ioutil"
"net/http" "net/http"
"os" "os"
"path/filepath"
"strings" "strings"
"syscall"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/api" "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/dao"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/config" "github.com/vmware/harbor/src/ui/config"
@ -21,8 +20,8 @@ type SystemInfoAPI struct {
isAdmin bool isAdmin bool
} }
const harborStoragePath = "/harbor_storage"
const defaultRootCert = "/harbor_storage/ca_download/ca.crt" const defaultRootCert = "/harbor_storage/ca_download/ca.crt"
const harborVersionFile = "/harbor/VERSION"
//SystemInfo models for system info. //SystemInfo models for system info.
type SystemInfo struct { type SystemInfo struct {
@ -45,6 +44,7 @@ type GeneralInfo struct {
ProjectCreationRestrict string `json:"project_creation_restriction"` ProjectCreationRestrict string `json:"project_creation_restriction"`
SelfRegistration bool `json:"self_registration"` SelfRegistration bool `json:"self_registration"`
HasCARoot bool `json:"has_ca_root"` HasCARoot bool `json:"has_ca_root"`
HarborVersion string `json:"harbor_version"`
} }
// validate for validating user if an admin. // 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.") sia.RenderError(http.StatusForbidden, "User does not have admin role.")
return 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{ systemInfo := SystemInfo{
HarborStorage: Storage{ HarborStorage: Storage{
Total: stat.Blocks * uint64(stat.Bsize), Total: capacity.Total,
Free: stat.Bavail * uint64(stat.Bsize), Free: capacity.Free,
}, },
} }
@ -112,22 +110,34 @@ func (sia *SystemInfoAPI) GetGeneralInfo() {
sia.CustomAbort(http.StatusInternalServerError, "Unexpected error") sia.CustomAbort(http.StatusInternalServerError, "Unexpected error")
} }
var registryURL string 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] registryURL = l[1]
} else { } else {
registryURL = l[0] registryURL = l[0]
} }
_, caStatErr := os.Stat(defaultRootCert) _, caStatErr := os.Stat(defaultRootCert)
harborVersion := sia.getVersion()
info := GeneralInfo{ info := GeneralInfo{
AdmiralEndpoint: cfg[comcfg.AdmiralEndpoint].(string), AdmiralEndpoint: cfg[common.AdmiralEndpoint].(string),
WithAdmiral: config.WithAdmiral(), WithAdmiral: config.WithAdmiral(),
WithNotary: config.WithNotary(), WithNotary: config.WithNotary(),
AuthMode: cfg[comcfg.AUTHMode].(string), AuthMode: cfg[common.AUTHMode].(string),
ProjectCreationRestrict: cfg[comcfg.ProjectCreationRestriction].(string), ProjectCreationRestrict: cfg[common.ProjectCreationRestriction].(string),
SelfRegistration: cfg[comcfg.SelfRegistration].(bool), SelfRegistration: cfg[common.SelfRegistration].(bool),
RegistryURL: registryURL, RegistryURL: registryURL,
HasCARoot: caStatErr == nil, HasCARoot: caStatErr == nil,
HarborVersion: harborVersion,
} }
sia.Data["json"] = info sia.Data["json"] = info
sia.ServeJSON() 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 ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/stretchr/testify/assert"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
func TestGetVolumeInfo(t *testing.T) { func TestGetVolumeInfo(t *testing.T) {
@ -49,6 +50,7 @@ func TestGetGeneralInfo(t *testing.T) {
assert.Nil(err, fmt.Sprintf("Unexpected Error: %v", err)) assert.Nil(err, fmt.Sprintf("Unexpected Error: %v", err))
assert.Equal(false, g.WithNotary, "with notary should be false") assert.Equal(false, g.WithNotary, "with notary should be false")
assert.Equal(true, g.HasCARoot, "has ca root should be true") 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) { func TestGetCert(t *testing.T) {

View File

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

View File

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

View File

@ -62,7 +62,7 @@ func initRouters() {
//API: //API:
beego.Router("/api/search", &api.SearchAPI{}) beego.Router("/api/search", &api.SearchAPI{})
beego.Router("/api/projects/:pid([0-9]+)/members/?:mid", &api.ProjectMemberAPI{}) 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", &api.ProjectAPI{})
beego.Router("/api/projects/:id/publicity", &api.ProjectAPI{}, "put:ToggleProjectPublic") beego.Router("/api/projects/:id/publicity", &api.ProjectAPI{}, "put:ToggleProjectPublic")
beego.Router("/api/statistics", &api.StatisticAPI{}) beego.Router("/api/statistics", &api.StatisticAPI{})

View File

@ -7,11 +7,11 @@
<div class="form-group form-group-override"> <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" 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)"> <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 required
name="oldPassword" name="oldPassword"
[(ngModel)]="oldPwd" [(ngModel)]="oldPwd"
#oldPassInput="ngModel" size="30"> #oldPassInput="ngModel" size="42">
<span class="tooltip-content"> <span class="tooltip-content">
{{'TOOLTIP.CURRENT_PWD' | translate}} {{'TOOLTIP.CURRENT_PWD' | translate}}
</span> </span>
@ -19,27 +19,32 @@
</div> </div>
<div class="form-group form-group-override"> <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" 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)"> <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" placeholder='{{"PLACEHOLDER.NEW_PWD" | translate}}' <input type="password" id="newPassword"
required 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" name="newPassword"
[(ngModel)]="newPwd" [(ngModel)]="newPwd"
#newPassInput="ngModel" size="30"> #newPassInput="ngModel" size="42"
(input)='handleValidation("newPassword", false)'
(focusout)='handleValidation("newPassword", true)'>
<span class="tooltip-content"> <span class="tooltip-content">
{{'TOOLTIP.PASSWORD' | translate}} {{'TOOLTIP.PASSWORD' | translate}}
</span> </span>
</label> </label>
<label class="sub-label-for-input">{{'CHANGE_PWD.PASS_TIPS' | translate}}</label>
</div> </div>
<div class="form-group form-group-override"> <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" 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)"> <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" placeholder='{{"PLACEHOLDER.CONFIRM_PWD" | translate}}' <input type="password" id="reNewPassword"
required 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" name="reNewPassword"
[(ngModel)]="reNewPwd" [(ngModel)]="reNewPwd"
#reNewPassInput="ngModel" size="30"> #reNewPassInput="ngModel" size="42"
(input)='handleValidation("reNewPassword", false)'
(focusout)='handleValidation("reNewPassword", true)'>
<span class="tooltip-content"> <span class="tooltip-content">
{{'TOOLTIP.CONFIRM_PWD' | translate}} {{'TOOLTIP.CONFIRM_PWD' | translate}}
</span> </span>

View File

@ -23,6 +23,10 @@ export class PasswordSettingComponent implements AfterViewChecked {
private formValueChanged: boolean = false; private formValueChanged: boolean = false;
private onCalling: boolean = false; private onCalling: boolean = false;
private validationStateMap: any = {
"newPassword": true,
"reNewPassword": true
};
pwdFormRef: NgForm; pwdFormRef: NgForm;
@ViewChild("changepwdForm") pwdForm: NgForm; @ViewChild("changepwdForm") pwdForm: NgForm;
@ -52,6 +56,29 @@ export class PasswordSettingComponent implements AfterViewChecked {
return this.onCalling; 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() { ngAfterViewChecked() {
if (this.pwdFormRef != this.pwdForm) { if (this.pwdFormRef != this.pwdForm) {
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 { CookieKeyOfAdmiral, HarborQueryParamKey } from './shared/shared.const';
import { maintainUrlQueryParmas } from './shared/shared.utils'; import { maintainUrlQueryParmas } from './shared/shared.utils';
export const systemInfoEndpoint = "/api/systeminfo"; export const systemInfoEndpoint = "/api/systeminfo";
/** /**
* Declare service to handle the bootstrap options * Declare service to handle the bootstrap options
@ -50,7 +49,6 @@ export class AppConfigService {
//Catch the error //Catch the error
console.error("Failed to load bootstrap options with error: ", error); console.error("Failed to load bootstrap options with error: ", error);
}); });
} }
public getConfig(): AppConfig { public getConfig(): AppConfig {

View File

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

View File

@ -7,7 +7,7 @@
</div> </div>
<div class="header-nav"> <div class="header-nav">
<a href="{{admiralLink}}" class="nav-link" *ngIf="isIntegrationMode"><span class="nav-text">Management</span></a> <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> </div>
<global-search></global-search> <global-search></global-search>
<div class="header-actions"> <div class="header-actions">

View File

@ -9,9 +9,11 @@ import { SessionUser } from '../../shared/session-user';
import { SessionService } from '../../shared/session.service'; import { SessionService } from '../../shared/session.service';
import { CookieService } from 'angular2-cookie/core'; 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 { AppConfigService } from '../../app-config.service';
import { SearchTriggerService } from '../global-search/search-trigger.service';
import { MessageService } from '../../global-message/message.service';
@Component({ @Component({
selector: 'navigator', selector: 'navigator',
@ -31,7 +33,9 @@ export class NavigatorComponent implements OnInit {
private router: Router, private router: Router,
private translate: TranslateService, private translate: TranslateService,
private cookie: CookieService, private cookie: CookieService,
private appConfigService: AppConfigService) { } private appConfigService: AppConfigService,
private msgService: MessageService,
private searchTrigger: SearchTriggerService) { }
ngOnInit(): void { ngOnInit(): void {
this.selectedLang = this.translate.currentLang; this.selectedLang = this.translate.currentLang;
@ -98,8 +102,10 @@ export class NavigatorComponent implements OnInit {
this.router.navigate([CommonRoutes.EMBEDDED_SIGN_IN]); this.router.navigate([CommonRoutes.EMBEDDED_SIGN_IN]);
}) })
.catch(error => { .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 //Switch languages
@ -124,5 +130,12 @@ export class NavigatorComponent implements OnInit {
//Naviagte to signin page //Naviagte to signin page
this.router.navigate([CommonRoutes.HARBOR_ROOT]); 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 { .form-group-label-override {
font-size: 14px; font-size: 14px;
font-weight: 400; 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> <h3 class="modal-title">{{'PROJECT.NEW_PROJECT' | translate}}</h3>
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert> <inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
<div class="modal-body"> <div class="modal-body">
<form #projectForm="ngForm"> <form #projectForm="ngForm">
<section class="form-block"> <section class="form-block">
<div class="form-group"> <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"> <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)"> <span class="tooltip-content" *ngIf="projectName.errors && projectName.errors.required && (projectName.dirty || projectName.touched)">
{{'PROJECT.NAME_IS_REQUIRED' | translate}} {{'PROJECT.NAME_IS_REQUIRED' | translate}}
</span> </span>
<span class="tooltip-content" *ngIf="projectName.errors && projectName.errors.minlength && (projectName.dirty || projectName.touched)"> <span class="tooltip-content" *ngIf="projectName.errors && projectName.errors.minlength && (projectName.dirty || projectName.touched)">
{{'PROJECT.NAME_MINIMUM_LENGTH' | translate}} {{'PROJECT.NAME_MINIMUM_LENGTH' | translate}}
</span> </span>
<span class="tooltip-content" *ngIf="projectName.errors && projectName.errors.targetExists && (projectName.dirty || projectName.touched)">
{{'PROJECT.NAME_ALREADY_EXISTS' | translate}}
</span>
</label> </label>
</div> </div>
<div class="form-group"> <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"> <div class="checkbox-inline">
<input type="checkbox" id="create_project_public" [(ngModel)]="project.public" name="public"> <input type="checkbox" id="create_project_public" [(ngModel)]="project.public" name="public">
<label for="create_project_public"></label> <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 { Response } from '@angular/http';
import { NgForm } from '@angular/forms'; import { NgForm } from '@angular/forms';
@ -14,6 +13,7 @@ import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.com
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@Component({ @Component({
selector: 'create-project', selector: 'create-project',
templateUrl: 'create-project.component.html', templateUrl: 'create-project.component.html',
@ -27,12 +27,15 @@ export class CreateProjectComponent implements AfterViewChecked {
currentForm: NgForm; currentForm: NgForm;
project: Project = new Project(); project: Project = new Project();
initVal: Project = new Project();
createProjectOpened: boolean; createProjectOpened: boolean;
hasChanged: boolean; hasChanged: boolean;
staticBackdrop: boolean = true;
closable: boolean = false;
@Output() create = new EventEmitter<boolean>(); @Output() create = new EventEmitter<boolean>();
@ViewChild(InlineAlertComponent) @ViewChild(InlineAlertComponent)
private inlineAlert: InlineAlertComponent; private inlineAlert: InlineAlertComponent;
@ -75,7 +78,9 @@ export class CreateProjectComponent implements AfterViewChecked {
this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'}); this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'});
} else { } else {
this.createProjectOpened = false; this.createProjectOpened = false;
this.projectForm.reset();
} }
} }
ngAfterViewChecked(): void { ngAfterViewChecked(): void {
@ -83,17 +88,14 @@ export class CreateProjectComponent implements AfterViewChecked {
if(this.projectForm) { if(this.projectForm) {
this.projectForm.valueChanges.subscribe(data=>{ this.projectForm.valueChanges.subscribe(data=>{
for(let i in data) { for(let i in data) {
let item = data[i]; let origin = this.initVal[i];
if(typeof item === 'string' && (<string>item).trim().length !== 0) { let current = data[i];
this.hasChanged = true; if(current && current !== origin) {
break;
} else if (typeof item === 'boolean' && (<boolean>item)) {
this.hasChanged = true; this.hasChanged = true;
break; break;
} else { } else {
this.hasChanged = false; this.hasChanged = false;
this.inlineAlert.close(); this.inlineAlert.close();
break;
} }
} }
}); });
@ -109,7 +111,7 @@ export class CreateProjectComponent implements AfterViewChecked {
confirmCancel(event: boolean): void { confirmCancel(event: boolean): void {
this.createProjectOpened = false; this.createProjectOpened = false;
this.inlineAlert.close(); 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.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.PUBLIC_OR_PRIVATE' | 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> <clr-dg-column>{{'PROJECT.REPO_COUNT'| translate}}</clr-dg-column>
@ -19,5 +19,5 @@
<clr-dg-footer> <clr-dg-footer>
{{totalRecordCount || (projects ? projects.length : 0)}} {{'PROJECT.ITEMS' | translate}} {{totalRecordCount || (projects ? projects.length : 0)}} {{'PROJECT.ITEMS' | translate}}
<clr-dg-pagination [clrDgPageSize]="pageOffset" [clrDgTotalItems]="totalPage"></clr-dg-pagination> <clr-dg-pagination [clrDgPageSize]="pageOffset" [clrDgTotalItems]="totalPage"></clr-dg-pagination>
</clr-dg-footer> </clr-dg-footer>
</clr-datagrid> </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> <h3 class="modal-title">{{'MEMBER.NEW_MEMBER' | translate}}</h3>
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert> <inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
<div class="modal-body"> <div class="modal-body">
<form #memberForm="ngForm"> <form #memberForm="ngForm">
<section class="form-block"> <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"> <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"> <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)"> <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> </span>
</label> </label>
</div> </div>
<div class="form-group"> <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"> <div class="radio">
<input type="radio" name="roleRadios" id="checkrads_project_admin" value="1" [(ngModel)]="member.role_id"> <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> <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 { export class AddMemberComponent implements AfterViewChecked {
member: Member = new Member(); member: Member = new Member();
initVal: Member = new Member();
addMemberOpened: boolean; addMemberOpened: boolean;
memberForm: NgForm; memberForm: NgForm;
staticBackdrop: boolean = true;
closable: boolean = false;
@ViewChild('memberForm') @ViewChild('memberForm')
currentForm: NgForm; currentForm: NgForm;
@ -40,6 +45,7 @@ export class AddMemberComponent implements AfterViewChecked {
private translateService: TranslateService) {} private translateService: TranslateService) {}
onSubmit(): void { onSubmit(): void {
if(!this.member.username || this.member.username.length === 0) { return; }
console.log('Adding member:' + JSON.stringify(this.member)); console.log('Adding member:' + JSON.stringify(this.member));
this.memberService this.memberService
.addMember(this.projectId, this.member.username, +this.member.role_id) .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'}); this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'});
} else { } else {
this.addMemberOpened = false; this.addMemberOpened = false;
this.memberForm.reset();
} }
} }
@ -83,21 +90,15 @@ export class AddMemberComponent implements AfterViewChecked {
this.memberForm = this.currentForm; this.memberForm = this.currentForm;
if(this.memberForm) { if(this.memberForm) {
this.memberForm.valueChanges.subscribe(data=>{ this.memberForm.valueChanges.subscribe(data=>{
for(let i in data) { for(let i in data) {
let item = data[i]; let origin = this.initVal[i];
if(typeof item === 'string' && (<string>item).trim().length !== 0) { let current = data[i];
this.hasChanged = true; if(current && current !== origin) {
break;
} else if (typeof item === 'boolean' && (<boolean>item)) {
this.hasChanged = true;
break;
} else if (typeof item === 'number' && (<number>item) !== 0) {
this.hasChanged = true; this.hasChanged = true;
break; break;
} else { } else {
this.hasChanged = false; this.hasChanged = false;
this.inlineAlert.close(); this.inlineAlert.close();
break;
} }
} }
}); });
@ -107,6 +108,7 @@ export class AddMemberComponent implements AfterViewChecked {
confirmCancel(confirmed: boolean) { confirmCancel(confirmed: boolean) {
this.addMemberOpened = false; this.addMemberOpened = false;
this.inlineAlert.close(); this.inlineAlert.close();
this.memberForm.reset();
} }
openAddMemberModal(): void { openAddMemberModal(): void {

View File

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

View File

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

View File

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

View File

@ -62,4 +62,12 @@ export class ProjectService {
.map(response=>response.status) .map(response=>response.status)
.catch(error=>Observable.throw(error)); .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> <h3 class="modal-title">{{modalTitle}}</h3>
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert> <inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
<div class="modal-body"> <div class="modal-body">
<form #targetForm="ngForm"> <form #targetForm="ngForm">
<section class="form-block"> <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"> <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"> <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> <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)"> <span class="tooltip-content" *ngIf="targetName.errors && targetName.errors.required && (targetName.dirty || targetName.touched)">
@ -21,7 +14,7 @@
</label> </label>
</div> </div>
<div class="form-group"> <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"> <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> <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)"> <span class="tooltip-content" *ngIf="targetEndpoint.errors && targetEndpoint.errors.required && (targetEndpoint.dirty || targetEndpoint.touched)">
@ -30,17 +23,17 @@
</label> </label>
</div> </div>
<div class="form-group"> <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"> <input type="text" class="col-md-8" id="destination_username" [disabled]="testOngoing" [(ngModel)]="target.username" size="20" name="username" #username="ngModel">
</div> </div>
<div class="form-group"> <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"> <input type="password" class="col-md-8" id="destination_password" [disabled]="testOngoing" [(ngModel)]="target.password" size="20" name="password" #password="ngModel">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="spin" class="col-md-4"></label> <label for="spin" class="col-md-4"></label>
<span class="col-md-8 spinner spinner-inline" [hidden]="!testOngoing"></span> <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> </div>
</section> </section>
</form> </form>

View File

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

View File

@ -16,8 +16,8 @@
<clr-dg-cell>{{t.tag}}</clr-dg-cell> <clr-dg-cell>{{t.tag}}</clr-dg-cell>
<clr-dg-cell>{{t.pullCommand}}</clr-dg-cell> <clr-dg-cell>{{t.pullCommand}}</clr-dg-cell>
<clr-dg-cell> <clr-dg-cell>
<clr-icon shape="check" *ngIf="t.verified" style="color: #1D5100;"></clr-icon> <clr-icon shape="check" *ngIf="t.signed" style="color: #1D5100;"></clr-icon>
<clr-icon shape="close" *ngIf="!t.verified" style="color: #C92100;"></clr-icon> <clr-icon shape="close" *ngIf="!t.signed" style="color: #C92100;"></clr-icon>
</clr-dg-cell> </clr-dg-cell>
<clr-dg-cell>{{t.author}}</clr-dg-cell> <clr-dg-cell>{{t.author}}</clr-dg-cell>
<clr-dg-cell>{{t.created | date: 'yyyy/MM/dd'}}</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; repoName: string;
tags: TagView[]; tags: TagView[];
registryUrl: string;
private subscription: Subscription; private subscription: Subscription;
@ -66,6 +67,7 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
this.projectId = this.route.snapshot.params['id']; this.projectId = this.route.snapshot.params['id'];
this.repoName = this.route.snapshot.params['repo']; this.repoName = this.route.snapshot.params['repo'];
this.tags = []; this.tags = [];
this.registryUrl = this.appConfigService.getConfig().registry_url;
this.retrieve(); this.retrieve();
} }
@ -87,10 +89,10 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
let data = JSON.parse(t.manifest.history[0].v1Compatibility); let data = JSON.parse(t.manifest.history[0].v1Compatibility);
tag.architecture = data['architecture']; tag.architecture = data['architecture'];
tag.author = data['author']; tag.author = data['author'];
tag.verified = t.signed; tag.signed = t.signed;
tag.created = data['created']; tag.created = data['created'];
tag.dockerVersion = data['docker_version']; 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']; tag.os = data['os'];
this.tags.push(tag); this.tags.push(tag);
}); });
@ -100,18 +102,20 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
deleteTag(tag: TagView) { deleteTag(tag: TagView) {
if (tag) { if (tag) {
let titleKey: string, summaryKey: string; let titleKey: string, summaryKey: string, content: string;
if (tag.verified) { if (tag.signed) {
titleKey = 'REPOSITORY.DELETION_TITLE_TAG_DENIED'; titleKey = 'REPOSITORY.DELETION_TITLE_TAG_DENIED';
summaryKey = 'REPOSITORY.DELETION_SUMMARY_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 { } else {
titleKey = 'REPOSITORY.DELETION_TITLE_TAG'; titleKey = 'REPOSITORY.DELETION_TITLE_TAG';
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG'; summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG';
content = tag.tag;
} }
let message = new ConfirmationMessage( let message = new ConfirmationMessage(
titleKey, titleKey,
summaryKey, summaryKey,
tag.tag, content,
tag, tag,
ConfirmationTargets.TAG); ConfirmationTargets.TAG);
this.deletionDialogService.openComfirmDialog(message); this.deletionDialogService.openComfirmDialog(message);

View File

@ -1,7 +1,7 @@
export class TagView { export class TagView {
tag: string; tag: string;
pullCommand: string; pullCommand: string;
verified: boolean; signed: boolean;
author: string; author: string;
created: Date; created: Date;
dockerVersion: string; 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> <h3 class="modal-title">{{modalTitle}}</h3>
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert> <inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
<div class="modal-body"> <div class="modal-body">
<form #policyForm="ngForm"> <form #policyForm="ngForm">
<section class="form-block"> <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"> <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"> <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)"> <span class="tooltip-content" *ngIf="name.errors && name.errors.required && (name.dirty || name.touched)">
{{'REPLICATION.NAME_IS_REQUIRED'}} {{'REPLICATION.NAME_IS_REQUIRED' | translate}}
</span> </span>
</label> </label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="policy_description" class="col-md-4">{{'REPLICATION.DESCRIPTION' | translate}}</label> <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"> <input type="text" class="col-md-8" id="policy_description" [(ngModel)]="createEditPolicy.description" name="description" size="20" #description="ngModel" [disabled]="readonly">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="col-md-4">{{'REPLICATION.ENABLE' | translate}}</label> <label class="col-md-4">{{'REPLICATION.ENABLE' | translate}}</label>
<div class="checkbox-inline"> <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> <label for="policy_enable"></label>
</div> </div>
</div> </div>
<div class="form-group"> <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"> <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> <option *ngFor="let t of targets" [value]="t.id" [selected]="t.id == createEditPolicy.targetId">{{t.name}}</option>
</select> </select>
</div> </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"> <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)"> <span class="tooltip-content" *ngIf="targetName.errors && targetName.errors.required && (targetName.dirty || targetName.touched)">
{{'REPLICATION.DESTINATION_NAME_IS_REQUIRED' | translate}} {{'REPLICATION.DESTINATION_NAME_IS_REQUIRED' | translate}}
</span> </span>
</label> </label>
<div class="checkbox-inline"> <div class="checkbox-inline" *ngIf="showNewDestination">
<input type="checkbox" id="check_new" (click)="newDestination(checkedAddNew.checked)" #checkedAddNew [checked]="isCreateDestination" [disabled]="testOngoing"> <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> <label for="check_new">{{'REPLICATION.NEW_DESTINATION' | translate}}</label>
</div> </div>
</div> </div>
<div class="form-group"> <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"> <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)"> <span class="tooltip-content" *ngIf="endpointUrl.errors && endpointUrl.errors.required && (endpointUrl.dirty || endpointUrl.touched)">
{{'REPLICATION.DESTINATION_URL_IS_REQUIRED' | translate}} {{'REPLICATION.DESTINATION_URL_IS_REQUIRED' | translate}}
</span> </span>
</label> </label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="destination_username" class="col-md-4">{{'REPLICATION.DESTINATION_USERNAME' | translate}}</label> <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" [(ngModel)]="createEditPolicy.username" size="20" name="username" #username="ngModel"> <input type="text" class="col-md-8" id="destination_username" [disabled]="testOngoing || readonly" [(ngModel)]="createEditPolicy.username" size="20" name="username" #username="ngModel">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="destination_password" class="col-md-4">{{'REPLICATION.DESTINATION_PASSWORD' | translate}}</label> <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" [(ngModel)]="createEditPolicy.password" size="20" name="password" #password="ngModel"> <input type="password" class="col-md-8" id="destination_password" [disabled]="testOngoing || readonly" [(ngModel)]="createEditPolicy.password" size="20" name="password" #password="ngModel">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="spin" class="col-md-4"></label> <label for="spin" class="col-md-4"></label>
<span class="col-md-8 spinner spinner-inline" [hidden]="!testOngoing"></span> <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> </div>
</section> </section>
</form> </form>

View File

@ -24,6 +24,7 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
modalTitle: string; modalTitle: string;
createEditPolicyOpened: boolean; createEditPolicyOpened: boolean;
createEditPolicy: CreateEditPolicy = new CreateEditPolicy(); createEditPolicy: CreateEditPolicy = new CreateEditPolicy();
initVal: CreateEditPolicy = new CreateEditPolicy();
actionType: ActionType; actionType: ActionType;
@ -40,6 +41,9 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
policyForm: NgForm; policyForm: NgForm;
staticBackdrop: boolean = true;
closable: boolean = false;
@ViewChild('policyForm') @ViewChild('policyForm')
currentForm: NgForm; currentForm: NgForm;
@ -48,6 +52,18 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
@ViewChild(InlineAlertComponent) @ViewChild(InlineAlertComponent)
inlineAlert: 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( constructor(
private replicationService: ReplicationService, private replicationService: ReplicationService,
private messageService: MessageService, private messageService: MessageService,
@ -67,6 +83,11 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
this.createEditPolicy.endpointUrl = initialTarget.endpoint; this.createEditPolicy.endpointUrl = initialTarget.endpoint;
this.createEditPolicy.username = initialTarget.username; this.createEditPolicy.username = initialTarget.username;
this.createEditPolicy.password = initialTarget.password; 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) 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 { openCreateEditPolicy(policyId?: number): void {
this.createEditPolicyOpened = true; this.createEditPolicyOpened = true;
this.createEditPolicy = new CreateEditPolicy(); this.createEditPolicy = new CreateEditPolicy();
this.isCreateDestination = false; this.isCreateDestination = false;
this.hasChanged = false; this.hasChanged = false;
@ -97,7 +119,11 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
this.createEditPolicy.name = policy.name; this.createEditPolicy.name = policy.name;
this.createEditPolicy.description = policy.description; this.createEditPolicy.description = policy.description;
this.createEditPolicy.enable = policy.enabled === 1? true : false; 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 { } else {
@ -218,12 +244,14 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'}); this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'});
} else { } else {
this.createEditPolicyOpened = false; this.createEditPolicyOpened = false;
this.policyForm.reset();
} }
} }
confirmCancel(confirmed: boolean) { confirmCancel(confirmed: boolean) {
this.createEditPolicyOpened = false; this.createEditPolicyOpened = false;
this.inlineAlert.close(); this.inlineAlert.close();
this.policyForm.reset();
} }
ngAfterViewChecked(): void { ngAfterViewChecked(): void {
@ -231,24 +259,20 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
if(this.policyForm) { if(this.policyForm) {
this.policyForm.valueChanges.subscribe(data=>{ this.policyForm.valueChanges.subscribe(data=>{
for(let i in data) { for(let i in data) {
let item = data[i]; let origin = this.initVal[i];
if(typeof item === 'string' && (<string>item).trim().length !== 0) { let current = data[i];
this.hasChanged = true; if(current && current !== origin) {
break;
} else if (typeof item === 'boolean' && (<boolean>item)) {
this.hasChanged = true; this.hasChanged = true;
break; break;
} else { } else {
this.hasChanged = false; this.hasChanged = false;
this.inlineAlert.close(); this.inlineAlert.close();
break;
} }
} }
}); });
} }
} }
testConnection() { testConnection() {
this.pingStatus = true; this.pingStatus = true;
this.translateService.get('REPLICATION.TESTING_CONNECTION').subscribe(res=>this.pingTestMessage=res); 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-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> <clr-dg-action-overflow>
<button class="action-item" (click)="editPolicy(p)">{{'REPLICATION.EDIT_POLICY' | translate}}</button> <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> <button class="action-item" (click)="deletePolicy(p)">{{'REPLICATION.DELETE_POLICY' | translate}}</button>
</clr-dg-action-overflow> </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 *ngIf="projectless">{{p.project_name}}</clr-dg-cell>
<clr-dg-cell>{{p.description}}</clr-dg-cell> <clr-dg-cell>{{p.description}}</clr-dg-cell>
<clr-dg-cell>{{p.target_name}}</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() editOne = new EventEmitter<number>();
@Output() toggleOne = new EventEmitter<Policy>(); @Output() toggleOne = new EventEmitter<Policy>();
toggleSubscription: Subscription;
subscription: Subscription; subscription: Subscription;
constructor( constructor(
private replicationService: ReplicationService, private replicationService: ReplicationService,
private toggleConfirmDialogService: ConfirmationDialogService,
private deletionDialogService: ConfirmationDialogService, private deletionDialogService: ConfirmationDialogService,
private messageService: MessageService) { 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$ .confirmationConfirm$
.subscribe( .subscribe(
message => { message => {
if (message && if (message &&
message.source === ConfirmationTargets.POLICY && message.source === ConfirmationTargets.POLICY &&
message.state === ConfirmationState.CONFIRMED) { message.state === ConfirmationState.CONFIRMED) {
this.replicationService this.replicationService
.deletePolicy(message.data) .deletePolicy(message.data)
.subscribe( .subscribe(
response => { response => {
console.log('Successful delete policy with ID:' + message.data); console.log('Successful delete policy with ID:' + message.data);
this.reload.emit(true); this.reload.emit(true);
}, },
error => this.messageService.announceMessage(error.status, 'Failed to delete policy with ID:' + message.data, AlertType.DANGER) 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) { if (this.subscription) {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
if(this.toggleSubscription) {
this.toggleSubscription.unsubscribe();
}
} }
selectPolicy(policy: Policy): void { selectPolicy(policy: Policy): void {
@ -74,13 +99,14 @@ export class ListPolicyComponent implements OnDestroy {
} }
togglePolicy(policy: Policy) { togglePolicy(policy: Policy) {
policy.enabled = policy.enabled === 0 ? 1 : 0; let toggleConfirmMessage: ConfirmationMessage = new ConfirmationMessage(
console.log('Enable policy ID:' + policy.id + ' with activation status ' + policy.enabled); policy.enabled === 1 ? 'REPLICATION.TOGGLE_DISABLE_TITLE' : 'REPLICATION.TOGGLE_ENABLE_TITLE',
this.replicationService.enablePolicy(policy.id, policy.enabled) policy.enabled === 1 ? 'REPLICATION.CONFIRM_TOGGLE_DISABLE_POLICY': 'REPLICATION.CONFIRM_TOGGLE_ENABLE_POLICY',
.subscribe( policy.name,
res => console.log('Successful toggled policy status'), policy,
error => this.messageService.announceMessage(error.status, "Failed to toggle policy status.", AlertType.DANGER) ConfirmationTargets.TOGGLE_CONFIRM
); );
this.toggleConfirmDialogService.openComfirmDialog(toggleConfirmMessage);
} }
deletePolicy(policy: Policy) { deletePolicy(policy: Policy) {

View File

@ -2,4 +2,9 @@
margin: 0px !important; margin: 0px !important;
padding: 0px !important; padding: 0px !important;
margin-top: -5px !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"> <div class="form-group form-group-override">
<label for="username" class="required form-group-label-override">{{'PROFILE.USER_NAME' | translate}}</label> <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")'> <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)' (input)='handleValidation("username", false)'
(focusout)='handleValidation("username", true)'> (focusout)='handleValidation("username", true)'>
<span class="tooltip-content"> <span class="tooltip-content">
{{usernameTooltip | translate}} {{usernameTooltip | translate}}
</span> </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>
<div class="form-group form-group-override"> <div class="form-group form-group-override">
<label for="email" class="required form-group-label-override">{{'PROFILE.EMAIL' | translate}}</label> <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")'> <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" <input name="email" type="text" #eamilInput="ngModel" [(ngModel)]="newUser.email"
placeholder='{{"PLACEHOLDER.MAIL" | translate}}'
required 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)' (input)='handleValidation("email", false)'
(focusout)='handleValidation("email", true)'> (focusout)='handleValidation("email", true)'>
<span class="tooltip-content"> <span class="tooltip-content">
{{emailTooltip | translate}} {{emailTooltip | translate}}
</span> </span>
</label> </label>
<label *ngIf="isSelfRegistration" role="tooltip" aria-haspopup="true" class="tooltip tooltip-bottom-left"> <span class="spinner spinner-inline spinner-pos" [hidden]='isChecking("email")'></span>
<clr-icon shape="info" class="is-info" size="24"></clr-icon> <label class="sub-label-for-input" *ngIf="isSelfRegistration">{{'TOOLTIP.SIGN_UP_MAIL' | translate}}</label>
<span class="tooltip-content">
{{'TOOLTIP.SIGN_UP_MAIL' | translate}}
</span>
</label><span class="spinner spinner-inline" [hidden]='isChecking("email")'></span>
</div> </div>
<div class="form-group form-group-override"> <div class="form-group form-group-override">
<label for="realname" class="required form-group-label-override">{{'PROFILE.FULL_NAME' | translate}}</label> <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")'> <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)' (input)='handleValidation("realname", false)'
(focusout)='handleValidation("realname", true)'> (focusout)='handleValidation("realname", true)'>
<span class="tooltip-content"> <span class="tooltip-content">
{{'TOOLTIP.FULL_NAME' | translate}} {{'TOOLTIP.FULL_NAME' | translate}}
</span> </span>
</label> </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>
<div class="form-group form-group-override"> <div class="form-group form-group-override">
<label for="newPassword" class="required form-group-label-override">{{'PROFILE.PASSWORD' | translate}}</label> <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")'> <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}}' <input type="password" id="newPassword"
required 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" name="newPassword"
[(ngModel)]="newUser.password" [(ngModel)]="newUser.password"
#newPassInput="ngModel" size="28" #newPassInput="ngModel" size="40"
(input)='handleValidation("newPassword", false)' (input)='handleValidation("newPassword", false)'
(focusout)='handleValidation("newPassword", true)'> (focusout)='handleValidation("newPassword", true)'>
<span class="tooltip-content"> <span class="tooltip-content">
{{'TOOLTIP.PASSWORD' | translate}} {{'TOOLTIP.PASSWORD' | translate}}
</span> </span>
</label> </label>
<label *ngIf="isSelfRegistration" role="tooltip" aria-haspopup="true" class="tooltip tooltip-bottom-left"> <label class="sub-label-for-input" *ngIf="isSelfRegistration">{{'CHANGE_PWD.PASS_TIPS' | translate}}</label>
<clr-icon shape="info" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">
{{'TOOLTIP.PASSWORD' | translate}}
</span>
</label>
</div> </div>
<div class="form-group form-group-override"> <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" 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")'> <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 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" name="confirmPassword"
[(ngModel)]="confirmedPwd" [(ngModel)]="confirmedPwd"
#confirmPassInput="ngModel" size="28" #confirmPassInput="ngModel" size="40"
(input)='handleValidation("confirmPassword", false)' (input)='handleValidation("confirmPassword", false)'
(focusout)='handleValidation("confirmPassword", true)'> (focusout)='handleValidation("confirmPassword", true)'>
<span class="tooltip-content"> <span class="tooltip-content">
@ -90,7 +74,7 @@
<div class="form-group form-group-override"> <div class="form-group form-group-override">
<label for="comment" class="form-group-label-override">{{'PROFILE.COMMENT' | translate}}</label> <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")'> <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)' (input)='handleValidation("comment", false)'
(focusout)='handleValidation("comment", true)'> (focusout)='handleValidation("comment", true)'>
<span class="tooltip-content"> <span class="tooltip-content">
@ -100,5 +84,4 @@
</div> </div>
</section> </section>
</form> </form>
</div> </div>
<div style="height: 15px;"></div>

View File

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

View File

@ -19,6 +19,7 @@ export const enum ConfirmationTargets {
PROJECT_MEMBER, PROJECT_MEMBER,
USER, USER,
POLICY, POLICY,
TOGGLE_CONFIRM,
TARGET, TARGET,
REPOSITORY, REPOSITORY,
TAG, 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", "TITLE": "User Profile",
"USER_NAME": "Username", "USER_NAME": "Username",
"EMAIL": "Email", "EMAIL": "Email",
"FULL_NAME": "Full name", "FULL_NAME": "First and last name",
"COMMENT": "Comments", "COMMENT": "Comments",
"PASSWORD": "Password", "PASSWORD": "Password",
"SAVE_SUCCESS": "User profile saved successfully" "SAVE_SUCCESS": "User profile saved successfully"
@ -66,7 +66,8 @@
"CURRENT_PWD": "Current Password", "CURRENT_PWD": "Current Password",
"NEW_PWD": "New Password", "NEW_PWD": "New Password",
"CONFIRM_PWD": "Confirm 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": { "ACCOUNT_SETTINGS": {
"PROFILE": "User Profile", "PROFILE": "User Profile",
@ -152,6 +153,7 @@
"DELETE": "Delete", "DELETE": "Delete",
"ITEMS": "item(s)", "ITEMS": "item(s)",
"ACTIONS": "Actions", "ACTIONS": "Actions",
"USERNAME_IS_REQUIRED": "Username is required",
"USERNAME_DOES_NOT_EXISTS": "Username does not exist.", "USERNAME_DOES_NOT_EXISTS": "Username does not exist.",
"USERNAME_ALREADY_EXISTS": "Username already exists.", "USERNAME_ALREADY_EXISTS": "Username already exists.",
"UNKNOWN_ERROR": "Unknown error occurred while adding member.", "UNKNOWN_ERROR": "Unknown error occurred while adding member.",
@ -229,7 +231,11 @@
"CREATION_TIME": "Start Time", "CREATION_TIME": "Start Time",
"END_TIME": "End Time", "END_TIME": "End Time",
"LOGS": "Logs", "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": { "DESTINATION": {
"NEW_ENDPOINT": "New Endpoint", "NEW_ENDPOINT": "New Endpoint",
@ -268,7 +274,7 @@
"DELETION_TITLE_TAG": "Confirm Tag Deletion", "DELETION_TITLE_TAG": "Confirm Tag Deletion",
"DELETION_SUMMARY_TAG": "Do you want to delete tag {{param}}?", "DELETION_SUMMARY_TAG": "Do you want to delete tag {{param}}?",
"DELETION_TITLE_TAG_DENIED": "Signed Tag can't be deleted", "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", "FILTER_FOR_REPOSITORIES": "Filter for repositories",
"TAG": "Tag", "TAG": "Tag",
"SIGNED": "Signed", "SIGNED": "Signed",

View File

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

View File

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

View File

@ -31,5 +31,6 @@ services:
volumes: volumes:
- /data/config/:/etc/adminserver/ - /data/config/:/etc/adminserver/
- /data/secretkey:/etc/adminserver/key - /data/secretkey:/etc/adminserver/key
- /data/:/data/
ports: ports:
- 8888:80 - 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